lean-computer-use 0.0.0-stage → 0.1.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/AGENTS.md +71 -0
- package/CHANGELOG.md +18 -0
- package/CLA.md +37 -0
- package/CLAUDE.md +5 -0
- package/CODE_OF_CONDUCT.md +127 -0
- package/CONTRIBUTING.md +28 -0
- package/CREDITS.md +17 -0
- package/LICENSE +21 -0
- package/MAINTAINERS.md +8 -0
- package/README.md +233 -2
- package/RELEASING.md +49 -0
- package/REPO_SETTINGS.md +26 -0
- package/SECURITY.md +18 -0
- package/agents/desk-runner.md +48 -0
- package/bin/lean-computer-use.mjs +9 -0
- package/briefs/desk-brief.md +26 -0
- package/desk/README.md +64 -0
- package/desk/_resolve.py +279 -0
- package/desk/desk +33 -0
- package/desk/desk-clean +177 -0
- package/desk/desk-read +39 -0
- package/desk/exit-codes.json +13 -0
- package/docs/INSTALLER.md +62 -0
- package/docs/METHOD.md +17 -0
- package/docs/PROVENANCE.md +19 -0
- package/docs/README.md +12 -0
- package/docs/STACK.md +8 -0
- package/docs/cua-driver-install.md +57 -0
- package/docs/install.md +62 -0
- package/hooks/README.md +26 -0
- package/hooks/_common.py +172 -0
- package/hooks/native_batch_nudge.py +37 -0
- package/hooks/native_batch_nudge.sh +6 -0
- package/hooks/native_retry_guard.py +36 -0
- package/hooks/native_retry_guard.sh +8 -0
- package/hooks/settings-snippet.json +34 -0
- package/llms.txt +48 -0
- package/package.json +23 -4
- package/skills/lean-computer-use/SKILL.md +70 -0
- package/skills/lean-computer-use/methods/background-only.md +14 -0
- package/skills/lean-computer-use/methods/cheap-helper-or-subagent.md +18 -0
- package/skills/lean-computer-use/methods/cleanup-with-proof.md +24 -0
- package/skills/lean-computer-use/methods/desk-run.md +19 -0
- package/skills/lean-computer-use/methods/failure-modes.md +16 -0
- package/skills/lean-computer-use/methods/filtered-reads.md +30 -0
- package/skills/lean-computer-use/methods/same-app-chaining.md +20 -0
- package/skills/lean-computer-use/methods/verify.md +14 -0
- package/src/cli.mjs +33 -0
- package/src/doctor.mjs +87 -0
- package/src/hooks-register.mjs +56 -0
- package/src/install.mjs +291 -0
- package/src/measure.mjs +184 -0
- package/src/paths.mjs +81 -0
package/desk/desk-clean
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""desk-clean <bundle_id> [--app-name NAME] [--launched-by-run]: release run state."""
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import math
|
|
6
|
+
import os
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import tempfile
|
|
10
|
+
import time
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from _resolve import (CACHE, DRIVER_MARKER, OWN_FILES, STATE, DeskError,
|
|
14
|
+
driver_command, driver_status, state_is_ours)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def own_files():
|
|
18
|
+
if not STATE.exists():
|
|
19
|
+
return []
|
|
20
|
+
return [path for path in STATE.iterdir() if (OWN_FILES.fullmatch(path.name) or path.name == DRIVER_MARKER)
|
|
21
|
+
and not path.is_symlink() and path.is_file()]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def read_json(path):
|
|
25
|
+
try:
|
|
26
|
+
value = json.loads(path.read_text(encoding="utf-8"))
|
|
27
|
+
return value if isinstance(value, dict) else {}
|
|
28
|
+
except (ValueError, OSError):
|
|
29
|
+
return {}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def cached_name(bundle_id, files, cache):
|
|
33
|
+
entry = cache.get(bundle_id)
|
|
34
|
+
if isinstance(entry, dict) and entry.get("name"):
|
|
35
|
+
return entry["name"]
|
|
36
|
+
# Some callers retain a launch result. Match its bundle before using its name.
|
|
37
|
+
launches = [path for path in files if path.name.startswith("launch_app-")]
|
|
38
|
+
for path in sorted(launches, key=lambda path: path.stat().st_mtime, reverse=True):
|
|
39
|
+
launch = read_json(path)
|
|
40
|
+
detail = launch.get("detail")
|
|
41
|
+
detail = detail if isinstance(detail, dict) else {}
|
|
42
|
+
if (launch.get("bundle_id") or detail.get("bundle_id")) == bundle_id:
|
|
43
|
+
name = launch.get("name") or detail.get("name")
|
|
44
|
+
if name:
|
|
45
|
+
return name
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def is_running(escaped, timeout):
|
|
50
|
+
"""Ask macOS itself: the display name need not match the process name."""
|
|
51
|
+
try:
|
|
52
|
+
result = subprocess.run(["osascript", "-e", f'application "{escaped}" is running'],
|
|
53
|
+
capture_output=True, text=True, timeout=timeout)
|
|
54
|
+
except subprocess.TimeoutExpired:
|
|
55
|
+
raise DeskError("osascript is-running check timed out") from None
|
|
56
|
+
answer = result.stdout.strip()
|
|
57
|
+
if result.returncode or answer not in ("true", "false"):
|
|
58
|
+
raise DeskError("osascript is-running check failed")
|
|
59
|
+
return answer == "true"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def quit_app(name):
|
|
63
|
+
if (not isinstance(name, str) or not name.strip() or name.startswith("-")
|
|
64
|
+
or any(ord(char) < 32 or ord(char) == 127 for char in name)):
|
|
65
|
+
raise DeskError("valid --app-name required when no cached launch name is available")
|
|
66
|
+
wait = float(os.environ.get("DESK_CLEAN_WAIT_S", "5"))
|
|
67
|
+
if not math.isfinite(wait) or not 0 <= wait <= 5:
|
|
68
|
+
raise DeskError("DESK_CLEAN_WAIT_S must be between 0 and 5")
|
|
69
|
+
quit_wait = float(os.environ.get("DESK_CLEAN_QUIT_TIMEOUT_S", "5"))
|
|
70
|
+
if not math.isfinite(quit_wait) or not 0 < quit_wait <= 5:
|
|
71
|
+
raise DeskError("DESK_CLEAN_QUIT_TIMEOUT_S must be above 0 and at most 5")
|
|
72
|
+
escaped = name.replace("\\", "\\\\").replace('"', '\\"')
|
|
73
|
+
try:
|
|
74
|
+
# A failed or slow quit command is not the verdict: the is-running poll is.
|
|
75
|
+
subprocess.run(["osascript", "-e", f'quit app "{escaped}"'],
|
|
76
|
+
capture_output=True, text=True, timeout=quit_wait)
|
|
77
|
+
except subprocess.TimeoutExpired:
|
|
78
|
+
pass
|
|
79
|
+
deadline = time.monotonic() + wait
|
|
80
|
+
while True:
|
|
81
|
+
if not is_running(escaped, min(5, max(0.5, deadline - time.monotonic()))):
|
|
82
|
+
return "quit"
|
|
83
|
+
if time.monotonic() >= deadline:
|
|
84
|
+
return "still-running"
|
|
85
|
+
time.sleep(min(0.05, max(0, deadline - time.monotonic())))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def clean_state(bundle_id, files, cache):
|
|
89
|
+
removed = 0
|
|
90
|
+
cache.pop(bundle_id, None)
|
|
91
|
+
for path in files:
|
|
92
|
+
if path == CACHE and cache:
|
|
93
|
+
# Keep other bundles' identities. Atomic replacement keeps mode 0600.
|
|
94
|
+
fd, name = tempfile.mkstemp(prefix="apps-", suffix=".json", dir=STATE)
|
|
95
|
+
temporary = Path(name)
|
|
96
|
+
try:
|
|
97
|
+
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
|
98
|
+
json.dump(cache, handle, ensure_ascii=False)
|
|
99
|
+
handle.write("\n")
|
|
100
|
+
os.replace(temporary, CACHE)
|
|
101
|
+
finally:
|
|
102
|
+
temporary.unlink(missing_ok=True)
|
|
103
|
+
else:
|
|
104
|
+
try:
|
|
105
|
+
# Recheck immediately before removal, just like the prune rule.
|
|
106
|
+
if not path.is_symlink() and path.is_file():
|
|
107
|
+
path.unlink()
|
|
108
|
+
removed += 1
|
|
109
|
+
except FileNotFoundError:
|
|
110
|
+
pass
|
|
111
|
+
return removed
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def stop_driver():
|
|
115
|
+
try:
|
|
116
|
+
sessions = driver_command("sessions")
|
|
117
|
+
running = driver_status()
|
|
118
|
+
if running is False:
|
|
119
|
+
return "already-stopped"
|
|
120
|
+
if running is None or sessions.returncode:
|
|
121
|
+
return "stop-failed"
|
|
122
|
+
if sessions.stdout.strip().rstrip(".") != "No live sessions":
|
|
123
|
+
return "left-running-live-sessions" if sessions.stdout.strip() else "stop-failed"
|
|
124
|
+
# Only the driver's graceful stop command may stop the shared daemon.
|
|
125
|
+
driver_command("stop")
|
|
126
|
+
deadline = time.monotonic() + 5
|
|
127
|
+
while True:
|
|
128
|
+
remaining = deadline - time.monotonic()
|
|
129
|
+
if remaining <= 0:
|
|
130
|
+
return "stop-failed"
|
|
131
|
+
if driver_status(min(2, remaining)) is False:
|
|
132
|
+
return "stopped"
|
|
133
|
+
time.sleep(min(0.1, max(0, deadline - time.monotonic())))
|
|
134
|
+
except DeskError:
|
|
135
|
+
return "stop-failed"
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def main():
|
|
139
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
140
|
+
parser.add_argument("bundle_id")
|
|
141
|
+
parser.add_argument("--app-name")
|
|
142
|
+
parser.add_argument("--launched-by-run", action="store_true")
|
|
143
|
+
options = parser.parse_args()
|
|
144
|
+
app, rc, error = "left-open", 0, None
|
|
145
|
+
try:
|
|
146
|
+
refused = STATE.exists() and not state_is_ours()
|
|
147
|
+
files = [] if refused else own_files()
|
|
148
|
+
cache = read_json(CACHE) if CACHE in files else {}
|
|
149
|
+
if options.launched_by_run:
|
|
150
|
+
try:
|
|
151
|
+
name = options.app_name or cached_name(options.bundle_id, files, cache)
|
|
152
|
+
app = "still-running" if name else "left-open"
|
|
153
|
+
app = quit_app(name)
|
|
154
|
+
rc = 3 if app == "still-running" else 0
|
|
155
|
+
except (DeskError, ValueError, OSError, subprocess.TimeoutExpired) as failure:
|
|
156
|
+
rc, error = 2, failure
|
|
157
|
+
removed = clean_state(options.bundle_id, files, cache)
|
|
158
|
+
driver = stop_driver()
|
|
159
|
+
if driver == "stop-failed":
|
|
160
|
+
rc = 3
|
|
161
|
+
print(f"cleanup: app={app} state_files_removed={removed} "
|
|
162
|
+
f"remaining_own_files={len(own_files())} driver={driver}")
|
|
163
|
+
if refused and not error:
|
|
164
|
+
error = DeskError("refusing to delete: state dir lacks .desk-state marker, "
|
|
165
|
+
"is not owned by this user or is not mode 0700")
|
|
166
|
+
if error and driver != "stop-failed":
|
|
167
|
+
raise DeskError(str(error))
|
|
168
|
+
if error:
|
|
169
|
+
print("desk-clean: " + str(error).replace("\n", " ").replace("\r", " "), file=sys.stderr)
|
|
170
|
+
return rc
|
|
171
|
+
except (DeskError, ValueError, OSError) as failure:
|
|
172
|
+
print("desk-clean: " + str(failure).replace("\n", " ").replace("\r", " "), file=sys.stderr)
|
|
173
|
+
return 2
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
if __name__ == "__main__":
|
|
177
|
+
sys.exit(main())
|
package/desk/desk-read
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""desk-read <bundle_id> [filter] [--max N]: compact query read plus full JSON file."""
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from _resolve import DeskError, redact, run_tool, save_json
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def main():
|
|
10
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
11
|
+
parser.add_argument("bundle_id")
|
|
12
|
+
parser.add_argument("filter", nargs="?", default="AXStaticText")
|
|
13
|
+
parser.add_argument("--max", type=int, default=40)
|
|
14
|
+
options = parser.parse_args()
|
|
15
|
+
if options.max < 0:
|
|
16
|
+
parser.error("--max must be nonnegative")
|
|
17
|
+
try:
|
|
18
|
+
rc, result, pid, wid, cached = run_tool(options.bundle_id, "get_window_state", {
|
|
19
|
+
"include_screenshot": False, "query": options.filter,
|
|
20
|
+
})
|
|
21
|
+
path = save_json(result, prefix="tree")
|
|
22
|
+
tree = str(result.get("tree_markdown") or "") if isinstance(result, dict) else ""
|
|
23
|
+
lines = [safe.strip() for raw, safe in zip(tree.splitlines(), redact(tree).splitlines())
|
|
24
|
+
if options.filter.lower() in raw.lower()]
|
|
25
|
+
print(f"pid={pid} window={wid} cached={cached} shown={min(len(lines), options.max)}/{len(lines)} "
|
|
26
|
+
f"bytes={path.stat().st_size} file={json.dumps(str(path))}")
|
|
27
|
+
for line in lines[:options.max]:
|
|
28
|
+
print(redact(line))
|
|
29
|
+
if rc or isinstance(result, dict) and result.get("error"):
|
|
30
|
+
print("desk-read: cua-driver read failed; see saved JSON", file=sys.stderr)
|
|
31
|
+
return rc or 1
|
|
32
|
+
return 0
|
|
33
|
+
except (DeskError, OSError) as error:
|
|
34
|
+
print("desk-read: " + str(error).replace("\n", " "), file=sys.stderr)
|
|
35
|
+
return 2
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
if __name__ == "__main__":
|
|
39
|
+
sys.exit(main())
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
[
|
|
2
|
+
{"command":"desk","exit":"0","test":"test_pid_window_cache_second_call_does_not_launch_app"},
|
|
3
|
+
{"command":"desk","exit":"1","test":"test_error_result_without_driver_failure_exits_one"},
|
|
4
|
+
{"command":"desk","exit":"2","test":"test_invalid_arguments_exit_two_without_desktop_call"},
|
|
5
|
+
{"command":"desk","exit":"other nonzero","test":"test_driver_failure_exit_code_is_preserved"},
|
|
6
|
+
{"command":"desk-read","exit":"0","test":"test_desk_read_filters_compact_lines_and_saves_full_json"},
|
|
7
|
+
{"command":"desk-read","exit":"1","test":"test_error_result_without_driver_failure_exits_one"},
|
|
8
|
+
{"command":"desk-read","exit":"2","test":"test_invalid_arguments_exit_two_without_desktop_call"},
|
|
9
|
+
{"command":"desk-read","exit":"other nonzero","test":"test_driver_failure_exit_code_is_preserved"},
|
|
10
|
+
{"command":"desk-clean","exit":"0","test":"test_clean_launched_by_run_quits_with_explicit_name"},
|
|
11
|
+
{"command":"desk-clean","exit":"2","test":"test_clean_missing_name_fails_safely_and_still_cleans_state"},
|
|
12
|
+
{"command":"desk-clean","exit":"3","test":"test_clean_still_running_exits_three_and_removes_state"}
|
|
13
|
+
]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Installer contract
|
|
2
|
+
|
|
3
|
+
## Commands
|
|
4
|
+
|
|
5
|
+
| Command | Behavior |
|
|
6
|
+
|---|---|
|
|
7
|
+
| `npx lean-computer-use` | Preview an install and ask for confirmation. |
|
|
8
|
+
| `npx lean-computer-use install --yes` | Apply the previewed install without a prompt. |
|
|
9
|
+
| `npx lean-computer-use install --with-hooks` | Also merge marked native hook registrations. |
|
|
10
|
+
| `npx lean-computer-use install --dir <path> --share <path>` | Use explicit host and share roots. |
|
|
11
|
+
| `npx lean-computer-use uninstall` | Remove unchanged files recorded in the manifest. |
|
|
12
|
+
| `npx lean-computer-use --uninstall` | Alias for `uninstall`. |
|
|
13
|
+
| `npx lean-computer-use install --dry-run` | Show the same action plan with no writes. |
|
|
14
|
+
| `npx lean-computer-use uninstall --dry` | Preview removal; `--dry` equals `--dry-run`. |
|
|
15
|
+
|
|
16
|
+
Default host root: `~/.claude`.
|
|
17
|
+
Default share root: `~/.local/share/lean-computer-use`.
|
|
18
|
+
`LEAN_COMPUTER_USE_HOME` overrides the default share root; `--share` takes precedence.
|
|
19
|
+
`--dir` and `--share` may name absolute target roots.
|
|
20
|
+
Recorded paths inside each root must remain relative, contained and free of symlinks.
|
|
21
|
+
|
|
22
|
+
## Install ownership
|
|
23
|
+
|
|
24
|
+
The installer writes the skill under `skills/lean-computer-use`, the runner under `agents/desk-runner.md`, and desk tools and hook files under the share root.
|
|
25
|
+
It copies packaged `briefs/desk-brief.md` to `<claude dir>/skills/lean-computer-use/desk-brief.md` as an owned skill file, with the same hash, backup and removal rules.
|
|
26
|
+
It replaces `__DESK_DIR__` in the runner with the installed desk directory.
|
|
27
|
+
Desk scripts are executable.
|
|
28
|
+
It records paths, file hashes and created directories in `<share>/install-manifest.json`.
|
|
29
|
+
|
|
30
|
+
The installer previews before mutation and asks `y/N` unless `--yes` is supplied.
|
|
31
|
+
A non-interactive install requires `--yes`.
|
|
32
|
+
A differing existing file is preserved unless replacement is approved with `--yes`; replacement keeps a `.bak`.
|
|
33
|
+
The manifest records enough ownership information to preserve or restore prior content on removal.
|
|
34
|
+
The installer never installs third-party tools, runs vendor logins, or changes global settings without confirmation.
|
|
35
|
+
|
|
36
|
+
## Hook merge
|
|
37
|
+
|
|
38
|
+
`--with-hooks` backs up `settings.json`, preserves all other keys and registrations, and adds marked entries idempotently.
|
|
39
|
+
MCP native tools and Bash have separate matchers.
|
|
40
|
+
The Bash handler checks that the first word of a command segment is `desk`, `desk-read` or `desk-clean` before it applies native behavior.
|
|
41
|
+
Uninstall removes only entries carrying this install's marker.
|
|
42
|
+
If the snippet was pasted manually, inspect the printed manual steps and remove only those pasted entries yourself.
|
|
43
|
+
|
|
44
|
+
## Safe removal
|
|
45
|
+
|
|
46
|
+
Uninstall reads the manifest and rechecks each target before unlinking it.
|
|
47
|
+
Removal budget: uninstall removes every file the installer wrote that still matches its recorded hash; a file you edited stays in place and is listed for review.
|
|
48
|
+
Traversal, absolute recorded paths and symlink targets are refused.
|
|
49
|
+
It removes a directory only when this install created it and it is empty.
|
|
50
|
+
The manifest is removed last.
|
|
51
|
+
Backups of replaced content are restored according to their recorded ownership.
|
|
52
|
+
|
|
53
|
+
`--dry-run` performs the same validation and reports the same planned actions without writing.
|
|
54
|
+
With no manifest, uninstall exits 2 and names the missing manifest path.
|
|
55
|
+
Restore the manifest from your backup or follow its reported manual cleanup steps after reviewing each file.
|
|
56
|
+
Do not delete a shared skill directory or an entire settings file as a substitute.
|
|
57
|
+
|
|
58
|
+
## Verification
|
|
59
|
+
|
|
60
|
+
`test/installer.test.mjs` exercises temporary roots, dry runs, replacement backup, settings merge, edited-file preservation and install/uninstall restoration.
|
|
61
|
+
`npx lean-computer-use doctor --json` compares installed files with the package and reports optional hook registration.
|
|
62
|
+
All tests use temporary paths and fake commands.
|
package/docs/METHOD.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Native app method
|
|
2
|
+
|
|
3
|
+
Every agent step re-reads the conversation available to that agent.
|
|
4
|
+
Put screen work in a small, fresh context and bundle predictable actions so it takes fewer steps.
|
|
5
|
+
The main session frames the task, checks saved evidence, and reports the result; a cheap helper or `desk-runner` performs the app work.
|
|
6
|
+
When no cheap helper is available, the runner handles every native app task.
|
|
7
|
+
|
|
8
|
+
1. **Frame:** name one app's bundle id, approved actions, done check, evidence path and limits in the [brief](../briefs/desk-brief.md).
|
|
9
|
+
2. **Snapshot:** check `osascript -e 'application "<Name>" is running'` before acting.
|
|
10
|
+
3. **Act:** choose background keys or typing, test one action, and chain the remaining predictable actions in one call.
|
|
11
|
+
4. **Read:** use one case-insensitive substring per `desk-read`, shared by the wanted lines (for example `AXStaticText`). Use two reads when two fields share none. Each printed file path holds the full read so the main session can inspect any other line.
|
|
12
|
+
5. **Clean:** preserve evidence, run `desk-clean`, and compare the final app-running check with the initial snapshot.
|
|
13
|
+
6. **Verify:** the main session opens the saved evidence and checks the stated done signal before reporting.
|
|
14
|
+
|
|
15
|
+
Choose the focused method you need in [SKILL.md](../skills/lean-computer-use/SKILL.md).
|
|
16
|
+
The [lean-browser-use sibling](https://github.com/aunysillyme/lean-browser-use) applies the same method to web pages.
|
|
17
|
+
Measure your own transcript costs with `npx lean-computer-use measure --json`; this package's synthetic fixture demonstrates the calculation without making a savings claim about your setup.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Provenance
|
|
2
|
+
|
|
3
|
+
This package implements a native app workflow: frame the job in the main session, run app actions in a smaller context, then verify saved evidence.
|
|
4
|
+
The desk wrappers build on cua-driver from [trycua/cua](https://github.com/trycua/cua).
|
|
5
|
+
The driver is a separate installation; its code and app are not bundled.
|
|
6
|
+
|
|
7
|
+
| Technique | Source | Local implementation |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| Background app actions through the accessibility tree | [trycua/cua](https://github.com/trycua/cua) | `desk/desk`, `desk/_resolve.py` |
|
|
10
|
+
| Small-context runner and hook lifecycle | [Claude Code](https://code.claude.com/docs/en/overview) | `agents/desk-runner.md`, `hooks/`, `src/hooks-register.mjs` |
|
|
11
|
+
| Packet actions and index choice from observed candidates | [Jev / typesafe-sdk](https://pypi.org/project/typesafe-sdk/) | `skills/lean-computer-use/methods/desk-run.md`, `same-app-chaining.md` |
|
|
12
|
+
| Filtered output with retained full evidence | [rtk-ai/rtk](https://github.com/rtk-ai/rtk) | `desk/desk-read`, `skills/lean-computer-use/methods/filtered-reads.md` |
|
|
13
|
+
| Plain retrieval before interactive browsing | [Crawl4AI](https://github.com/unclecode/crawl4ai), [SearXNG](https://github.com/searxng/searxng) | Context only; native app tasks stay in `docs/METHOD.md` |
|
|
14
|
+
| The same method applied to web pages | [lean-browser-use](https://github.com/aunysillyme/lean-browser-use) | `docs/METHOD.md`, README sibling link |
|
|
15
|
+
|
|
16
|
+
The public runner and native hooks are written for this package.
|
|
17
|
+
Example transcripts are synthetic.
|
|
18
|
+
Measurements describe this package or its fixture, with commands and dates in `proof/results.json`.
|
|
19
|
+
Upstream licence versions were not rechecked over the network during this build: see the linked repositories and [CREDITS.md](../CREDITS.md).
|
package/docs/README.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Documentation
|
|
2
|
+
|
|
3
|
+
- [Install](install.md): preview, install, optional hooks and removal.
|
|
4
|
+
- [Driver precautions](cua-driver-install.md): verify the upstream app and permissions.
|
|
5
|
+
- [Installer contract](INSTALLER.md): ownership, backups and safe removal.
|
|
6
|
+
- [Method](METHOD.md): the native app workflow in one page.
|
|
7
|
+
- [Skill](../skills/lean-computer-use/SKILL.md): the entry point for an AI agent.
|
|
8
|
+
- [Desk commands](../desk/README.md): action, read and cleanup behavior and exit codes.
|
|
9
|
+
- [Provenance](PROVENANCE.md): upstream techniques and the files that use them.
|
|
10
|
+
- [Stack card](STACK.md): runtime choices and budgets.
|
|
11
|
+
- [Agent instructions](../AGENTS.md): repository layout, commands and enforced rules.
|
|
12
|
+
- [Package proof](../proof/results.json): reproducible measurements with dates and expiry.
|
package/docs/STACK.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Stack card
|
|
2
|
+
|
|
3
|
+
1. **Runs on:** Node >=22 for the installer, doctor and measurement; macOS for native app control.
|
|
4
|
+
2. **User supplies:** Python 3, cua-driver, macOS Accessibility and Screen Recording permissions, and an AI host that reads skill Markdown.
|
|
5
|
+
3. **Install:** `npx lean-computer-use`, one confirmation; optional hooks use `--with-hooks`.
|
|
6
|
+
4. **Pick:** JavaScript ESM checked with TypeScript `checkJs`, plus Python standard library desk tools and hooks because the driver wrappers were Python.
|
|
7
|
+
5. **Budgets:** zero runtime dependencies; packed tarball at most 300 KB, unpacked at most 1 MB; cold `--help` at most 300 ms; offline checks on 2000 files under 2 s; uninstall removes every file the installer wrote that still matches its recorded hash; a file you edited stays in place and is listed for review.
|
|
8
|
+
6. **Checks:** `test/budgets.test.mjs`, `test/installer.test.mjs`, `node --test`, and `python3 -m pytest -q desk/tests hooks/tests`; package receipts live in `proof/results.json`.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Install cua-driver carefully
|
|
2
|
+
|
|
3
|
+
The upstream driver controls native Mac apps through macOS permissions.
|
|
4
|
+
Use [trycua/cua's README](https://github.com/trycua/cua) for its current installation command and supported release.
|
|
5
|
+
This package does not bundle or install cua-driver.
|
|
6
|
+
The vendor release, checksum and command behavior below are UNVERIFIED for your installed version until you check them locally.
|
|
7
|
+
|
|
8
|
+
## Verify the download
|
|
9
|
+
|
|
10
|
+
Compare the downloaded artifact with a checksum published by the upstream project when one is available.
|
|
11
|
+
Confirm the signature's identity against upstream release information.
|
|
12
|
+
For the app bundle, use Apple's signature and notarization checks:
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
codesign --verify --deep --strict /Applications/CuaDriver.app
|
|
16
|
+
spctl --assess --verbose /Applications/CuaDriver.app
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Resolve a failed assessment before granting permissions.
|
|
20
|
+
These commands verify the local artifact; a successful check alone does not establish that it is the release you intended to download.
|
|
21
|
+
|
|
22
|
+
## Check telemetry and permissions
|
|
23
|
+
|
|
24
|
+
Inspect the commands offered by your installed version:
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
cua-driver --version
|
|
28
|
+
cua-driver telemetry --help
|
|
29
|
+
cua-driver telemetry status
|
|
30
|
+
cua-driver permissions status --json
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Disable telemetry with the installed version's supported command (`cua-driver telemetry disable` when listed), then check `cua-driver telemetry status` again.
|
|
34
|
+
The exact disable subcommand is UNVERIFIED until confirmed by your local help output.
|
|
35
|
+
|
|
36
|
+
Grant Accessibility and Screen Recording deliberately:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
cua-driver permissions grant
|
|
40
|
+
cua-driver permissions status --json
|
|
41
|
+
cua-driver status
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Confirm that `accessibility` and `screen_recording` are true.
|
|
45
|
+
Review macOS System Settings if the permission command opens a settings pane.
|
|
46
|
+
Keep credentials and other sensitive content out of app briefs.
|
|
47
|
+
|
|
48
|
+
## Use and close
|
|
49
|
+
|
|
50
|
+
`desk` starts the app-backed daemon on demand in the background, retaining its macOS permission identity.
|
|
51
|
+
After a run, call `desk-clean <bundle> --app-name <Name>`.
|
|
52
|
+
Add `--launched-by-run` only when the initial snapshot showed the app closed and this run launched it.
|
|
53
|
+
This requests a gentle quit, removes owned desk state and stops the driver when no live sessions remain.
|
|
54
|
+
An app that remains running is a pending cleanup result; keep its unsaved work safe and report it.
|
|
55
|
+
|
|
56
|
+
Use `npx lean-computer-use doctor` to check the installation and receive one-line fixes.
|
|
57
|
+
On other platforms the driver checks are warnings and package checks still run.
|
package/docs/install.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Install
|
|
2
|
+
|
|
3
|
+
Use Node >=22 to run the installer.
|
|
4
|
+
Native app control needs macOS, Python 3 and the separately installed cua-driver.
|
|
5
|
+
Follow [the driver precautions](cua-driver-install.md) before granting system permissions.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npx lean-computer-use
|
|
9
|
+
npx lean-computer-use --uninstall
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The first command previews its changes and asks for one confirmation.
|
|
13
|
+
The second is the removal alias; `npx lean-computer-use uninstall` is equivalent.
|
|
14
|
+
The installer copies the skill and runner to `~/.claude` and the desk tools and hooks to `~/.local/share/lean-computer-use`.
|
|
15
|
+
The brief template is installed with the skill at `~/.claude/skills/lean-computer-use/desk-brief.md`.
|
|
16
|
+
It does not install the driver or perform a vendor login.
|
|
17
|
+
|
|
18
|
+
Preview a custom target without writing:
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
npx lean-computer-use install --dir <host-dir> --share <share-dir> --dry-run --yes
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`--dry` is an alias for `--dry-run`.
|
|
25
|
+
Use `--yes` for an unattended install after reviewing the plan.
|
|
26
|
+
`LEAN_COMPUTER_USE_HOME` sets the share root when `--share` is absent.
|
|
27
|
+
A differing existing file needs `--yes` to replace it; the installer keeps a `.bak` copy.
|
|
28
|
+
|
|
29
|
+
## Optional hooks
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
npx lean-computer-use install --with-hooks
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
This merges marked registrations into `<host-dir>/settings.json` after a backup.
|
|
36
|
+
Other settings and hooks remain in place.
|
|
37
|
+
Repeated installation keeps one copy of each registration.
|
|
38
|
+
Without this flag the installer prints the installed `settings-snippet.json` path and merge instructions.
|
|
39
|
+
For a manual merge, replace the hook-directory placeholder as instructed, append the snippet's event entries to the matching `hooks` arrays, and retain existing entries.
|
|
40
|
+
Restart the host after changing hooks, then run `npx lean-computer-use doctor`.
|
|
41
|
+
|
|
42
|
+
## Activate
|
|
43
|
+
|
|
44
|
+
Start a fresh host session so it can discover `skills/lean-computer-use/SKILL.md` and `agents/desk-runner.md`.
|
|
45
|
+
Ask: "Use lean-computer-use. Give desk-runner one Calculator task, verify the saved filtered read, and report cleanup."
|
|
46
|
+
Review the [brief template](../briefs/desk-brief.md) before handing an agent an app task.
|
|
47
|
+
|
|
48
|
+
## Other hosts
|
|
49
|
+
|
|
50
|
+
Copy `skills/lean-computer-use/` to the host's documented skill directory and point its runner at the desk tools.
|
|
51
|
+
Copy `briefs/desk-brief.md` into that skill directory as `desk-brief.md`.
|
|
52
|
+
The automatic installation target is Claude Code.
|
|
53
|
+
|
|
54
|
+
## Remove
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
npx lean-computer-use uninstall --dry-run
|
|
58
|
+
npx lean-computer-use uninstall
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Removal uses the install manifest and preserves user-edited files.
|
|
62
|
+
Read [the installer contract](INSTALLER.md) for manifest recovery and manual hook cleanup.
|
package/hooks/README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Native-app hooks
|
|
2
|
+
|
|
3
|
+
The optional hooks help a runner stop a repeated error and combine small actions. Install their registrations with `npx lean-computer-use --with-hooks`. The installer merges [settings-snippet.json](settings-snippet.json) into the selected Claude Code settings file, substitutes the installed hook folder for `__HOOKS_DIR__`, and marks the entries `lean-computer-use`. Uninstall removes those marked entries while preserving other settings.
|
|
4
|
+
|
|
5
|
+
Claude Code calls a self-locating shell shim with a JSON payload on stdin. The shim runs its neighboring Python file, which imports `_common.py`. Python uses only its standard library. The hooks run locally and make no network requests. Nothing watches their state or alerts when a hook fails open; the runner still stops and reports after the same error twice.
|
|
6
|
+
|
|
7
|
+
| Hook | Trigger | Result |
|
|
8
|
+
| --- | --- | --- |
|
|
9
|
+
| `native_retry_guard.sh` | PreToolUse and PostToolUse for native MCP tools and desk commands | Records an error result and blocks a third attempt with identical tool name and input after two errors |
|
|
10
|
+
| `native_batch_nudge.sh` | PostToolUse for native MCP tools and desk commands | Adds one non-blocking context hint after consecutive single actions |
|
|
11
|
+
|
|
12
|
+
MCP matchers cover `mcp__computer-use__.*` and `mcp__cua-driver__.*`. Bash has its own registration. The scripts match `desk`, `desk-read` and `desk-clean` only when one is the first word of a command segment. Quoted arguments and unrelated shell commands stay outside that match. Here-document payloads are ignored.
|
|
13
|
+
|
|
14
|
+
## State and closed loop
|
|
15
|
+
|
|
16
|
+
State lives under `LEAN_COMPUTER_USE_STATE`, or under `${TMPDIR:-/tmp}/lean-computer-use-<uid>` by default. The directory must be owned by the current user with mode 0700. State and lock files use mode 0600. A session's filename is its SHA-256 prefix; a subagent id joins that identity when present. Each hook updates its own section under one file lock. Inputs and responses are represented by hashes rather than saved screen text.
|
|
17
|
+
|
|
18
|
+
The retry hook records explicit error flags, response text starting with `error`, and failing desk command responses. A success with the same input clears that error count. A successful call to a different target also clears it. A different failed call starts a new count. Probe tools (`list_apps`, `list_windows`, `launch_app`, `end_session`, `status`, `list_tools`) never count or block.
|
|
19
|
+
|
|
20
|
+
The batch hint appears once per streak. A probe, a filtered `desk-read`, cleanup, or a call chaining desk commands with `&&` resets the streak. The runner chains the remaining actions, reads once, and returns the saved read for independent verification.
|
|
21
|
+
|
|
22
|
+
## Failure and verification
|
|
23
|
+
|
|
24
|
+
Malformed JSON, a missing session id, an unsafe state path, or an internal exception allows the tool call. A missing neighboring Python file also allows it. Exit 2 from the retry guard is the sole block signal; its stderr tells the runner to stop, report the error, and try a different approach. The hint uses PostToolUse `additionalContext` JSON on stdout and always exits 0.
|
|
25
|
+
|
|
26
|
+
Run `python3 -m pytest -q hooks/tests` with the package acceptance suite. The same contracts can run without pytest using `python3 -m unittest discover -s hooks/tests`. They send synthetic stdin payloads, use temporary state under the test folder, verify secure file modes, and invoke both shims. The Python sources and settings snippet in this folder are the source of truth.
|