agnostic-prompt-aps 1.1.5__py3-none-any.whl → 1.1.6__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.
- {agnostic_prompt_aps-1.1.5.dist-info → agnostic_prompt_aps-1.1.6.dist-info}/METADATA +2 -1
- {agnostic_prompt_aps-1.1.5.dist-info → agnostic_prompt_aps-1.1.6.dist-info}/RECORD +18 -14
- {agnostic_prompt_aps-1.1.5.dist-info → agnostic_prompt_aps-1.1.6.dist-info}/WHEEL +1 -1
- aps_cli/__init__.py +1 -1
- aps_cli/__main__.py +1 -0
- aps_cli/cli.py +491 -124
- aps_cli/core.py +222 -13
- aps_cli/payload/agnostic-prompt-standard/SKILL.md +1 -1
- aps_cli/payload/agnostic-prompt-standard/assets/formats/format-docs-index-v1.0.0.example.md +25 -0
- aps_cli/payload/agnostic-prompt-standard/platforms/_schemas/platform-manifest.schema.json +6 -2
- aps_cli/payload/agnostic-prompt-standard/platforms/claude-code/manifest.json +9 -0
- aps_cli/payload/agnostic-prompt-standard/platforms/opencode/manifest.json +32 -0
- aps_cli/payload/agnostic-prompt-standard/platforms/opencode/tools-registry.json +4 -0
- aps_cli/payload/agnostic-prompt-standard/platforms/vscode-copilot/manifest.json +7 -0
- aps_cli/schemas.py +170 -0
- {agnostic_prompt_aps-1.1.5.dist-info → agnostic_prompt_aps-1.1.6.dist-info}/entry_points.txt +0 -0
- {agnostic_prompt_aps-1.1.5.dist-info → agnostic_prompt_aps-1.1.6.dist-info}/licenses/LICENSE +0 -0
- {agnostic_prompt_aps-1.1.5.dist-info → agnostic_prompt_aps-1.1.6.dist-info}/top_level.txt +0 -0
aps_cli/core.py
CHANGED
|
@@ -3,21 +3,51 @@ from __future__ import annotations
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
5
|
import shutil
|
|
6
|
-
|
|
6
|
+
import sys
|
|
7
|
+
from dataclasses import dataclass, field
|
|
7
8
|
from pathlib import Path
|
|
8
|
-
from typing import
|
|
9
|
+
from typing import Callable, Literal, Optional
|
|
10
|
+
|
|
11
|
+
from .schemas import safe_parse_platform_manifest, normalize_detection_marker
|
|
9
12
|
|
|
10
13
|
SKILL_ID = "agnostic-prompt-standard"
|
|
11
14
|
|
|
15
|
+
# Explicit ordering for known adapters in UI
|
|
16
|
+
DEFAULT_ADAPTER_ORDER: tuple[str, ...] = ("vscode-copilot", "claude-code", "opencode")
|
|
17
|
+
|
|
18
|
+
KnownAdapterId = Literal["vscode-copilot", "claude-code", "opencode"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class DetectionMarker:
|
|
23
|
+
"""A marker file or directory used to detect a platform."""
|
|
24
|
+
|
|
25
|
+
kind: Literal["file", "dir"]
|
|
26
|
+
label: str
|
|
27
|
+
rel_path: str
|
|
28
|
+
|
|
12
29
|
|
|
13
30
|
@dataclass(frozen=True)
|
|
14
31
|
class Platform:
|
|
32
|
+
"""Information about a platform adapter."""
|
|
33
|
+
|
|
15
34
|
platform_id: str
|
|
16
35
|
display_name: str
|
|
17
36
|
adapter_version: Optional[str]
|
|
37
|
+
detection_markers: tuple[DetectionMarker, ...] = field(default_factory=tuple)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class AdapterDetection:
|
|
42
|
+
"""Result of detecting a platform adapter in a workspace."""
|
|
43
|
+
|
|
44
|
+
platform_id: str
|
|
45
|
+
detected: bool
|
|
46
|
+
reasons: tuple[str, ...]
|
|
18
47
|
|
|
19
48
|
|
|
20
49
|
def is_tty() -> bool:
|
|
50
|
+
"""Check if running in interactive terminal."""
|
|
21
51
|
try:
|
|
22
52
|
return bool(os.isatty(0) and os.isatty(1))
|
|
23
53
|
except Exception:
|
|
@@ -25,6 +55,7 @@ def is_tty() -> bool:
|
|
|
25
55
|
|
|
26
56
|
|
|
27
57
|
def find_repo_root(start_dir: Path) -> Optional[Path]:
|
|
58
|
+
"""Find git repository root by walking up from start directory."""
|
|
28
59
|
cur = start_dir.resolve()
|
|
29
60
|
while True:
|
|
30
61
|
if (cur / ".git").exists():
|
|
@@ -35,6 +66,13 @@ def find_repo_root(start_dir: Path) -> Optional[Path]:
|
|
|
35
66
|
cur = parent
|
|
36
67
|
|
|
37
68
|
|
|
69
|
+
def pick_workspace_root(cli_root: Optional[str]) -> Optional[Path]:
|
|
70
|
+
"""Resolve workspace root from CLI option or auto-detect."""
|
|
71
|
+
if cli_root:
|
|
72
|
+
return Path(cli_root).expanduser().resolve()
|
|
73
|
+
return find_repo_root(Path.cwd())
|
|
74
|
+
|
|
75
|
+
|
|
38
76
|
def default_project_skill_path(repo_root: Path, *, claude: bool = False) -> Path:
|
|
39
77
|
"""Return the project-skill path for the detected agent ecosystem.
|
|
40
78
|
|
|
@@ -55,11 +93,70 @@ def default_personal_skill_path(*, claude: bool = False) -> Path:
|
|
|
55
93
|
return base / SKILL_ID
|
|
56
94
|
|
|
57
95
|
|
|
96
|
+
def is_claude_platform(platform_id: str) -> bool:
|
|
97
|
+
"""Check if platform uses Claude-specific paths."""
|
|
98
|
+
return platform_id == "claude-code"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def compute_skill_destinations(
|
|
102
|
+
scope: Literal["repo", "personal"],
|
|
103
|
+
workspace_root: Optional[Path],
|
|
104
|
+
selected_platforms: list[str],
|
|
105
|
+
) -> list[Path]:
|
|
106
|
+
"""Compute skill installation destinations based on selected platforms.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
scope: Installation scope (repo or personal)
|
|
110
|
+
workspace_root: Workspace root path (required for repo scope)
|
|
111
|
+
selected_platforms: List of selected platform IDs
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
List of unique destination paths
|
|
115
|
+
"""
|
|
116
|
+
wants_claude = any(is_claude_platform(p) for p in selected_platforms)
|
|
117
|
+
wants_non_claude = any(not is_claude_platform(p) for p in selected_platforms)
|
|
118
|
+
|
|
119
|
+
# Default to non-Claude location if no adapters selected
|
|
120
|
+
include_claude = wants_claude
|
|
121
|
+
include_non_claude = wants_non_claude or len(selected_platforms) == 0
|
|
122
|
+
|
|
123
|
+
if scope == "repo":
|
|
124
|
+
if not workspace_root:
|
|
125
|
+
raise ValueError("Repo install selected but no workspace root found.")
|
|
126
|
+
dests: list[Path] = []
|
|
127
|
+
if include_non_claude:
|
|
128
|
+
dests.append(default_project_skill_path(workspace_root, claude=False))
|
|
129
|
+
if include_claude:
|
|
130
|
+
dests.append(default_project_skill_path(workspace_root, claude=True))
|
|
131
|
+
return _unique_paths(dests)
|
|
132
|
+
|
|
133
|
+
dests = []
|
|
134
|
+
if include_non_claude:
|
|
135
|
+
dests.append(default_personal_skill_path(claude=False))
|
|
136
|
+
if include_claude:
|
|
137
|
+
dests.append(default_personal_skill_path(claude=True))
|
|
138
|
+
return _unique_paths(dests)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _unique_paths(paths: list[Path]) -> list[Path]:
|
|
142
|
+
"""Remove duplicate paths while preserving order."""
|
|
143
|
+
seen: set[Path] = set()
|
|
144
|
+
out: list[Path] = []
|
|
145
|
+
for p in paths:
|
|
146
|
+
if p not in seen:
|
|
147
|
+
seen.add(p)
|
|
148
|
+
out.append(p)
|
|
149
|
+
return out
|
|
150
|
+
|
|
151
|
+
|
|
58
152
|
def infer_platform_id(workspace_root: Path) -> Optional[str]:
|
|
153
|
+
"""Infer platform ID based on workspace directory structure (legacy)."""
|
|
59
154
|
gh = workspace_root / ".github"
|
|
60
155
|
has_agents = (gh / "agents").exists()
|
|
61
156
|
has_prompts = (gh / "prompts").exists()
|
|
62
|
-
has_instructions = (gh / "copilot-instructions.md").exists() or (
|
|
157
|
+
has_instructions = (gh / "copilot-instructions.md").exists() or (
|
|
158
|
+
gh / "instructions"
|
|
159
|
+
).exists()
|
|
63
160
|
if has_agents or has_prompts or has_instructions:
|
|
64
161
|
return "vscode-copilot"
|
|
65
162
|
return None
|
|
@@ -83,12 +180,16 @@ def resolve_payload_skill_dir() -> Path:
|
|
|
83
180
|
if dev.is_dir():
|
|
84
181
|
return dev
|
|
85
182
|
|
|
86
|
-
raise FileNotFoundError(
|
|
183
|
+
raise FileNotFoundError(
|
|
184
|
+
"APS payload not found. (Did you run tools/sync_payload.py before building?)"
|
|
185
|
+
)
|
|
87
186
|
|
|
88
187
|
|
|
89
188
|
def load_platforms(skill_dir: Path) -> list[Platform]:
|
|
189
|
+
"""Load all platform adapters from the skill's platforms directory."""
|
|
90
190
|
platforms_dir = skill_dir / "platforms"
|
|
91
191
|
out: list[Platform] = []
|
|
192
|
+
|
|
92
193
|
for entry in platforms_dir.iterdir():
|
|
93
194
|
if not entry.is_dir():
|
|
94
195
|
continue
|
|
@@ -97,42 +198,152 @@ def load_platforms(skill_dir: Path) -> list[Platform]:
|
|
|
97
198
|
manifest_path = entry / "manifest.json"
|
|
98
199
|
if not manifest_path.exists():
|
|
99
200
|
continue
|
|
201
|
+
|
|
100
202
|
try:
|
|
101
|
-
|
|
203
|
+
raw = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
102
204
|
except Exception:
|
|
103
205
|
continue
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
206
|
+
|
|
207
|
+
# Validate with Pydantic
|
|
208
|
+
manifest, error = safe_parse_platform_manifest(raw)
|
|
209
|
+
if error:
|
|
210
|
+
print(
|
|
211
|
+
f"Warning: Invalid platform manifest at {manifest_path}: {error}",
|
|
212
|
+
file=sys.stderr,
|
|
213
|
+
)
|
|
214
|
+
# Fall back to partial extraction
|
|
215
|
+
platform_id = raw.get("platformId", entry.name)
|
|
216
|
+
display_name = raw.get("displayName", entry.name)
|
|
217
|
+
adapter_version = raw.get("adapterVersion")
|
|
218
|
+
detection_markers: tuple[DetectionMarker, ...] = ()
|
|
219
|
+
else:
|
|
220
|
+
assert manifest is not None
|
|
221
|
+
platform_id = manifest.platform_id
|
|
222
|
+
display_name = manifest.display_name
|
|
223
|
+
adapter_version = manifest.adapter_version
|
|
224
|
+
# Get normalized detection markers from manifest
|
|
225
|
+
detection_markers = tuple(
|
|
226
|
+
DetectionMarker(
|
|
227
|
+
kind=m.kind, # type: ignore[arg-type]
|
|
228
|
+
label=m.label,
|
|
229
|
+
rel_path=m.rel_path,
|
|
230
|
+
)
|
|
231
|
+
for m in manifest.detection_markers
|
|
232
|
+
)
|
|
233
|
+
|
|
107
234
|
out.append(
|
|
108
235
|
Platform(
|
|
109
236
|
platform_id=platform_id,
|
|
110
237
|
display_name=display_name,
|
|
111
238
|
adapter_version=adapter_version,
|
|
239
|
+
detection_markers=detection_markers,
|
|
112
240
|
)
|
|
113
241
|
)
|
|
114
|
-
|
|
242
|
+
|
|
243
|
+
return out
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def sort_platforms_for_ui(platforms: list[Platform]) -> list[Platform]:
|
|
247
|
+
"""Sort platforms with known adapters first in defined order."""
|
|
248
|
+
known_order = {pid: i for i, pid in enumerate(DEFAULT_ADAPTER_ORDER)}
|
|
249
|
+
known = [p for p in platforms if p.platform_id in known_order]
|
|
250
|
+
remaining = [p for p in platforms if p.platform_id not in known_order]
|
|
251
|
+
|
|
252
|
+
known.sort(key=lambda p: known_order[p.platform_id])
|
|
253
|
+
remaining.sort(key=lambda p: p.display_name.lower())
|
|
254
|
+
|
|
255
|
+
return known + remaining
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _marker_exists(workspace_root: Path, marker: DetectionMarker) -> bool:
|
|
259
|
+
"""Check if a marker file or directory exists."""
|
|
260
|
+
full = workspace_root / marker.rel_path
|
|
261
|
+
if marker.kind == "dir":
|
|
262
|
+
return full.is_dir()
|
|
263
|
+
return full.exists()
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def detect_adapters(
|
|
267
|
+
workspace_root: Path, platforms: list[Platform]
|
|
268
|
+
) -> dict[str, AdapterDetection]:
|
|
269
|
+
"""Detect which platform adapters are present in a workspace.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
workspace_root: Workspace root directory
|
|
273
|
+
platforms: List of platforms with detection markers
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
Dict mapping platform IDs to detection results
|
|
277
|
+
"""
|
|
278
|
+
out: dict[str, AdapterDetection] = {}
|
|
279
|
+
|
|
280
|
+
for platform in platforms:
|
|
281
|
+
reasons: list[str] = []
|
|
282
|
+
for marker in platform.detection_markers:
|
|
283
|
+
if _marker_exists(workspace_root, marker):
|
|
284
|
+
reasons.append(marker.label)
|
|
285
|
+
|
|
286
|
+
out[platform.platform_id] = AdapterDetection(
|
|
287
|
+
platform_id=platform.platform_id,
|
|
288
|
+
detected=len(reasons) > 0,
|
|
289
|
+
reasons=tuple(reasons),
|
|
290
|
+
)
|
|
291
|
+
|
|
115
292
|
return out
|
|
116
293
|
|
|
117
294
|
|
|
295
|
+
def format_detection_label(detection: AdapterDetection) -> str:
|
|
296
|
+
"""Format a detection result as a label suffix."""
|
|
297
|
+
if not detection.detected:
|
|
298
|
+
return ""
|
|
299
|
+
return " (detected)"
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def detect_platforms(workspace_root: Path, skill_dir: Path) -> list[str]:
|
|
303
|
+
"""Detect all platforms with markers present in workspace (legacy API).
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
workspace_root: Path to workspace root
|
|
307
|
+
skill_dir: Path to skill directory with platform manifests
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
List of detected platform IDs
|
|
311
|
+
"""
|
|
312
|
+
platforms = load_platforms(skill_dir)
|
|
313
|
+
detections = detect_adapters(workspace_root, platforms)
|
|
314
|
+
return [pid for pid, det in detections.items() if det.detected]
|
|
315
|
+
|
|
316
|
+
|
|
118
317
|
def ensure_dir(p: Path) -> None:
|
|
318
|
+
"""Ensure a directory exists, creating it recursively if needed."""
|
|
119
319
|
p.mkdir(parents=True, exist_ok=True)
|
|
120
320
|
|
|
121
321
|
|
|
122
322
|
def remove_dir(p: Path) -> None:
|
|
323
|
+
"""Remove a directory recursively."""
|
|
123
324
|
shutil.rmtree(p, ignore_errors=True)
|
|
124
325
|
|
|
125
326
|
|
|
126
327
|
def copy_dir(src: Path, dst: Path) -> None:
|
|
328
|
+
"""Copy a directory recursively."""
|
|
127
329
|
shutil.copytree(src, dst)
|
|
128
330
|
|
|
129
331
|
|
|
332
|
+
def list_files_recursive(root_dir: Path) -> list[Path]:
|
|
333
|
+
"""Recursively list all files in a directory."""
|
|
334
|
+
results: list[Path] = []
|
|
335
|
+
for item in root_dir.rglob("*"):
|
|
336
|
+
if item.is_file():
|
|
337
|
+
results.append(item)
|
|
338
|
+
return results
|
|
339
|
+
|
|
340
|
+
|
|
130
341
|
def copy_template_tree(
|
|
131
342
|
src_dir: Path,
|
|
132
343
|
dst_root: Path,
|
|
133
344
|
*,
|
|
134
345
|
force: bool = False,
|
|
135
|
-
filter_fn: Optional[
|
|
346
|
+
filter_fn: Optional[Callable[[str], bool]] = None,
|
|
136
347
|
) -> list[str]:
|
|
137
348
|
"""Copy template files individually with optional filtering.
|
|
138
349
|
|
|
@@ -159,6 +370,4 @@ def copy_template_tree(
|
|
|
159
370
|
dst_file.parent.mkdir(parents=True, exist_ok=True)
|
|
160
371
|
shutil.copy2(src_file, dst_file)
|
|
161
372
|
copied.append(rel_str)
|
|
162
|
-
return copied
|
|
163
|
-
|
|
164
|
-
|
|
373
|
+
return copied
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<format id="DOCS_INDEX_V1" name="Documentation Index" purpose="Token-efficient hierarchical documentation map for AI navigation.">
|
|
2
|
+
# <PROJECT_TITLE> Documentation Map
|
|
3
|
+
|
|
4
|
+
> Fetch the complete documentation index at: <INDEX_URL>
|
|
5
|
+
> Last updated: <TIMESTAMP>
|
|
6
|
+
|
|
7
|
+
## <GROUP_NAME>
|
|
8
|
+
|
|
9
|
+
### [<PAGE_TITLE>](<PAGE_URL>)
|
|
10
|
+
* <HEADING_TEXT>
|
|
11
|
+
* <SUBHEADING_TEXT>
|
|
12
|
+
|
|
13
|
+
...
|
|
14
|
+
|
|
15
|
+
WHERE:
|
|
16
|
+
- <PROJECT_TITLE> is String; name of the project or documentation set.
|
|
17
|
+
- <INDEX_URL> is URI; URL where this index can be fetched.
|
|
18
|
+
- <TIMESTAMP> is ISO8601; when the index was generated.
|
|
19
|
+
- <GROUP_NAME> is String; documentation section/category name.
|
|
20
|
+
- <PAGE_TITLE> is String; title of the documentation page.
|
|
21
|
+
- <PAGE_URL> is URI; link to the documentation page.
|
|
22
|
+
- <HEADING_TEXT> is String; H2/H3 heading text from the page.
|
|
23
|
+
- <SUBHEADING_TEXT> is String; nested heading under parent.
|
|
24
|
+
- ... denotes repetition; groups contain pages, pages contain headings.
|
|
25
|
+
</format>
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
"required": [
|
|
7
7
|
"platformId",
|
|
8
8
|
"displayName",
|
|
9
|
-
"adapterVersion"
|
|
9
|
+
"adapterVersion",
|
|
10
|
+
"fileConventions"
|
|
10
11
|
],
|
|
11
12
|
"properties": {
|
|
12
13
|
"platformId": {
|
|
@@ -60,8 +61,11 @@
|
|
|
60
61
|
},
|
|
61
62
|
"instructions": {
|
|
62
63
|
"type": "array",
|
|
64
|
+
"minItems": 1,
|
|
65
|
+
"uniqueItems": true,
|
|
63
66
|
"items": {
|
|
64
|
-
"type": "string"
|
|
67
|
+
"type": "string",
|
|
68
|
+
"minLength": 1
|
|
65
69
|
},
|
|
66
70
|
"description": "Instruction/memory file locations"
|
|
67
71
|
},
|
|
@@ -3,6 +3,15 @@
|
|
|
3
3
|
"displayName": "Claude Code CLI",
|
|
4
4
|
"adapterVersion": "1.0.0",
|
|
5
5
|
"lastUpdated": "2026-01-20",
|
|
6
|
+
"detectionMarkers": [
|
|
7
|
+
".claude",
|
|
8
|
+
"CLAUDE.md",
|
|
9
|
+
"CLAUDE.local.md",
|
|
10
|
+
".mcp.json",
|
|
11
|
+
".claude/settings.json",
|
|
12
|
+
".claude/agents",
|
|
13
|
+
".claude/rules"
|
|
14
|
+
],
|
|
6
15
|
"docs": {
|
|
7
16
|
"memory": "https://docs.anthropic.com/en/docs/claude-code/memory",
|
|
8
17
|
"settings": "https://docs.anthropic.com/en/docs/claude-code/settings",
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"platformId": "opencode",
|
|
3
|
+
"displayName": "OpenCode",
|
|
4
|
+
"adapterVersion": "1.0.0",
|
|
5
|
+
"lastUpdated": "2026-01-27",
|
|
6
|
+
"detectionMarkers": [
|
|
7
|
+
".opencode",
|
|
8
|
+
".opencode/opencode.jsonc",
|
|
9
|
+
".opencode/opencode.json",
|
|
10
|
+
"opencode.json",
|
|
11
|
+
"opencode.jsonc",
|
|
12
|
+
".opencode.json"
|
|
13
|
+
],
|
|
14
|
+
"docs": {
|
|
15
|
+
"official": "https://opencode.ai/docs",
|
|
16
|
+
"config": "https://opencode.ai/docs/config"
|
|
17
|
+
},
|
|
18
|
+
"fileConventions": {
|
|
19
|
+
"instructions": [
|
|
20
|
+
"AGENTS.md",
|
|
21
|
+
".opencode/instructions.md"
|
|
22
|
+
],
|
|
23
|
+
"config": [
|
|
24
|
+
".opencode/opencode.jsonc",
|
|
25
|
+
".opencode/opencode.json",
|
|
26
|
+
"opencode.json",
|
|
27
|
+
"opencode.jsonc",
|
|
28
|
+
".opencode.json"
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
"notes": "OpenCode is an AI coding agent by anomalyco. Configuration supports JSONC (JSON with comments). The AGENTS.md file follows the OpenAI agents.md standard and is a shared marker with other platforms."
|
|
32
|
+
}
|
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
"displayName": "VS Code + GitHub Copilot",
|
|
4
4
|
"adapterVersion": "1.0.0",
|
|
5
5
|
"lastUpdated": "2026-01-15",
|
|
6
|
+
"detectionMarkers": [
|
|
7
|
+
".github/copilot-instructions.md",
|
|
8
|
+
".github/agents",
|
|
9
|
+
".github/prompts",
|
|
10
|
+
".github/instructions",
|
|
11
|
+
".github/skills"
|
|
12
|
+
],
|
|
6
13
|
"docs": {
|
|
7
14
|
"agentSkills": "https://code.visualstudio.com/docs/copilot/customization/agent-skills",
|
|
8
15
|
"promptFiles": "https://code.visualstudio.com/docs/copilot/customization/prompt-files",
|
aps_cli/schemas.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Pydantic v2 schemas for manifest and skill validation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional, Union
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, Field, ValidationError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FileConventions(BaseModel):
|
|
11
|
+
"""File convention paths for a platform."""
|
|
12
|
+
|
|
13
|
+
instructions: Optional[list[str]] = None
|
|
14
|
+
agents: Optional[list[str]] = None
|
|
15
|
+
prompts: Optional[list[str]] = None
|
|
16
|
+
skills: Optional[list[str]] = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DetectionMarkerObject(BaseModel):
|
|
20
|
+
"""A marker file or directory used to detect a platform (object format)."""
|
|
21
|
+
|
|
22
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
23
|
+
|
|
24
|
+
kind: str = Field(..., pattern="^(file|dir)$")
|
|
25
|
+
label: str
|
|
26
|
+
rel_path: str = Field(..., alias="relPath")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class DetectionMarker(BaseModel):
|
|
30
|
+
"""Normalized detection marker with all fields."""
|
|
31
|
+
|
|
32
|
+
kind: str
|
|
33
|
+
label: str
|
|
34
|
+
rel_path: str
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def normalize_detection_marker(
|
|
38
|
+
input_marker: Union[str, dict, DetectionMarkerObject],
|
|
39
|
+
) -> DetectionMarker:
|
|
40
|
+
"""Convert a detection marker input (string or object) to normalized format.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
input_marker: String path or marker object/dict
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Normalized DetectionMarker
|
|
47
|
+
"""
|
|
48
|
+
if isinstance(input_marker, str):
|
|
49
|
+
# String format: ".github/agents/" -> dir, ".github/copilot-instructions.md" -> file
|
|
50
|
+
is_dir = input_marker.endswith("/")
|
|
51
|
+
rel_path = input_marker.rstrip("/") if is_dir else input_marker
|
|
52
|
+
return DetectionMarker(
|
|
53
|
+
kind="dir" if is_dir else "file",
|
|
54
|
+
label=input_marker,
|
|
55
|
+
rel_path=rel_path,
|
|
56
|
+
)
|
|
57
|
+
elif isinstance(input_marker, DetectionMarkerObject):
|
|
58
|
+
return DetectionMarker(
|
|
59
|
+
kind=input_marker.kind,
|
|
60
|
+
label=input_marker.label,
|
|
61
|
+
rel_path=input_marker.rel_path,
|
|
62
|
+
)
|
|
63
|
+
elif isinstance(input_marker, dict):
|
|
64
|
+
# Validate dict input to match Node's strictness.
|
|
65
|
+
try:
|
|
66
|
+
obj = DetectionMarkerObject.model_validate(input_marker)
|
|
67
|
+
except ValidationError as e:
|
|
68
|
+
raise ValueError(f"Invalid detection marker object: {e}") from e
|
|
69
|
+
return DetectionMarker(kind=obj.kind, label=obj.label, rel_path=obj.rel_path)
|
|
70
|
+
else:
|
|
71
|
+
raise ValueError(f"Invalid detection marker type: {type(input_marker)}")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class PlatformManifest(BaseModel):
|
|
75
|
+
"""Schema for platform manifest.json files."""
|
|
76
|
+
|
|
77
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
78
|
+
|
|
79
|
+
platform_id: str = Field(..., min_length=1, alias="platformId")
|
|
80
|
+
display_name: str = Field(..., min_length=1, alias="displayName")
|
|
81
|
+
adapter_version: Optional[str] = Field(None, alias="adapterVersion")
|
|
82
|
+
description: Optional[str] = None
|
|
83
|
+
skill_root: Optional[str] = Field(None, alias="skillRoot")
|
|
84
|
+
file_conventions: Optional[FileConventions] = Field(None, alias="fileConventions")
|
|
85
|
+
detection_markers_raw: Optional[list[Union[str, DetectionMarkerObject]]] = Field(
|
|
86
|
+
None, alias="detectionMarkers"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def detection_markers(self) -> list[DetectionMarker]:
|
|
91
|
+
"""Get normalized detection markers."""
|
|
92
|
+
if not self.detection_markers_raw:
|
|
93
|
+
return []
|
|
94
|
+
return [normalize_detection_marker(m) for m in self.detection_markers_raw]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class SkillFrontmatter(BaseModel):
|
|
98
|
+
"""Schema for SKILL.md frontmatter."""
|
|
99
|
+
|
|
100
|
+
model_config = ConfigDict(extra="allow")
|
|
101
|
+
|
|
102
|
+
name: Optional[str] = None
|
|
103
|
+
version: Optional[str] = None
|
|
104
|
+
description: Optional[str] = None
|
|
105
|
+
author: Optional[str] = None
|
|
106
|
+
license: Optional[str] = None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def parse_platform_manifest(data: dict) -> PlatformManifest:
|
|
110
|
+
"""Parse and validate a platform manifest.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
data: Raw manifest data
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Validated PlatformManifest
|
|
117
|
+
|
|
118
|
+
Raises:
|
|
119
|
+
ValidationError: If validation fails
|
|
120
|
+
"""
|
|
121
|
+
return PlatformManifest.model_validate(data)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def safe_parse_platform_manifest(
|
|
125
|
+
data: dict,
|
|
126
|
+
) -> tuple[Optional[PlatformManifest], Optional[ValidationError]]:
|
|
127
|
+
"""Safely parse a platform manifest without raising.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
data: Raw manifest data
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Tuple of (manifest, error) - one will be None
|
|
134
|
+
"""
|
|
135
|
+
try:
|
|
136
|
+
return parse_platform_manifest(data), None
|
|
137
|
+
except ValidationError as e:
|
|
138
|
+
return None, e
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def parse_skill_frontmatter(data: dict) -> SkillFrontmatter:
|
|
142
|
+
"""Parse and validate skill frontmatter.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
data: Raw frontmatter data
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Validated SkillFrontmatter
|
|
149
|
+
|
|
150
|
+
Raises:
|
|
151
|
+
ValidationError: If validation fails
|
|
152
|
+
"""
|
|
153
|
+
return SkillFrontmatter.model_validate(data)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def safe_parse_skill_frontmatter(
|
|
157
|
+
data: dict,
|
|
158
|
+
) -> tuple[Optional[SkillFrontmatter], Optional[ValidationError]]:
|
|
159
|
+
"""Safely parse skill frontmatter without raising.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
data: Raw frontmatter data
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
Tuple of (frontmatter, error) - one will be None
|
|
166
|
+
"""
|
|
167
|
+
try:
|
|
168
|
+
return parse_skill_frontmatter(data), None
|
|
169
|
+
except ValidationError as e:
|
|
170
|
+
return None, e
|
{agnostic_prompt_aps-1.1.5.dist-info → agnostic_prompt_aps-1.1.6.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{agnostic_prompt_aps-1.1.5.dist-info → agnostic_prompt_aps-1.1.6.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|