opencodekit 0.20.7 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1 -1
- package/dist/template/.opencode/AGENTS.md +60 -0
- package/dist/template/.opencode/agent/build.md +3 -2
- package/dist/template/.opencode/agent/explore.md +14 -14
- package/dist/template/.opencode/agent/general.md +1 -1
- package/dist/template/.opencode/agent/plan.md +1 -1
- package/dist/template/.opencode/agent/review.md +1 -1
- package/dist/template/.opencode/agent/vision.md +0 -9
- package/dist/template/.opencode/memory.db +0 -0
- package/dist/template/.opencode/memory.db-shm +0 -0
- package/dist/template/.opencode/memory.db-wal +0 -0
- package/dist/template/.opencode/opencode.json +83 -614
- package/dist/template/.opencode/opencodex-fast.jsonc +1 -1
- package/dist/template/.opencode/package.json +1 -1
- package/dist/template/.opencode/plugin/copilot-auth.ts +27 -12
- package/dist/template/.opencode/plugin/prompt-leverage.ts +193 -0
- package/dist/template/.opencode/plugin/prompt-leverage.ts.bak +228 -0
- package/dist/template/.opencode/plugin/sdk/copilot/copilot-provider.ts +14 -2
- package/dist/template/.opencode/plugin/sdk/copilot/index.ts +2 -2
- package/dist/template/.opencode/plugin/sdk/copilot/responses/convert-to-openai-responses-input.ts +335 -0
- package/dist/template/.opencode/plugin/sdk/copilot/responses/map-openai-responses-finish-reason.ts +22 -0
- package/dist/template/.opencode/plugin/sdk/copilot/responses/openai-config.ts +18 -0
- package/dist/template/.opencode/plugin/sdk/copilot/responses/openai-error.ts +22 -0
- package/dist/template/.opencode/plugin/sdk/copilot/responses/openai-responses-api-types.ts +214 -0
- package/dist/template/.opencode/plugin/sdk/copilot/responses/openai-responses-language-model.ts +1770 -0
- package/dist/template/.opencode/plugin/sdk/copilot/responses/openai-responses-prepare-tools.ts +173 -0
- package/dist/template/.opencode/plugin/sdk/copilot/responses/openai-responses-settings.ts +1 -0
- package/dist/template/.opencode/plugin/sdk/copilot/responses/tool/code-interpreter.ts +87 -0
- package/dist/template/.opencode/plugin/sdk/copilot/responses/tool/file-search.ts +127 -0
- package/dist/template/.opencode/plugin/sdk/copilot/responses/tool/image-generation.ts +114 -0
- package/dist/template/.opencode/plugin/sdk/copilot/responses/tool/local-shell.ts +64 -0
- package/dist/template/.opencode/plugin/sdk/copilot/responses/tool/web-search-preview.ts +103 -0
- package/dist/template/.opencode/plugin/sdk/copilot/responses/tool/web-search.ts +102 -0
- package/dist/template/.opencode/pnpm-lock.yaml +791 -9
- package/dist/template/.opencode/skill/api-and-interface-design/SKILL.md +162 -0
- package/dist/template/.opencode/skill/beads/SKILL.md +10 -9
- package/dist/template/.opencode/skill/beads/references/MULTI_AGENT.md +10 -10
- package/dist/template/.opencode/skill/ci-cd-and-automation/SKILL.md +202 -0
- package/dist/template/.opencode/skill/code-search-patterns/SKILL.md +253 -0
- package/dist/template/.opencode/skill/code-simplification/SKILL.md +211 -0
- package/dist/template/.opencode/skill/condition-based-waiting/SKILL.md +12 -0
- package/dist/template/.opencode/skill/defense-in-depth/SKILL.md +16 -6
- package/dist/template/.opencode/skill/deprecation-and-migration/SKILL.md +189 -0
- package/dist/template/.opencode/skill/development-lifecycle/SKILL.md +12 -48
- package/dist/template/.opencode/skill/documentation-and-adrs/SKILL.md +220 -0
- package/dist/template/.opencode/skill/gh-address-comments/SKILL.md +29 -0
- package/dist/template/.opencode/skill/gh-address-comments/scripts/fetch_comments.py +237 -0
- package/dist/template/.opencode/skill/gh-fix-ci/SKILL.md +38 -0
- package/dist/template/.opencode/skill/gh-fix-ci/scripts/inspect_pr_checks.py +509 -0
- package/dist/template/.opencode/skill/incremental-implementation/SKILL.md +191 -0
- package/dist/template/.opencode/skill/performance-optimization/SKILL.md +236 -0
- package/dist/template/.opencode/skill/prompt-leverage/SKILL.md +90 -0
- package/dist/template/.opencode/skill/prompt-leverage/references/framework.md +91 -0
- package/dist/template/.opencode/skill/prompt-leverage/scripts/augment_prompt.py +157 -0
- package/dist/template/.opencode/skill/receiving-code-review/SKILL.md +11 -0
- package/dist/template/.opencode/skill/screenshot/SKILL.md +48 -0
- package/dist/template/.opencode/skill/screenshot/scripts/ensure_macos_permissions.sh +54 -0
- package/dist/template/.opencode/skill/screenshot/scripts/macos_display_info.swift +22 -0
- package/dist/template/.opencode/skill/screenshot/scripts/macos_permissions.swift +40 -0
- package/dist/template/.opencode/skill/screenshot/scripts/macos_window_info.swift +126 -0
- package/dist/template/.opencode/skill/screenshot/scripts/take_screenshot.ps1 +163 -0
- package/dist/template/.opencode/skill/screenshot/scripts/take_screenshot.py +585 -0
- package/dist/template/.opencode/skill/security-and-hardening/SKILL.md +296 -0
- package/dist/template/.opencode/skill/security-threat-model/SKILL.md +36 -0
- package/dist/template/.opencode/skill/security-threat-model/references/prompt-template.md +255 -0
- package/dist/template/.opencode/skill/security-threat-model/references/security-controls-and-assets.md +32 -0
- package/dist/template/.opencode/skill/skill-installer/SKILL.md +58 -0
- package/dist/template/.opencode/skill/skill-installer/scripts/github_utils.py +21 -0
- package/dist/template/.opencode/skill/skill-installer/scripts/install-skill-from-github.py +313 -0
- package/dist/template/.opencode/skill/skill-installer/scripts/list-skills.py +106 -0
- package/dist/template/.opencode/skill/structured-edit/SKILL.md +10 -0
- package/dist/template/.opencode/skill/swarm-coordination/SKILL.md +66 -1
- package/package.json +1 -1
- package/dist/template/.opencode/skill/beads-bridge/SKILL.md +0 -321
- package/dist/template/.opencode/skill/code-navigation/SKILL.md +0 -130
- package/dist/template/.opencode/skill/mqdh/SKILL.md +0 -171
- package/dist/template/.opencode/skill/obsidian/SKILL.md +0 -192
- package/dist/template/.opencode/skill/obsidian/mcp.json +0 -22
- package/dist/template/.opencode/skill/pencil/SKILL.md +0 -72
- package/dist/template/.opencode/skill/ralph/SKILL.md +0 -296
- package/dist/template/.opencode/skill/tilth-cli/SKILL.md +0 -207
- package/dist/template/.opencode/skill/tool-priority/SKILL.md +0 -299
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Shared GitHub helpers for skill install scripts."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import urllib.request
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def github_request(url: str, user_agent: str) -> bytes:
|
|
11
|
+
headers = {"User-Agent": user_agent}
|
|
12
|
+
token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
|
|
13
|
+
if token:
|
|
14
|
+
headers["Authorization"] = f"token {token}"
|
|
15
|
+
req = urllib.request.Request(url, headers=headers)
|
|
16
|
+
with urllib.request.urlopen(req) as resp:
|
|
17
|
+
return resp.read()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def github_api_contents_url(repo: str, path: str, ref: str) -> str:
|
|
21
|
+
return f"https://api.github.com/repos/{repo}/contents/{path}?ref={ref}"
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Install a skill from a GitHub repo path into OpenCode skill directory."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
import os
|
|
9
|
+
import shutil
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
import tempfile
|
|
13
|
+
import urllib.error
|
|
14
|
+
import urllib.parse
|
|
15
|
+
import zipfile
|
|
16
|
+
|
|
17
|
+
from github_utils import github_request
|
|
18
|
+
|
|
19
|
+
DEFAULT_REF = "main"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class Args:
|
|
24
|
+
url: str | None = None
|
|
25
|
+
repo: str | None = None
|
|
26
|
+
path: list[str] | None = None
|
|
27
|
+
ref: str = DEFAULT_REF
|
|
28
|
+
dest: str | None = None
|
|
29
|
+
name: str | None = None
|
|
30
|
+
method: str = "auto"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class Source:
|
|
35
|
+
owner: str
|
|
36
|
+
repo: str
|
|
37
|
+
ref: str
|
|
38
|
+
paths: list[str]
|
|
39
|
+
repo_url: str | None = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class InstallError(Exception):
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _opencode_home() -> str:
|
|
47
|
+
return os.environ.get("OPENCODE_HOME", os.path.expanduser("~/.config/opencode"))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _tmp_root() -> str:
|
|
51
|
+
base = os.path.join(tempfile.gettempdir(), "opencode")
|
|
52
|
+
os.makedirs(base, exist_ok=True)
|
|
53
|
+
return base
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _request(url: str) -> bytes:
|
|
57
|
+
return github_request(url, "opencode-skill-install")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _parse_github_url(url: str, default_ref: str) -> tuple[str, str, str, str | None]:
|
|
61
|
+
parsed = urllib.parse.urlparse(url)
|
|
62
|
+
if parsed.netloc != "github.com":
|
|
63
|
+
raise InstallError("Only GitHub URLs are supported for download mode.")
|
|
64
|
+
parts = [p for p in parsed.path.split("/") if p]
|
|
65
|
+
if len(parts) < 2:
|
|
66
|
+
raise InstallError("Invalid GitHub URL.")
|
|
67
|
+
owner, repo = parts[0], parts[1]
|
|
68
|
+
ref = default_ref
|
|
69
|
+
subpath = ""
|
|
70
|
+
if len(parts) > 2:
|
|
71
|
+
if parts[2] in ("tree", "blob"):
|
|
72
|
+
if len(parts) < 4:
|
|
73
|
+
raise InstallError("GitHub URL missing ref or path.")
|
|
74
|
+
ref = parts[3]
|
|
75
|
+
subpath = "/".join(parts[4:])
|
|
76
|
+
else:
|
|
77
|
+
subpath = "/".join(parts[2:])
|
|
78
|
+
return owner, repo, ref, subpath or None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _download_repo_zip(owner: str, repo: str, ref: str, dest_dir: str) -> str:
|
|
82
|
+
zip_url = f"https://codeload.github.com/{owner}/{repo}/zip/{ref}"
|
|
83
|
+
zip_path = os.path.join(dest_dir, "repo.zip")
|
|
84
|
+
try:
|
|
85
|
+
payload = _request(zip_url)
|
|
86
|
+
except urllib.error.HTTPError as exc:
|
|
87
|
+
raise InstallError(f"Download failed: HTTP {exc.code}") from exc
|
|
88
|
+
with open(zip_path, "wb") as file_handle:
|
|
89
|
+
file_handle.write(payload)
|
|
90
|
+
with zipfile.ZipFile(zip_path, "r") as zip_file:
|
|
91
|
+
_safe_extract_zip(zip_file, dest_dir)
|
|
92
|
+
top_levels = {name.split("/")[0] for name in zip_file.namelist() if name}
|
|
93
|
+
if not top_levels:
|
|
94
|
+
raise InstallError("Downloaded archive was empty.")
|
|
95
|
+
if len(top_levels) != 1:
|
|
96
|
+
raise InstallError("Unexpected archive layout.")
|
|
97
|
+
return os.path.join(dest_dir, next(iter(top_levels)))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _run_git(args: list[str]) -> None:
|
|
101
|
+
result = subprocess.run(
|
|
102
|
+
args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
|
|
103
|
+
)
|
|
104
|
+
if result.returncode != 0:
|
|
105
|
+
raise InstallError(result.stderr.strip() or "Git command failed.")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _safe_extract_zip(zip_file: zipfile.ZipFile, dest_dir: str) -> None:
|
|
109
|
+
dest_root = os.path.realpath(dest_dir)
|
|
110
|
+
for info in zip_file.infolist():
|
|
111
|
+
extracted_path = os.path.realpath(os.path.join(dest_dir, info.filename))
|
|
112
|
+
if extracted_path == dest_root or extracted_path.startswith(dest_root + os.sep):
|
|
113
|
+
continue
|
|
114
|
+
raise InstallError("Archive contains files outside the destination.")
|
|
115
|
+
zip_file.extractall(dest_dir)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _validate_relative_path(path: str) -> None:
|
|
119
|
+
if os.path.isabs(path) or os.path.normpath(path).startswith(".."):
|
|
120
|
+
raise InstallError("Skill path must be a relative path inside the repo.")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _validate_skill_name(name: str) -> None:
|
|
124
|
+
altsep = os.path.altsep
|
|
125
|
+
if not name or os.path.sep in name or (altsep and altsep in name):
|
|
126
|
+
raise InstallError("Skill name must be a single path segment.")
|
|
127
|
+
if name in (".", ".."):
|
|
128
|
+
raise InstallError("Invalid skill name.")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _git_sparse_checkout(
|
|
132
|
+
repo_url: str, ref: str, paths: list[str], dest_dir: str
|
|
133
|
+
) -> str:
|
|
134
|
+
repo_dir = os.path.join(dest_dir, "repo")
|
|
135
|
+
clone_cmd = [
|
|
136
|
+
"git",
|
|
137
|
+
"clone",
|
|
138
|
+
"--filter=blob:none",
|
|
139
|
+
"--depth",
|
|
140
|
+
"1",
|
|
141
|
+
"--sparse",
|
|
142
|
+
"--single-branch",
|
|
143
|
+
"--branch",
|
|
144
|
+
ref,
|
|
145
|
+
repo_url,
|
|
146
|
+
repo_dir,
|
|
147
|
+
]
|
|
148
|
+
try:
|
|
149
|
+
_run_git(clone_cmd)
|
|
150
|
+
except InstallError:
|
|
151
|
+
_run_git(
|
|
152
|
+
[
|
|
153
|
+
"git",
|
|
154
|
+
"clone",
|
|
155
|
+
"--filter=blob:none",
|
|
156
|
+
"--depth",
|
|
157
|
+
"1",
|
|
158
|
+
"--sparse",
|
|
159
|
+
"--single-branch",
|
|
160
|
+
repo_url,
|
|
161
|
+
repo_dir,
|
|
162
|
+
]
|
|
163
|
+
)
|
|
164
|
+
_run_git(["git", "-C", repo_dir, "sparse-checkout", "set", *paths])
|
|
165
|
+
_run_git(["git", "-C", repo_dir, "checkout", ref])
|
|
166
|
+
return repo_dir
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _validate_skill(path: str) -> None:
|
|
170
|
+
if not os.path.isdir(path):
|
|
171
|
+
raise InstallError(f"Skill path not found: {path}")
|
|
172
|
+
skill_md = os.path.join(path, "SKILL.md")
|
|
173
|
+
if not os.path.isfile(skill_md):
|
|
174
|
+
raise InstallError("SKILL.md not found in selected skill directory.")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _copy_skill(src: str, dest_dir: str) -> None:
|
|
178
|
+
os.makedirs(os.path.dirname(dest_dir), exist_ok=True)
|
|
179
|
+
if os.path.exists(dest_dir):
|
|
180
|
+
raise InstallError(f"Destination already exists: {dest_dir}")
|
|
181
|
+
shutil.copytree(src, dest_dir)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _build_repo_url(owner: str, repo: str) -> str:
|
|
185
|
+
return f"https://github.com/{owner}/{repo}.git"
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _build_repo_ssh(owner: str, repo: str) -> str:
|
|
189
|
+
return f"git@github.com:{owner}/{repo}.git"
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _prepare_repo(source: Source, method: str, tmp_dir: str) -> str:
|
|
193
|
+
if method in ("download", "auto"):
|
|
194
|
+
try:
|
|
195
|
+
return _download_repo_zip(source.owner, source.repo, source.ref, tmp_dir)
|
|
196
|
+
except InstallError as exc:
|
|
197
|
+
if method == "download":
|
|
198
|
+
raise
|
|
199
|
+
err_msg = str(exc)
|
|
200
|
+
if "HTTP 401" in err_msg or "HTTP 403" in err_msg or "HTTP 404" in err_msg:
|
|
201
|
+
pass
|
|
202
|
+
else:
|
|
203
|
+
raise
|
|
204
|
+
if method in ("git", "auto"):
|
|
205
|
+
repo_url = source.repo_url or _build_repo_url(source.owner, source.repo)
|
|
206
|
+
try:
|
|
207
|
+
return _git_sparse_checkout(repo_url, source.ref, source.paths, tmp_dir)
|
|
208
|
+
except InstallError:
|
|
209
|
+
repo_url = _build_repo_ssh(source.owner, source.repo)
|
|
210
|
+
return _git_sparse_checkout(repo_url, source.ref, source.paths, tmp_dir)
|
|
211
|
+
raise InstallError("Unsupported method.")
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _resolve_source(args: Args) -> Source:
|
|
215
|
+
if args.url:
|
|
216
|
+
owner, repo, ref, url_path = _parse_github_url(args.url, args.ref)
|
|
217
|
+
if args.path is not None:
|
|
218
|
+
paths = list(args.path)
|
|
219
|
+
elif url_path:
|
|
220
|
+
paths = [url_path]
|
|
221
|
+
else:
|
|
222
|
+
paths = []
|
|
223
|
+
if not paths:
|
|
224
|
+
raise InstallError("Missing --path for GitHub URL.")
|
|
225
|
+
return Source(owner=owner, repo=repo, ref=ref, paths=paths)
|
|
226
|
+
|
|
227
|
+
if not args.repo:
|
|
228
|
+
raise InstallError("Provide --repo or --url.")
|
|
229
|
+
if "://" in args.repo:
|
|
230
|
+
return _resolve_source(
|
|
231
|
+
Args(url=args.repo, repo=None, path=args.path, ref=args.ref)
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
repo_parts = [p for p in args.repo.split("/") if p]
|
|
235
|
+
if len(repo_parts) != 2:
|
|
236
|
+
raise InstallError("--repo must be in owner/repo format.")
|
|
237
|
+
if not args.path:
|
|
238
|
+
raise InstallError("Missing --path for --repo.")
|
|
239
|
+
paths = list(args.path)
|
|
240
|
+
return Source(
|
|
241
|
+
owner=repo_parts[0],
|
|
242
|
+
repo=repo_parts[1],
|
|
243
|
+
ref=args.ref,
|
|
244
|
+
paths=paths,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _default_dest() -> str:
|
|
249
|
+
return os.path.join(_opencode_home(), "skill")
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _parse_args(argv: list[str]) -> Args:
|
|
253
|
+
parser = argparse.ArgumentParser(description="Install a skill from GitHub.")
|
|
254
|
+
parser.add_argument("--repo", help="owner/repo")
|
|
255
|
+
parser.add_argument("--url", help="https://github.com/owner/repo[/tree/ref/path]")
|
|
256
|
+
parser.add_argument(
|
|
257
|
+
"--path",
|
|
258
|
+
nargs="+",
|
|
259
|
+
help="Path(s) to skill(s) inside repo",
|
|
260
|
+
)
|
|
261
|
+
parser.add_argument("--ref", default=DEFAULT_REF)
|
|
262
|
+
parser.add_argument("--dest", help="Destination skills directory")
|
|
263
|
+
parser.add_argument(
|
|
264
|
+
"--name", help="Destination skill name (defaults to basename of path)"
|
|
265
|
+
)
|
|
266
|
+
parser.add_argument(
|
|
267
|
+
"--method",
|
|
268
|
+
choices=["auto", "download", "git"],
|
|
269
|
+
default="auto",
|
|
270
|
+
)
|
|
271
|
+
return parser.parse_args(argv, namespace=Args())
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def main(argv: list[str]) -> int:
|
|
275
|
+
args = _parse_args(argv)
|
|
276
|
+
try:
|
|
277
|
+
source = _resolve_source(args)
|
|
278
|
+
source.ref = source.ref or args.ref
|
|
279
|
+
if not source.paths:
|
|
280
|
+
raise InstallError("No skill paths provided.")
|
|
281
|
+
for path in source.paths:
|
|
282
|
+
_validate_relative_path(path)
|
|
283
|
+
dest_root = args.dest or _default_dest()
|
|
284
|
+
tmp_dir = tempfile.mkdtemp(prefix="skill-install-", dir=_tmp_root())
|
|
285
|
+
try:
|
|
286
|
+
repo_root = _prepare_repo(source, args.method, tmp_dir)
|
|
287
|
+
installed = []
|
|
288
|
+
for path in source.paths:
|
|
289
|
+
skill_name = args.name if len(source.paths) == 1 else None
|
|
290
|
+
skill_name = skill_name or os.path.basename(path.rstrip("/"))
|
|
291
|
+
_validate_skill_name(skill_name)
|
|
292
|
+
if not skill_name:
|
|
293
|
+
raise InstallError("Unable to derive skill name.")
|
|
294
|
+
dest_dir = os.path.join(dest_root, skill_name)
|
|
295
|
+
if os.path.exists(dest_dir):
|
|
296
|
+
raise InstallError(f"Destination already exists: {dest_dir}")
|
|
297
|
+
skill_src = os.path.join(repo_root, path)
|
|
298
|
+
_validate_skill(skill_src)
|
|
299
|
+
_copy_skill(skill_src, dest_dir)
|
|
300
|
+
installed.append((skill_name, dest_dir))
|
|
301
|
+
finally:
|
|
302
|
+
if os.path.isdir(tmp_dir):
|
|
303
|
+
shutil.rmtree(tmp_dir, ignore_errors=True)
|
|
304
|
+
for skill_name, dest_dir in installed:
|
|
305
|
+
print(f"Installed {skill_name} to {dest_dir}")
|
|
306
|
+
return 0
|
|
307
|
+
except InstallError as exc:
|
|
308
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
309
|
+
return 1
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
if __name__ == "__main__":
|
|
313
|
+
raise SystemExit(main(sys.argv[1:]))
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""List skills from a GitHub repo path."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import urllib.error
|
|
11
|
+
|
|
12
|
+
from github_utils import github_api_contents_url, github_request
|
|
13
|
+
|
|
14
|
+
DEFAULT_REPO = "openai/skills"
|
|
15
|
+
DEFAULT_PATH = "skills/.curated"
|
|
16
|
+
DEFAULT_REF = "main"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ListError(Exception):
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Args(argparse.Namespace):
|
|
24
|
+
repo: str
|
|
25
|
+
path: str
|
|
26
|
+
ref: str
|
|
27
|
+
format: str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _request(url: str) -> bytes:
|
|
31
|
+
return github_request(url, "opencode-skill-list")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _opencode_home() -> str:
|
|
35
|
+
return os.environ.get("OPENCODE_HOME", os.path.expanduser("~/.config/opencode"))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _installed_skills() -> set[str]:
|
|
39
|
+
root = os.path.join(_opencode_home(), "skill")
|
|
40
|
+
if not os.path.isdir(root):
|
|
41
|
+
return set()
|
|
42
|
+
entries = set()
|
|
43
|
+
for name in os.listdir(root):
|
|
44
|
+
path = os.path.join(root, name)
|
|
45
|
+
if os.path.isdir(path):
|
|
46
|
+
entries.add(name)
|
|
47
|
+
return entries
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _list_skills(repo: str, path: str, ref: str) -> list[str]:
|
|
51
|
+
api_url = github_api_contents_url(repo, path, ref)
|
|
52
|
+
try:
|
|
53
|
+
payload = _request(api_url)
|
|
54
|
+
except urllib.error.HTTPError as exc:
|
|
55
|
+
if exc.code == 404:
|
|
56
|
+
raise ListError(
|
|
57
|
+
f"Skills path not found: https://github.com/{repo}/tree/{ref}/{path}"
|
|
58
|
+
) from exc
|
|
59
|
+
raise ListError(f"Failed to fetch skills: HTTP {exc.code}") from exc
|
|
60
|
+
data = json.loads(payload.decode("utf-8"))
|
|
61
|
+
if not isinstance(data, list):
|
|
62
|
+
raise ListError("Unexpected skills listing response.")
|
|
63
|
+
skills = [item["name"] for item in data if item.get("type") == "dir"]
|
|
64
|
+
return sorted(skills)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _parse_args(argv: list[str]) -> Args:
|
|
68
|
+
parser = argparse.ArgumentParser(description="List skills.")
|
|
69
|
+
parser.add_argument("--repo", default=DEFAULT_REPO)
|
|
70
|
+
parser.add_argument(
|
|
71
|
+
"--path",
|
|
72
|
+
default=DEFAULT_PATH,
|
|
73
|
+
help="Repo path to list (default: skills/.curated)",
|
|
74
|
+
)
|
|
75
|
+
parser.add_argument("--ref", default=DEFAULT_REF)
|
|
76
|
+
parser.add_argument(
|
|
77
|
+
"--format",
|
|
78
|
+
choices=["text", "json"],
|
|
79
|
+
default="text",
|
|
80
|
+
help="Output format",
|
|
81
|
+
)
|
|
82
|
+
return parser.parse_args(argv, namespace=Args())
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def main(argv: list[str]) -> int:
|
|
86
|
+
args = _parse_args(argv)
|
|
87
|
+
try:
|
|
88
|
+
skills = _list_skills(args.repo, args.path, args.ref)
|
|
89
|
+
installed = _installed_skills()
|
|
90
|
+
if args.format == "json":
|
|
91
|
+
payload = [
|
|
92
|
+
{"name": name, "installed": name in installed} for name in skills
|
|
93
|
+
]
|
|
94
|
+
print(json.dumps(payload))
|
|
95
|
+
else:
|
|
96
|
+
for idx, name in enumerate(skills, start=1):
|
|
97
|
+
suffix = " (already installed)" if name in installed else ""
|
|
98
|
+
print(f"{idx}. {name}{suffix}")
|
|
99
|
+
return 0
|
|
100
|
+
except ListError as exc:
|
|
101
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
102
|
+
return 1
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
if __name__ == "__main__":
|
|
106
|
+
raise SystemExit(main(sys.argv[1:]))
|
|
@@ -18,6 +18,16 @@ dependencies: []
|
|
|
18
18
|
- One-line edits where a direct edit is safe and unambiguous
|
|
19
19
|
- Large refactors better served by full rewrites or file splits
|
|
20
20
|
|
|
21
|
+
## Common Rationalizations
|
|
22
|
+
|
|
23
|
+
| Rationalization | Rebuttal |
|
|
24
|
+
| ------------------------------------------------- | ------------------------------------------------------------------------------------------- |
|
|
25
|
+
| "I remember what the file looks like" | You remember what it looked like 5 edits ago. Read it fresh |
|
|
26
|
+
| "The edit is simple, I don't need to verify" | Simple edits fail most often — whitespace, encoding, duplicate matches |
|
|
27
|
+
| "LSP lookup is an extra step I can skip" | LSP takes 1 call. Retrying a failed edit takes 3-5 calls |
|
|
28
|
+
| "I'll just use a larger context block to be safe" | Larger blocks = more chances for invisible character mismatches. Use minimal unique context |
|
|
29
|
+
| "The file hasn't changed since I last read it" | Other edits, formatters, and git operations can modify files between your reads |
|
|
30
|
+
|
|
21
31
|
## Overview
|
|
22
32
|
|
|
23
33
|
The `str_replace` edit tool is the #1 source of failures in LLM coding. Models reproduce content with subtle differences (whitespace, encoding, line endings) causing "string not found" errors.
|
|
@@ -8,7 +8,7 @@ description: >
|
|
|
8
8
|
version: "2.1.0"
|
|
9
9
|
license: MIT
|
|
10
10
|
tags: [agent-coordination, workflow]
|
|
11
|
-
dependencies: [beads
|
|
11
|
+
dependencies: [beads]
|
|
12
12
|
---
|
|
13
13
|
|
|
14
14
|
# Swarm Coordination - Kimi K2.5 PARL Multi-Agent Execution
|
|
@@ -137,6 +137,71 @@ SHUTDOWN:
|
|
|
137
137
|
- `monitor`: Progress tracking + visualization (actions: progress_update, render_block, status, clear)
|
|
138
138
|
- `sync`: Beads ↔ OpenCode todos (actions: push, pull, create_shared, get_shared, update_shared, list_shared)
|
|
139
139
|
|
|
140
|
+
## Beads Integration
|
|
141
|
+
|
|
142
|
+
### Session Start: Load Beads into Todos
|
|
143
|
+
|
|
144
|
+
Make Beads tasks visible to subagents:
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
// Push Beads to OpenCode todos
|
|
148
|
+
const result = await swarm({
|
|
149
|
+
operation: "sync",
|
|
150
|
+
action: "push",
|
|
151
|
+
filter: "open", // or "in_progress", "all"
|
|
152
|
+
});
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Session End: Sync Back to Beads
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
// Pull completed todos back to Beads
|
|
159
|
+
await swarm({ operation: "sync", action: "pull" });
|
|
160
|
+
|
|
161
|
+
// Clear swarm state
|
|
162
|
+
await swarm({
|
|
163
|
+
operation: "monitor",
|
|
164
|
+
action: "clear",
|
|
165
|
+
team_name: "api-refactor-swarm",
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Sync and commit
|
|
169
|
+
await bash("br sync --flush-only");
|
|
170
|
+
await bash("git add .beads/ && git commit -m 'close swarm'");
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Cross-Session Handoff
|
|
174
|
+
|
|
175
|
+
For work spanning multiple sessions, use shared task lists:
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
// Create shared list
|
|
179
|
+
const list = await swarm({
|
|
180
|
+
operation: "sync",
|
|
181
|
+
action: "create_shared",
|
|
182
|
+
name: "api-refactor-swarm",
|
|
183
|
+
tasks: JSON.stringify([
|
|
184
|
+
{ id: "task-1", content: "Refactor auth", status: "pending", priority: "high" },
|
|
185
|
+
]),
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Resume in new session
|
|
189
|
+
const shared = await swarm({
|
|
190
|
+
operation: "sync",
|
|
191
|
+
action: "get_shared",
|
|
192
|
+
list_id: "api-refactor-swarm",
|
|
193
|
+
});
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Data Locations
|
|
197
|
+
|
|
198
|
+
| Data Type | Location | Persistence |
|
|
199
|
+
| ----------------- | ----------------------------------------------- | ------------- |
|
|
200
|
+
| Beads tasks | `.beads/issues/*.md` | Git-backed |
|
|
201
|
+
| Swarm progress | `.beads/swarm-progress.jsonl` | Session |
|
|
202
|
+
| Shared task lists | `~/.local/share/opencode/storage/shared-tasks/` | Cross-session |
|
|
203
|
+
| OpenCode todos | `~/.local/share/opencode/storage/todo/` | Session |
|
|
204
|
+
|
|
140
205
|
## Rules
|
|
141
206
|
|
|
142
207
|
1. **Leader spawns, workers execute** - Clear role separation
|