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.
Files changed (53) hide show
  1. package/AGENTS.md +71 -0
  2. package/CHANGELOG.md +18 -0
  3. package/CLA.md +37 -0
  4. package/CLAUDE.md +5 -0
  5. package/CODE_OF_CONDUCT.md +127 -0
  6. package/CONTRIBUTING.md +28 -0
  7. package/CREDITS.md +17 -0
  8. package/LICENSE +21 -0
  9. package/MAINTAINERS.md +8 -0
  10. package/README.md +233 -2
  11. package/RELEASING.md +49 -0
  12. package/REPO_SETTINGS.md +26 -0
  13. package/SECURITY.md +18 -0
  14. package/agents/desk-runner.md +48 -0
  15. package/bin/lean-computer-use.mjs +9 -0
  16. package/briefs/desk-brief.md +26 -0
  17. package/desk/README.md +64 -0
  18. package/desk/_resolve.py +279 -0
  19. package/desk/desk +33 -0
  20. package/desk/desk-clean +177 -0
  21. package/desk/desk-read +39 -0
  22. package/desk/exit-codes.json +13 -0
  23. package/docs/INSTALLER.md +62 -0
  24. package/docs/METHOD.md +17 -0
  25. package/docs/PROVENANCE.md +19 -0
  26. package/docs/README.md +12 -0
  27. package/docs/STACK.md +8 -0
  28. package/docs/cua-driver-install.md +57 -0
  29. package/docs/install.md +62 -0
  30. package/hooks/README.md +26 -0
  31. package/hooks/_common.py +172 -0
  32. package/hooks/native_batch_nudge.py +37 -0
  33. package/hooks/native_batch_nudge.sh +6 -0
  34. package/hooks/native_retry_guard.py +36 -0
  35. package/hooks/native_retry_guard.sh +8 -0
  36. package/hooks/settings-snippet.json +34 -0
  37. package/llms.txt +48 -0
  38. package/package.json +23 -4
  39. package/skills/lean-computer-use/SKILL.md +70 -0
  40. package/skills/lean-computer-use/methods/background-only.md +14 -0
  41. package/skills/lean-computer-use/methods/cheap-helper-or-subagent.md +18 -0
  42. package/skills/lean-computer-use/methods/cleanup-with-proof.md +24 -0
  43. package/skills/lean-computer-use/methods/desk-run.md +19 -0
  44. package/skills/lean-computer-use/methods/failure-modes.md +16 -0
  45. package/skills/lean-computer-use/methods/filtered-reads.md +30 -0
  46. package/skills/lean-computer-use/methods/same-app-chaining.md +20 -0
  47. package/skills/lean-computer-use/methods/verify.md +14 -0
  48. package/src/cli.mjs +33 -0
  49. package/src/doctor.mjs +87 -0
  50. package/src/hooks-register.mjs +56 -0
  51. package/src/install.mjs +291 -0
  52. package/src/measure.mjs +184 -0
  53. package/src/paths.mjs +81 -0
package/src/paths.mjs ADDED
@@ -0,0 +1,81 @@
1
+ import { homedir } from 'node:os';
2
+ import { dirname, resolve, join, relative, isAbsolute, sep } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { lstatSync, existsSync, mkdirSync, readdirSync, readFileSync } from 'node:fs';
5
+ import { createHash } from 'node:crypto';
6
+
7
+ export const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
8
+ export const digest = value => createHash('sha256').update(value).digest('hex');
9
+ export const shellQuote = value => "'" + value.split("'").join("'\\''") + "'";
10
+
11
+ /** @param {{dir?: string, share?: string}} options @param {NodeJS.ProcessEnv} env */
12
+ export function locations(options = {}, env = process.env) {
13
+ return {
14
+ dir: resolve(options.dir || join(env.HOME || homedir(), '.claude')),
15
+ share: resolve(options.share || env.LEAN_COMPUTER_USE_HOME || join(env.HOME || homedir(), '.local/share/lean-computer-use')),
16
+ };
17
+ }
18
+
19
+ export function safeRelative(value) {
20
+ if (typeof value !== 'string' || !value || isAbsolute(value) || value.includes('\\') || value.split('/').some(part => !part || part === '.' || part === '..')) {
21
+ throw new Error(`Unsafe relative path: ${String(value)}`);
22
+ }
23
+ return value;
24
+ }
25
+
26
+ export function safePath(root, name = '') {
27
+ const absolute = name ? resolve(root, safeRelative(name)) : resolve(root);
28
+ const fromRoot = relative(resolve(root), absolute);
29
+ if (fromRoot === '..' || fromRoot.startsWith(`..${sep}`) || isAbsolute(fromRoot)) throw new Error('Path escapes its root');
30
+ let current = absolute;
31
+ while (true) {
32
+ try {
33
+ const stat = lstatSync(current);
34
+ if (stat.isSymbolicLink()) throw new Error(`Symlink refused: ${current}`);
35
+ if (current !== absolute && !stat.isDirectory()) throw new Error(`Parent is not a directory: ${current}`);
36
+ } catch (error) {
37
+ if (error.code !== 'ENOENT') throw error;
38
+ }
39
+ const parent = dirname(current);
40
+ if (parent === current) break;
41
+ current = parent;
42
+ }
43
+ return absolute;
44
+ }
45
+
46
+ export function ensureDirectory(path, created) {
47
+ safePath(path);
48
+ if (existsSync(path)) {
49
+ if (!lstatSync(path).isDirectory()) throw new Error(`Expected a directory: ${path}`);
50
+ return;
51
+ }
52
+ ensureDirectory(dirname(path), created);
53
+ mkdirSync(path, { mode: 0o755 });
54
+ created.push(path);
55
+ }
56
+
57
+ export function publicFiles(root, prefix = '') {
58
+ if (!existsSync(root)) throw new Error(`Package files missing: ${root}`);
59
+ return readdirSync(root, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name)).flatMap(entry => {
60
+ if (['tests', '__pycache__', '.pytest_cache'].includes(entry.name) || entry.name.endsWith('.pyc')) return [];
61
+ const name = prefix ? `${prefix}/${entry.name}` : entry.name;
62
+ if (entry.isSymbolicLink()) throw new Error(`Package symlink refused: ${name}`);
63
+ return entry.isDirectory() ? publicFiles(join(root, entry.name), name) : [name];
64
+ });
65
+ }
66
+
67
+ export function payloadFiles(roots, source = packageRoot) {
68
+ const files = [];
69
+ for (const [folder, root, destination] of [
70
+ ['skills/lean-computer-use', 'dir', 'skills/lean-computer-use'],
71
+ ['desk', 'share', 'desk'], ['hooks', 'share', 'hooks'],
72
+ ]) {
73
+ for (const name of publicFiles(join(source, folder))) {
74
+ files.push({ root, path: `${destination}/${name}`, content: readFileSync(join(source, folder, name)), mode: folder === 'desk' && ['desk', 'desk-read', 'desk-clean', '_resolve.py'].includes(name) || name.endsWith('.sh') ? 0o755 : 0o644 });
75
+ }
76
+ }
77
+ files.push({ root: 'dir', path: 'skills/lean-computer-use/desk-brief.md', content: readFileSync(join(source, 'briefs/desk-brief.md')), mode: 0o644 });
78
+ const agent = readFileSync(join(source, 'agents/desk-runner.md'), 'utf8').replaceAll('__DESK_DIR__', shellQuote(join(roots.share, 'desk')));
79
+ files.push({ root: 'dir', path: 'agents/desk-runner.md', content: Buffer.from(agent), mode: 0o644 });
80
+ return files;
81
+ }