okami.mom/albedo

git clone https://git.nkp.pet/okami.mom/albedo

Aly Raffauffeat(api)!: migrate daemon and clients to protocol 361c43ca

main
7.5 KiB217 linesraw
1"""What the daemon's folder browser and project readers need from a remote host.
2
3    albedo_gather.py
4
5reads one JSON request line on stdin and prints one JSON snapshot. The daemon
6interprets the snapshot with the same code it uses on its own disk
7(albedo/daemon/folders.gleam, albedo/harness/vcs.gleam), so this side only
8collects: directory entries, which repository markers exist, the vcs
9commands the daemon planned, file sizes, and project files. Everything comes
10back in one ssh round trip per question.
11
12Request: `{route, dir, plans, tracked, deadline, counted}`, `dir` absolute
13and normalised. `route` is list, repo, preview or project, or exists, which
14answers `directory` alone. `tracked` names, per kind, the planned command
15that lists a preview's tracked files and the separator its output uses.
16
17Snapshot: `{directory, entries: {dir: [[name, dir, modified, symlink]]}, exists:
18[path], outputs: {key: stdout}, sizes: {path: bytes}, files: {relative:
19base64}}`. An output's key is its program and arguments joined by NUL; only
20commands that exit 0 within the deadline answer.
21"""
22
23from __future__ import annotations
24
25import base64
26import json
27import os
28import stat
29import subprocess
30import sys
31from concurrent.futures import ThreadPoolExecutor
32
33MARKERS = (".jj", ".git")
34SUBDIRECTORIES = 256  # child directories whose entries a preview lists
35PROJECT_FILES = 512
36PROJECT_BYTES = 8 * 1024 * 1024
37FILE_BYTES = 1024 * 1024 + 1  # one past the instruction limit, so it still warns
38
39
40def child(directory: str, name: str) -> str:
41    return "/" + name if directory == "/" else f"{directory}/{name}"
42
43
44def shown(name: str) -> bool:
45    """Names the daemon can show and send back: valid UTF-8."""
46    try:
47        name.encode()
48        return True
49    except UnicodeEncodeError:
50        return False
51
52
53def entries(directory: str) -> list[list[object]]:
54    try:
55        names = os.listdir(directory)
56    except OSError:
57        return []
58    listed: list[list[object]] = []
59    for name in filter(shown, names):
60        path = child(directory, name)
61        symlink = os.path.islink(path)
62        try:
63            info = os.stat(path)
64            listed.append(
65                [name, stat.S_ISDIR(info.st_mode), int(info.st_mtime), symlink]
66            )
67        except OSError:
68            listed.append([name, False, 0, symlink])
69    return listed
70
71
72def marks(directory: str, exists: list[str]) -> str | None:
73    """Record the markers at `directory`; the kind it roots, .jj first."""
74    found = None
75    for marker in MARKERS:
76        path = child(directory, marker)
77        if os.path.lexists(path) and (os.path.isfile(path) or os.path.isdir(path)):
78            exists.append(path)
79            found = found or marker
80    return found
81
82
83def parent(directory: str) -> str | None:
84    return None if directory == "/" else os.path.dirname(directory) or "/"
85
86
87def checkout(directory: str, exists: list[str]) -> tuple[str, str] | None:
88    """The nearest enclosing repository: (kind, root)."""
89    current: str | None = directory
90    while current is not None:
91        found = marks(current, exists)
92        if found is not None:
93            return found[1:], current
94        current = parent(current)
95    return None
96
97
98def key(program: str, args: list[str]) -> str:
99    return "\0".join([program, *args])
100
101
102def run(
103    command: list, root: str, directory: str, deadline: float
104) -> tuple[str, str | None]:
105    program, args, at = command
106    try:
107        done = subprocess.run(
108            [program, *args],
109            cwd=root if at == "root" else directory,
110            stdin=subprocess.DEVNULL,
111            capture_output=True,
112            timeout=deadline,
113        )
114    except (OSError, subprocess.TimeoutExpired):
115        return key(program, args), None
116    output = done.stdout.decode(errors="replace") if done.returncode == 0 else None
117    return key(program, args), output
118
119
120def project(directory: str) -> dict[str, str]:
121    """Project instruction files and skills, relative to `directory`."""
122    picked: list[str] = []
123    for relative in ("", ".agents", ".albedo"):
124        base = child(directory, relative) if relative else directory
125        try:
126            names = sorted(filter(shown, os.listdir(base)))
127        except OSError:
128            continue
129        for name in names:
130            if name.lower().endswith(".md") and os.path.isfile(child(base, name)):
131                picked.append(f"{relative}/{name}" if relative else name)
132    for relative in (".agents/skills", ".albedo/skills"):
133        for root, directories, names in os.walk(child(directory, relative)):
134            directories.sort()
135            for name in sorted(filter(shown, names)):
136                path = os.path.join(root, name)
137                if os.path.isfile(path) and len(picked) < PROJECT_FILES:
138                    picked.append(os.path.relpath(path, directory))
139    files: dict[str, str] = {}
140    budget = PROJECT_BYTES
141    for relative in picked:
142        try:
143            with open(child(directory, relative), "rb") as handle:
144                data = handle.read(min(FILE_BYTES, budget))
145        except OSError:
146            continue
147        budget -= len(data)
148        files[relative] = base64.b64encode(data).decode()
149        if budget <= 0:
150            break
151    return files
152
153
154def gather(request: dict) -> dict[str, object]:
155    route, directory = request["route"], request["dir"]
156    snapshot: dict[str, object] = {"directory": os.path.isdir(directory)}
157    if not snapshot["directory"] or route == "exists":
158        return snapshot
159    if route == "project":
160        snapshot["files"] = project(directory)
161        return snapshot
162    exists: list[str] = []
163    listed = {directory: entries(directory)}
164    snapshot.update(entries=listed, exists=exists)
165    if route == "list":
166        for name, is_directory, _, _symlink in listed[directory]:
167            if is_directory:
168                marks(child(directory, str(name)), exists)
169        return snapshot
170    found = checkout(directory, exists)
171    if route == "preview":
172        children = [
173            child(directory, str(name))
174            for name, is_directory, _, _symlink in listed[directory]
175            if is_directory and not str(name).startswith(".")
176        ]
177        for path in children[:SUBDIRECTORIES]:
178            listed[path] = entries(path)
179    if found is None:
180        return snapshot
181    kind, root = found
182    deadline = float(request.get("deadline", 2.0))
183    plan = request.get("plans", {}).get(kind, [])
184    with ThreadPoolExecutor(max_workers=max(1, len(plan))) as pool:
185        answers = dict(pool.map(lambda c: run(c, root, directory, deadline), plan))
186    outputs = {k: v for k, v in answers.items() if v is not None}
187    snapshot["outputs"] = outputs
188    if route == "preview":
189        relatives: list[str] = []
190        tracked = request.get("tracked", {}).get(kind)
191        if tracked is not None:
192            program, args, _ = tracked["command"]
193            listing = outputs.get(key(program, args)) or ""
194            relatives = listing.split(tracked["separator"])
195        sizes: dict[str, int] = {}
196        for relative in relatives[: int(request.get("counted", 0))]:
197            if not relative:
198                continue
199            path = child(directory, relative)
200            try:
201                info = os.stat(path)
202            except OSError:
203                continue
204            if stat.S_ISREG(info.st_mode):
205                sizes[path] = info.st_size
206        snapshot["sizes"] = sizes
207    return snapshot
208
209
210def main() -> int:
211    request = json.loads(sys.stdin.readline())
212    print(json.dumps(gather(request)))
213    return 0
214
215
216if __name__ == "__main__":
217    sys.exit(main())