@sakanano-moe/agenthelm 6.5.1 → 6.5.2
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/package.json +1 -1
- package/runtime/agent_pack_config.py +30 -0
- package/runtime/agenthelm.py +46 -10
package/package.json
CHANGED
|
@@ -485,6 +485,34 @@ def _client_plugin_roots(client: str, home: Path) -> list[Path]:
|
|
|
485
485
|
return []
|
|
486
486
|
|
|
487
487
|
|
|
488
|
+
def _version_tuple(value: str) -> tuple[int, ...]:
|
|
489
|
+
numbers = re.findall(r"\d+", value.split("+", 1)[0])
|
|
490
|
+
return tuple(int(number) for number in numbers)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def _client_marketplace_plugins(client: str, home: Path) -> list[Path]:
|
|
494
|
+
"""Plugin roots the client installed from marketplaces (not agent-pack bootstrap copies)."""
|
|
495
|
+
found: list[Path] = []
|
|
496
|
+
if client == "claude":
|
|
497
|
+
try:
|
|
498
|
+
records = _read_json(home / ".claude" / "plugins" / "installed_plugins.json").get("plugins", {})
|
|
499
|
+
except AgentPackError:
|
|
500
|
+
records = {}
|
|
501
|
+
for entries in records.values() if isinstance(records, dict) else ():
|
|
502
|
+
for entry in entries if isinstance(entries, list) else ():
|
|
503
|
+
path = Path(str(entry.get("installPath", "")))
|
|
504
|
+
if (path / "plugin.json").is_file():
|
|
505
|
+
found.append(path)
|
|
506
|
+
elif client == "codex":
|
|
507
|
+
cache = home / ".codex" / "plugins" / "cache"
|
|
508
|
+
if cache.is_dir() and not cache.is_symlink():
|
|
509
|
+
for plugin_dir in sorted(path for market in cache.iterdir() if market.is_dir() for path in market.iterdir()):
|
|
510
|
+
versions = [path for path in plugin_dir.iterdir() if (path / "plugin.json").is_file()] if plugin_dir.is_dir() else []
|
|
511
|
+
if versions:
|
|
512
|
+
found.append(max(versions, key=lambda path: (_version_tuple(path.name), path.name)))
|
|
513
|
+
return found
|
|
514
|
+
|
|
515
|
+
|
|
488
516
|
def _claude_plugin_records(home: Path) -> Path:
|
|
489
517
|
"""Install records for Claude Code: one symlink per plugin to its source root."""
|
|
490
518
|
return home / ".claude" / "agent-pack" / "plugins"
|
|
@@ -527,6 +555,8 @@ def inventory_client(client: str, home: Path | None = None) -> Inventory:
|
|
|
527
555
|
for child in sorted(plugin_parent.iterdir(), key=lambda item: item.name):
|
|
528
556
|
if child.is_dir() and (child / "plugin.json").is_file():
|
|
529
557
|
_merge_inventory(inventory, _inventory_plugin(child, inventory.source))
|
|
558
|
+
for child in _client_marketplace_plugins(client, home):
|
|
559
|
+
_merge_inventory(inventory, _inventory_plugin(child, inventory.source))
|
|
530
560
|
known = {Path(item.path).resolve() for item in inventory.skills}
|
|
531
561
|
for skill_root in _client_skill_roots(client, home):
|
|
532
562
|
if not skill_root.is_dir() or skill_root.is_symlink():
|
package/runtime/agenthelm.py
CHANGED
|
@@ -37,7 +37,8 @@ DEFAULT_LATEST_URLS = (
|
|
|
37
37
|
ENV_LATEST = "AGENTHELM_LATEST_URL"
|
|
38
38
|
ENV_DISABLE = "AGENTHELM_NO_UPDATE_CHECK"
|
|
39
39
|
CACHE_TTL_SECONDS = 24 * 3600
|
|
40
|
-
|
|
40
|
+
# Interactive commands wait longer: some networks reach the site slowly.
|
|
41
|
+
FETCH_TIMEOUT_SECONDS = 10
|
|
41
42
|
HOOK_FETCH_TIMEOUT_SECONDS = 2
|
|
42
43
|
FAILURE_BACKOFF_SECONDS = 6 * 3600
|
|
43
44
|
CONTENT_LOCK = Path(".agents/moe.sakanano.agent-pack/content.lock.json")
|
|
@@ -133,10 +134,14 @@ def update_check_enabled(env: Env) -> bool:
|
|
|
133
134
|
|
|
134
135
|
# ---------------------------------------------------------------- cloud source
|
|
135
136
|
|
|
136
|
-
def
|
|
137
|
+
def _fetch_json(url: str, timeout: float = FETCH_TIMEOUT_SECONDS) -> Any:
|
|
137
138
|
request = urllib.request.Request(url, headers={"User-Agent": "agenthelm-check"})
|
|
138
139
|
with urllib.request.urlopen(request, timeout=timeout) as response: # noqa: S310
|
|
139
|
-
|
|
140
|
+
return json.loads(response.read().decode("utf-8"))
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _fetch(url: str, timeout: float = FETCH_TIMEOUT_SECONDS) -> dict:
|
|
144
|
+
data = _fetch_json(url, timeout)
|
|
140
145
|
if not isinstance(data, dict) or data.get("marketplace") != MARKETPLACE:
|
|
141
146
|
raise CheckError(f"{url} is not an agenthelm latest.json")
|
|
142
147
|
return data
|
|
@@ -331,7 +336,8 @@ def opinion_findings(project: Path, installed: list[dict]) -> list[dict]:
|
|
|
331
336
|
except (OSError, ValueError, KeyError, TypeError) as exc:
|
|
332
337
|
return [{"status": "error", "error": f"{OPINION_LOCK} 无法解析:{exc}"}]
|
|
333
338
|
with tempfile.TemporaryDirectory() as temporary:
|
|
334
|
-
|
|
339
|
+
# macOS temporary paths live under /var, a symlink opinion-manager refuses.
|
|
340
|
+
profile_file = Path(temporary).resolve() / "profile.json"
|
|
335
341
|
profile_file.write_text(json.dumps(profile, ensure_ascii=False), encoding="utf-8")
|
|
336
342
|
completed = subprocess.run(
|
|
337
343
|
[sys.executable, str(manager / "runtime" / "opinion_manager.py"), "updates",
|
|
@@ -523,7 +529,29 @@ def _safe_extract(archive: Path, destination: Path) -> None:
|
|
|
523
529
|
raise CheckError(f"发布包含越界路径:{member.name}")
|
|
524
530
|
if not (member.isfile() or member.isdir()):
|
|
525
531
|
raise CheckError(f"发布包含不支持的条目:{member.name}")
|
|
526
|
-
|
|
532
|
+
try:
|
|
533
|
+
bundle.extractall(destination, filter="data") # noqa: S202 - members validated above
|
|
534
|
+
except TypeError: # Python without extraction filters
|
|
535
|
+
bundle.extractall(destination) # noqa: S202
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def _plugin_versions(latest: dict) -> dict:
|
|
539
|
+
return {name: item.get("version") for name, item in latest.get("plugins", {}).items()}
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def github_release(latest: dict) -> dict:
|
|
543
|
+
"""Release details straight from GitHub, used when latest.json came from the raw fallback.
|
|
544
|
+
|
|
545
|
+
Accepted only when that tag ships exactly the plugin versions ``latest`` lists,
|
|
546
|
+
the same rule the site applies before attaching a release.
|
|
547
|
+
"""
|
|
548
|
+
try:
|
|
549
|
+
slug = github_slug(latest)
|
|
550
|
+
release = _fetch_json(f"https://github.com/{slug}/releases/latest/download/release.json")
|
|
551
|
+
tagged = _fetch_json(f"https://raw.githubusercontent.com/{slug}/{release['tag']}/latest.json")
|
|
552
|
+
except (OSError, ValueError, KeyError, TypeError, CheckError, urllib.error.URLError):
|
|
553
|
+
return {}
|
|
554
|
+
return release if _plugin_versions(tagged) == _plugin_versions(latest) else {}
|
|
527
555
|
|
|
528
556
|
|
|
529
557
|
def release_source(latest: dict, workdir: Path, source: Path | None = None) -> Path:
|
|
@@ -533,7 +561,7 @@ def release_source(latest: dict, workdir: Path, source: Path | None = None) -> P
|
|
|
533
561
|
if not (root / "plugins").is_dir():
|
|
534
562
|
raise CheckError(f"{root} 不含 plugins/ 目录")
|
|
535
563
|
return root
|
|
536
|
-
release = latest.get("release") or
|
|
564
|
+
release = latest.get("release") or github_release(latest)
|
|
537
565
|
tag, digest = release.get("tag"), release.get("tarball_sha256")
|
|
538
566
|
if not tag or not digest:
|
|
539
567
|
raise CheckError("云端 latest.json 没有发布 tag 与摘要;请用 --source 指定本地仓库")
|
|
@@ -845,9 +873,15 @@ def recovery_dir_for(env: Env, label: str) -> Path:
|
|
|
845
873
|
return env.home / ".agent-pack" / "recovery" / f"{stamp}-agenthelm-{label}"
|
|
846
874
|
|
|
847
875
|
|
|
848
|
-
def locate(env: Env, plugin: str, version: str | None = None) -> dict:
|
|
849
|
-
"""Where a plugin is installed, newest first: the stable lookup for downstream wrappers.
|
|
850
|
-
|
|
876
|
+
def locate(env: Env, plugin: str, version: str | None = None, *, include_legacy: bool = False) -> dict:
|
|
877
|
+
"""Where a plugin is installed, newest first: the stable lookup for downstream wrappers.
|
|
878
|
+
|
|
879
|
+
Legacy copies are left out unless asked for, so a wrapper that works today
|
|
880
|
+
keeps working after ``migrate`` retires them.
|
|
881
|
+
"""
|
|
882
|
+
found = [item for item in installed_plugins(env, {plugin})
|
|
883
|
+
if (version is None or item.get("version") == version)
|
|
884
|
+
and (include_legacy or item["client"] != "legacy")]
|
|
851
885
|
found.sort(key=lambda item: (version_key(item.get("version") or "") or (0,), item["client"] != "legacy"),
|
|
852
886
|
reverse=True)
|
|
853
887
|
return {"ok": bool(found), "plugin": plugin, "version": version,
|
|
@@ -930,6 +964,8 @@ def parser() -> argparse.ArgumentParser:
|
|
|
930
964
|
locate_parser = sub.add_parser("locate", help="输出插件的已安装位置(最新优先),供下游脚本查找")
|
|
931
965
|
locate_parser.add_argument("plugin")
|
|
932
966
|
locate_parser.add_argument("--version", help="只要这个版本")
|
|
967
|
+
locate_parser.add_argument("--include-legacy", action="store_true",
|
|
968
|
+
help="也列出旧方式安装(默认不列,避免清理后才失效)")
|
|
933
969
|
locate_parser.add_argument("--output", choices=("text", "json"), default="json")
|
|
934
970
|
locate_parser.add_argument("--home", type=Path, help=argparse.SUPPRESS)
|
|
935
971
|
return result
|
|
@@ -966,7 +1002,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
966
1002
|
if args.command == "check" and args.hook:
|
|
967
1003
|
return hook(env, project, args.hook_format)
|
|
968
1004
|
if args.command == "locate":
|
|
969
|
-
found = locate(env, args.plugin, args.version)
|
|
1005
|
+
found = locate(env, args.plugin, args.version, include_legacy=args.include_legacy)
|
|
970
1006
|
if args.output == "json":
|
|
971
1007
|
print(json.dumps(found, ensure_ascii=False, indent=2))
|
|
972
1008
|
elif found["path"]:
|