@sakanano-moe/agenthelm 6.5.1 → 6.5.3
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 +115 -37
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
|
|
@@ -250,7 +255,9 @@ def plugin_findings(latest: dict, installed: list[dict]) -> list[dict]:
|
|
|
250
255
|
for item in installed:
|
|
251
256
|
target = latest.get("plugins", {}).get(item["plugin"], {}).get("version")
|
|
252
257
|
current = item.get("version") or ""
|
|
253
|
-
if
|
|
258
|
+
if item["client"] == "legacy":
|
|
259
|
+
status = "legacy" # a pre-marketplace copy: it gets migrated, not updated
|
|
260
|
+
elif target is None:
|
|
254
261
|
status = "unknown"
|
|
255
262
|
elif is_newer(target, current):
|
|
256
263
|
status = "behind"
|
|
@@ -331,7 +338,8 @@ def opinion_findings(project: Path, installed: list[dict]) -> list[dict]:
|
|
|
331
338
|
except (OSError, ValueError, KeyError, TypeError) as exc:
|
|
332
339
|
return [{"status": "error", "error": f"{OPINION_LOCK} 无法解析:{exc}"}]
|
|
333
340
|
with tempfile.TemporaryDirectory() as temporary:
|
|
334
|
-
|
|
341
|
+
# macOS temporary paths live under /var, a symlink opinion-manager refuses.
|
|
342
|
+
profile_file = Path(temporary).resolve() / "profile.json"
|
|
335
343
|
profile_file.write_text(json.dumps(profile, ensure_ascii=False), encoding="utf-8")
|
|
336
344
|
completed = subprocess.run(
|
|
337
345
|
[sys.executable, str(manager / "runtime" / "opinion_manager.py"), "updates",
|
|
@@ -415,7 +423,11 @@ def render_text(report: dict) -> str:
|
|
|
415
423
|
if not report["plugins"]:
|
|
416
424
|
lines.append("未发现已安装的 agenthelm 插件。")
|
|
417
425
|
for item in report["plugins"]:
|
|
418
|
-
label = {"behind": "可更新", "current": "最新", "ahead": "高于云端", "unknown": "版本未知"
|
|
426
|
+
label = {"behind": "可更新", "current": "最新", "ahead": "高于云端", "unknown": "版本未知",
|
|
427
|
+
"legacy": "旧方式安装,待迁移"}[item["status"]]
|
|
428
|
+
if item["status"] == "legacy":
|
|
429
|
+
lines.append(f" [旧方式] {item['plugin']} {item.get('version')} {label}")
|
|
430
|
+
continue
|
|
419
431
|
line = f" [{item['client']}] {item['plugin']} {item.get('version')} → {item.get('latest')} {label}"
|
|
420
432
|
if item["status"] == "behind":
|
|
421
433
|
kinds = {"code": "代码", "content": "仅模板/样例", "both": "代码与模板"}
|
|
@@ -430,8 +442,6 @@ def render_text(report: dict) -> str:
|
|
|
430
442
|
hint = UPDATE_HINTS.get(item["client"])
|
|
431
443
|
if hint:
|
|
432
444
|
line += f"\n 更新:{hint.format(plugin=item['plugin'])}"
|
|
433
|
-
elif item["client"] == "legacy":
|
|
434
|
-
line += f"\n 旧方式安装:{UPDATE_HINTS['legacy']}"
|
|
435
445
|
lines.append(line)
|
|
436
446
|
for item in report["content"]:
|
|
437
447
|
if item.get("pack") is None:
|
|
@@ -452,6 +462,8 @@ def render_text(report: dict) -> str:
|
|
|
452
462
|
if not report["plugins"]:
|
|
453
463
|
lines.append(f"运行 `{CLI} install` 预览安装。")
|
|
454
464
|
else:
|
|
465
|
+
if any(item["status"] == "legacy" for item in report["plugins"]):
|
|
466
|
+
lines.append(f"旧方式安装可用 `{UPDATE_HINTS['legacy']}` 预览迁移。")
|
|
455
467
|
lines.append(one_line(report) or "全部为最新。")
|
|
456
468
|
return "\n".join(lines) + "\n"
|
|
457
469
|
|
|
@@ -506,9 +518,9 @@ def github_slug(latest: dict) -> str:
|
|
|
506
518
|
|
|
507
519
|
def detect_clients(env: Env) -> list[str]:
|
|
508
520
|
found = []
|
|
509
|
-
if
|
|
521
|
+
if find_cli(env, "claude") or env.claude_dir.is_dir():
|
|
510
522
|
found.append("claude")
|
|
511
|
-
if
|
|
523
|
+
if find_cli(env, "codex") or env.codex_dir.is_dir():
|
|
512
524
|
found.append("codex")
|
|
513
525
|
if (env.home / ".cursor").is_dir():
|
|
514
526
|
found.append("cursor")
|
|
@@ -523,7 +535,29 @@ def _safe_extract(archive: Path, destination: Path) -> None:
|
|
|
523
535
|
raise CheckError(f"发布包含越界路径:{member.name}")
|
|
524
536
|
if not (member.isfile() or member.isdir()):
|
|
525
537
|
raise CheckError(f"发布包含不支持的条目:{member.name}")
|
|
526
|
-
|
|
538
|
+
try:
|
|
539
|
+
bundle.extractall(destination, filter="data") # noqa: S202 - members validated above
|
|
540
|
+
except TypeError: # Python without extraction filters
|
|
541
|
+
bundle.extractall(destination) # noqa: S202
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def _plugin_versions(latest: dict) -> dict:
|
|
545
|
+
return {name: item.get("version") for name, item in latest.get("plugins", {}).items()}
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def github_release(latest: dict) -> dict:
|
|
549
|
+
"""Release details straight from GitHub, used when latest.json came from the raw fallback.
|
|
550
|
+
|
|
551
|
+
Accepted only when that tag ships exactly the plugin versions ``latest`` lists,
|
|
552
|
+
the same rule the site applies before attaching a release.
|
|
553
|
+
"""
|
|
554
|
+
try:
|
|
555
|
+
slug = github_slug(latest)
|
|
556
|
+
release = _fetch_json(f"https://github.com/{slug}/releases/latest/download/release.json")
|
|
557
|
+
tagged = _fetch_json(f"https://raw.githubusercontent.com/{slug}/{release['tag']}/latest.json")
|
|
558
|
+
except (OSError, ValueError, KeyError, TypeError, CheckError, urllib.error.URLError):
|
|
559
|
+
return {}
|
|
560
|
+
return release if _plugin_versions(tagged) == _plugin_versions(latest) else {}
|
|
527
561
|
|
|
528
562
|
|
|
529
563
|
def release_source(latest: dict, workdir: Path, source: Path | None = None) -> Path:
|
|
@@ -533,7 +567,7 @@ def release_source(latest: dict, workdir: Path, source: Path | None = None) -> P
|
|
|
533
567
|
if not (root / "plugins").is_dir():
|
|
534
568
|
raise CheckError(f"{root} 不含 plugins/ 目录")
|
|
535
569
|
return root
|
|
536
|
-
release = latest.get("release") or
|
|
570
|
+
release = latest.get("release") or github_release(latest)
|
|
537
571
|
tag, digest = release.get("tag"), release.get("tarball_sha256")
|
|
538
572
|
if not tag or not digest:
|
|
539
573
|
raise CheckError("云端 latest.json 没有发布 tag 与摘要;请用 --source 指定本地仓库")
|
|
@@ -580,8 +614,34 @@ class Step:
|
|
|
580
614
|
return data
|
|
581
615
|
|
|
582
616
|
|
|
583
|
-
def
|
|
584
|
-
|
|
617
|
+
def known_cli_locations(env: Env, name: str) -> list[Path]:
|
|
618
|
+
"""Where a client's command line lives when it is not on PATH (GUI apps, per-user installs)."""
|
|
619
|
+
home = env.home
|
|
620
|
+
if name == "codex":
|
|
621
|
+
bundle = "ChatGPT.app/Contents/Resources/codex-cli/bin/codex"
|
|
622
|
+
return [Path("/Applications") / bundle, home / "Applications" / bundle,
|
|
623
|
+
home / ".local/bin/codex", Path("/opt/homebrew/bin/codex"), Path("/usr/local/bin/codex")]
|
|
624
|
+
return [home / ".local/bin" / name, Path("/opt/homebrew/bin") / name, Path("/usr/local/bin") / name]
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def find_cli(env: Env, name: str) -> str | None:
|
|
628
|
+
"""A client's command line: ``AGENTHELM_<NAME>`` override, then PATH, then the usual app locations."""
|
|
629
|
+
override = env.get(f"AGENTHELM_{name.upper()}")
|
|
630
|
+
if override and os.access(override, os.X_OK):
|
|
631
|
+
return override
|
|
632
|
+
on_path = shutil.which(name, path=env.get("PATH") or "")
|
|
633
|
+
if on_path:
|
|
634
|
+
return on_path
|
|
635
|
+
for candidate in known_cli_locations(env, name):
|
|
636
|
+
if candidate.is_file() and os.access(candidate, os.X_OK):
|
|
637
|
+
return str(candidate)
|
|
638
|
+
return None
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def _cli(env: Env, name: str, *args: str) -> tuple[list[str], str | None]:
|
|
642
|
+
"""The command (using the resolved executable) and why it cannot run, if it cannot."""
|
|
643
|
+
found = find_cli(env, name)
|
|
644
|
+
return [found or name, *args], (None if found else f"未找到 {name} 命令(已查 PATH 与常见安装位置;可用 AGENTHELM_{name.upper()} 指定)")
|
|
585
645
|
|
|
586
646
|
|
|
587
647
|
def install_steps(env: Env, latest: dict, clients: list[str], plugins: list[str], source: Path | None,
|
|
@@ -590,17 +650,17 @@ def install_steps(env: Env, latest: dict, clients: list[str], plugins: list[str]
|
|
|
590
650
|
steps: list[Step] = []
|
|
591
651
|
for client in clients:
|
|
592
652
|
if client == "claude":
|
|
593
|
-
|
|
594
|
-
|
|
653
|
+
command, blocked = _cli(env, "claude", "plugin", "marketplace", "add", slug)
|
|
654
|
+
steps.append(Step("install", "claude", f"Claude Code 添加 agenthelm 市场({slug})", command, blocked=blocked))
|
|
595
655
|
for plugin in plugins:
|
|
596
|
-
|
|
597
|
-
|
|
656
|
+
command, blocked = _cli(env, "claude", "plugin", "install", f"{plugin}@{MARKETPLACE}")
|
|
657
|
+
steps.append(Step("install", "claude", f"Claude Code 安装 {plugin}", command, blocked=blocked))
|
|
598
658
|
elif client == "codex":
|
|
599
|
-
|
|
600
|
-
|
|
659
|
+
command, blocked = _cli(env, "codex", "plugin", "marketplace", "add", slug)
|
|
660
|
+
steps.append(Step("install", "codex", f"Codex 添加 agenthelm 市场({slug})", command, blocked=blocked))
|
|
601
661
|
for plugin in plugins:
|
|
602
|
-
|
|
603
|
-
|
|
662
|
+
command, blocked = _cli(env, "codex", "plugin", "add", f"{plugin}@{MARKETPLACE}")
|
|
663
|
+
steps.append(Step("install", "codex", f"Codex 安装 {plugin}", command, blocked=blocked))
|
|
604
664
|
elif client == "cursor":
|
|
605
665
|
steps.append(cursor_step(latest, plugins, source, home, "安装"))
|
|
606
666
|
return steps
|
|
@@ -627,17 +687,16 @@ def update_steps(env: Env, report: dict, project: Path | None, source: Path | No
|
|
|
627
687
|
if only in (None, "plugins"):
|
|
628
688
|
claude = sorted({item["plugin"] for item in behind if item["client"] == "claude"})
|
|
629
689
|
if claude:
|
|
630
|
-
|
|
631
|
-
|
|
690
|
+
command, blocked = _cli(env, "claude", "plugin", "marketplace", "update", MARKETPLACE)
|
|
691
|
+
steps.append(Step("update", "claude", "刷新 Claude Code 的 agenthelm 市场", command, blocked=blocked))
|
|
632
692
|
for item in [entry for entry in behind if entry["client"] == "claude"]:
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
command += ["--scope", item["scope"]]
|
|
693
|
+
extra = ["--scope", item["scope"]] if item.get("scope") and item["scope"] != "user" else []
|
|
694
|
+
command, blocked = _cli(env, "claude", "plugin", "update", f"{item['plugin']}@{MARKETPLACE}", *extra)
|
|
636
695
|
steps.append(Step("update", "claude", f"Claude Code 更新 {item['plugin']} → {item['latest']}",
|
|
637
|
-
command, blocked=
|
|
696
|
+
command, blocked=blocked))
|
|
638
697
|
if any(item["client"] == "codex" for item in behind):
|
|
639
|
-
|
|
640
|
-
|
|
698
|
+
command, blocked = _cli(env, "codex", "plugin", "marketplace", "upgrade", MARKETPLACE)
|
|
699
|
+
steps.append(Step("update", "codex", "Codex 升级 agenthelm 市场与已装插件", command, blocked=blocked))
|
|
641
700
|
cursor = sorted({item["plugin"] for item in behind if item["client"] == "cursor"})
|
|
642
701
|
if cursor:
|
|
643
702
|
steps.append(cursor_step(latest, cursor, source, home, "更新"))
|
|
@@ -771,6 +830,10 @@ def legacy_artifacts(env: Env, names: set[str]) -> list[dict]:
|
|
|
771
830
|
"path": str(env.home / ".agents/plugins/marketplace.json")})
|
|
772
831
|
for name in _codex_personal_sections(env, names):
|
|
773
832
|
artifacts.append({"kind": "codex-config-section", "name": name, "path": str(env.codex_dir / "config.toml")})
|
|
833
|
+
for name in sorted(names):
|
|
834
|
+
cache = env.codex_dir / "plugins" / "cache" / "personal" / name
|
|
835
|
+
if cache.is_dir() and not cache.is_symlink():
|
|
836
|
+
artifacts.append({"kind": "codex-personal-cache", "name": name, "path": str(cache)})
|
|
774
837
|
try:
|
|
775
838
|
servers = read_json(env.home / ".claude.json").get("mcpServers", {})
|
|
776
839
|
except (OSError, ValueError, AttributeError):
|
|
@@ -799,10 +862,17 @@ def retire_legacy(env: Env, artifacts: list[dict], recovery: Path) -> list[dict]
|
|
|
799
862
|
done: list[dict] = []
|
|
800
863
|
backed_up: set[str] = set()
|
|
801
864
|
|
|
865
|
+
def inside_recovery(path: Path) -> Path:
|
|
866
|
+
"""The path below the recovery folder: home-relative, or absolute for locations outside home."""
|
|
867
|
+
try:
|
|
868
|
+
return path.relative_to(env.home)
|
|
869
|
+
except ValueError:
|
|
870
|
+
return Path("external") / str(path).lstrip("/")
|
|
871
|
+
|
|
802
872
|
def backup(path: Path) -> None:
|
|
803
873
|
if str(path) in backed_up:
|
|
804
874
|
return
|
|
805
|
-
target = recovery / "files" / path
|
|
875
|
+
target = recovery / "files" / inside_recovery(path)
|
|
806
876
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
807
877
|
shutil.copy2(path, target)
|
|
808
878
|
backed_up.add(str(path))
|
|
@@ -812,8 +882,8 @@ def retire_legacy(env: Env, artifacts: list[dict], recovery: Path) -> list[dict]
|
|
|
812
882
|
if item.get("manual"):
|
|
813
883
|
done.append({**item, "status": "manual"})
|
|
814
884
|
continue
|
|
815
|
-
if item["kind"] in {"plugin-copy", "plugin-record", "claude-skill-link"}:
|
|
816
|
-
target = recovery / "moved" / path
|
|
885
|
+
if item["kind"] in {"plugin-copy", "plugin-record", "claude-skill-link", "codex-personal-cache"}:
|
|
886
|
+
target = recovery / "moved" / inside_recovery(path)
|
|
817
887
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
818
888
|
os.replace(path, target)
|
|
819
889
|
done.append({**item, "status": "moved", "to": str(target)})
|
|
@@ -845,9 +915,15 @@ def recovery_dir_for(env: Env, label: str) -> Path:
|
|
|
845
915
|
return env.home / ".agent-pack" / "recovery" / f"{stamp}-agenthelm-{label}"
|
|
846
916
|
|
|
847
917
|
|
|
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
|
-
|
|
918
|
+
def locate(env: Env, plugin: str, version: str | None = None, *, include_legacy: bool = False) -> dict:
|
|
919
|
+
"""Where a plugin is installed, newest first: the stable lookup for downstream wrappers.
|
|
920
|
+
|
|
921
|
+
Legacy copies are left out unless asked for, so a wrapper that works today
|
|
922
|
+
keeps working after ``migrate`` retires them.
|
|
923
|
+
"""
|
|
924
|
+
found = [item for item in installed_plugins(env, {plugin})
|
|
925
|
+
if (version is None or item.get("version") == version)
|
|
926
|
+
and (include_legacy or item["client"] != "legacy")]
|
|
851
927
|
found.sort(key=lambda item: (version_key(item.get("version") or "") or (0,), item["client"] != "legacy"),
|
|
852
928
|
reverse=True)
|
|
853
929
|
return {"ok": bool(found), "plugin": plugin, "version": version,
|
|
@@ -930,6 +1006,8 @@ def parser() -> argparse.ArgumentParser:
|
|
|
930
1006
|
locate_parser = sub.add_parser("locate", help="输出插件的已安装位置(最新优先),供下游脚本查找")
|
|
931
1007
|
locate_parser.add_argument("plugin")
|
|
932
1008
|
locate_parser.add_argument("--version", help="只要这个版本")
|
|
1009
|
+
locate_parser.add_argument("--include-legacy", action="store_true",
|
|
1010
|
+
help="也列出旧方式安装(默认不列,避免清理后才失效)")
|
|
933
1011
|
locate_parser.add_argument("--output", choices=("text", "json"), default="json")
|
|
934
1012
|
locate_parser.add_argument("--home", type=Path, help=argparse.SUPPRESS)
|
|
935
1013
|
return result
|
|
@@ -966,7 +1044,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
966
1044
|
if args.command == "check" and args.hook:
|
|
967
1045
|
return hook(env, project, args.hook_format)
|
|
968
1046
|
if args.command == "locate":
|
|
969
|
-
found = locate(env, args.plugin, args.version)
|
|
1047
|
+
found = locate(env, args.plugin, args.version, include_legacy=args.include_legacy)
|
|
970
1048
|
if args.output == "json":
|
|
971
1049
|
print(json.dumps(found, ensure_ascii=False, indent=2))
|
|
972
1050
|
elif found["path"]:
|