Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ From a local checkout, use the shorter equivalents:
```

Cleanup scripts always preview first and require explicit confirmation before
applying changes. The [agent research](docs/agent-tools.md) records supported
applying changes. `cleaner dev --root ~/Coding` inventories manifest-backed
build output, dependency trees, and language caches before offering removal.
The [agent research](docs/agent-tools.md) records supported
clients and cleanup limits; the [security research](docs/security-research.md)
explains scanner, CVE, KEV, and host-check coverage; the
[Slurm example](examples/slurm.toml) shows every resource and command option.
Expand Down
25 changes: 25 additions & 0 deletions bin/_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import os
import shlex
import shutil
import stat
import subprocess
import sys
from collections.abc import Sequence
Expand Down Expand Up @@ -167,6 +168,30 @@ def safe_targets(root: Path, patterns: Sequence[str]) -> list[Path]:
return sorted(targets, key=lambda path: (len(path.parts), str(path)), reverse=True)


def target_usage(targets: Sequence[Path]) -> tuple[int, int]:
"""Count entries and logical bytes without following symlinks."""
roots: list[Path] = []
for target in sorted(set(targets), key=lambda path: (len(path.parts), str(path))):
if not any(root == target or root in target.parents for root in roots):
roots.append(target)

items = total_bytes = 0
pending = roots[:]
while pending:
path = pending.pop()
try:
metadata = path.lstat()
items += 1
if stat.S_ISDIR(metadata.st_mode):
with os.scandir(path) as entries:
pending.extend(Path(entry.path) for entry in entries)
else:
total_bytes += metadata.st_size
except OSError:
continue
return items, total_bytes


def remove_targets(targets: Sequence[Path]) -> tuple[int, int]:
files = directories = 0
for target in targets:
Expand Down
71 changes: 41 additions & 30 deletions bin/agent
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,12 @@ from _common import (
run,
safe_targets,
show_steps,
target_usage,
warning,
)

SKILLS_PACKAGE = "skills@1.5.22"


@dataclass(frozen=True)
class AgentSpec:
Expand Down Expand Up @@ -498,29 +501,6 @@ def _native_clean_steps(spec: AgentSpec, kind: str) -> list[Step]:
return []


def _target_usage(targets: list[Path]) -> tuple[int, int]:
roots: list[Path] = []
for target in sorted(set(targets), key=lambda path: (len(path.parts), str(path))):
if not any(root == target or root in target.parents for root in roots):
roots.append(target)

items = total_bytes = 0
pending = roots[:]
while pending:
path = pending.pop()
try:
metadata = path.lstat()
except FileNotFoundError:
continue
items += 1
if stat.S_ISDIR(metadata.st_mode):
with os.scandir(path) as entries:
pending.extend(Path(entry.path) for entry in entries)
else:
total_bytes += metadata.st_size
return items, total_bytes


def clean(args: argparse.Namespace) -> int:
heading(f"Agent clean · {args.kind}")
targets: list[Path] = []
Expand Down Expand Up @@ -555,7 +535,7 @@ def clean(args: argparse.Namespace) -> int:
if not targets and not steps:
print("No safely automatable local state was found.")
return 1 if blocked else 0
items, total_bytes = _target_usage(targets)
items, total_bytes = target_usage(targets)
print(
f"Plan: {items} local item(s) ({total_bytes:,} bytes) across "
f"{len(targets)} target(s); {len(steps)} native operation(s) "
Expand Down Expand Up @@ -717,12 +697,12 @@ def market(args: argparse.Namespace) -> int:
if npx is None:
raise ToolError("npx is required; install a current Node.js release")
if args.install:
if args.agent == "all" and os.environ.get("SCRIPTS_TUI"):
raise ToolError("choose one target agent before installing a skill")
command = [npx, "--yes", "skills", "add", args.install]
command = [npx, "--yes", SKILLS_PACKAGE, "add", args.install]
if args.skill:
command.extend(("--skill", args.skill))
if args.agent != "all":
if args.agent == "all":
command.extend(("--agent", "*"))
else:
spec = choose_agents(args.agent)[0]
if spec.skill_agent is None:
raise ToolError(f"{spec.label} does not natively load Agent Skills")
Expand All @@ -749,7 +729,7 @@ def market(args: argparse.Namespace) -> int:

if not args.query and os.environ.get("SCRIPTS_TUI"):
raise ToolError("enter a market query in the wizard")
command = [npx, "--yes", "skills", "find"]
command = [npx, "--yes", SKILLS_PACKAGE, "find"]
if args.query:
command.append(args.query)
heading("Indexed Agent Skills catalog")
Expand Down Expand Up @@ -790,6 +770,37 @@ def self_test() -> int:
assert all(
spec.skill_agent is not None for key, spec in agents.items() if key != "aider"
)
add = ["npx", "--yes", SKILLS_PACKAGE, "add", "owner/repository", "--agent"]
market_cases = (
(
["market", "--install", "owner/repository"],
[*add, "*", "--list"],
),
(
["market", "--install", "owner/repository", "--agent", "codex"],
[*add, "codex", "--list"],
),
(
["market", "--install", "owner/repository", "--apply", "--yes"],
[*add, "*", "--yes"],
),
(
["market", "--query", "python"],
["npx", "--yes", SKILLS_PACKAGE, "find", "python"],
),
)
with (
patch(f"{__name__}.find_program", return_value="npx"),
patch(
f"{__name__}.run",
return_value=SimpleNamespace(returncode=0),
) as market_run,
):
for arguments, _expected in market_cases:
assert market(parse_args(arguments)) == 0
assert [call.args[0] for call in market_run.call_args_list] == [
expected for _arguments, expected in market_cases
]
assert _read_json_lines('{"id":"one"}\n{"id":"two"}') == [
{"id": "one"},
{"id": "two"},
Expand Down Expand Up @@ -888,7 +899,7 @@ def self_test() -> int:
(usage / "one").write_bytes(b"123")
nested_file = nested / "two"
nested_file.write_bytes(b"45")
assert _target_usage([usage, nested_file]) == (4, 5)
assert target_usage([usage, nested_file]) == (4, 5)
partial = AgentSpec(
"fixture",
"Fixture",
Expand Down
11 changes: 6 additions & 5 deletions bin/agent.man
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,8 @@ Droid, OpenHands, Pi, and Rovo Dev. Market discovery delegates to the live
private skill.

> **Trust boundary:** Every `market` search or install runs
> `npx --yes skills ...`. It requires Node.js, `npx`, and network access and
> may download and execute the current npm package. Review that package and
> `npx --yes skills@1.5.22 ...`. It requires Node.js, `npx`, and network access and
> may download and execute the pinned npm package. Review that package and
> each skill source before continuing.

## Actions
Expand Down Expand Up @@ -174,9 +174,10 @@ retain separate copies. Gemini session listing is current-project scoped.
### `market`

Enter a `--query` to search interactively. To preview an install, set
`--install owner/repository`, optionally `--skill name`, choose an `--agent`,
and select `--global` for user scope. A preview lists what the live package
would install; it does not establish that the source is trustworthy.
`--install owner/repository`, optionally `--skill name`, and select `--global`
for user scope. The default `all` target uses the installer's supported `*`
agent target; choose one `--agent` to narrow it. A preview lists what the live
package would install; it does not establish that the source is trustworthy.

## Direct CLI

Expand Down
Loading
Loading