@moxxy/plugin-computer-control 0.41.2 → 0.42.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 (269) hide show
  1. package/bin/win32-x64/moxxy-computer.exe +0 -0
  2. package/bin/win32-x64/moxxy-computer.exe.json +1 -1
  3. package/dist/backend/access.d.ts +129 -0
  4. package/dist/backend/access.d.ts.map +1 -0
  5. package/dist/backend/access.js +158 -0
  6. package/dist/backend/access.js.map +1 -0
  7. package/dist/backend/app-hints.d.ts +14 -0
  8. package/dist/backend/app-hints.d.ts.map +1 -0
  9. package/dist/backend/app-hints.js +30 -0
  10. package/dist/backend/app-hints.js.map +1 -0
  11. package/dist/backend/backend.d.ts +70 -0
  12. package/dist/backend/backend.d.ts.map +1 -0
  13. package/dist/backend/backend.js +466 -0
  14. package/dist/backend/backend.js.map +1 -0
  15. package/dist/backend/rpc.d.ts +1438 -0
  16. package/dist/backend/rpc.d.ts.map +1 -0
  17. package/dist/backend/rpc.js +75 -0
  18. package/dist/backend/rpc.js.map +1 -0
  19. package/dist/backend/turn-controls.d.ts +20 -0
  20. package/dist/backend/turn-controls.d.ts.map +1 -0
  21. package/dist/backend/turn-controls.js +115 -0
  22. package/dist/backend/turn-controls.js.map +1 -0
  23. package/dist/contract/guidance.d.ts +12 -0
  24. package/dist/contract/guidance.d.ts.map +1 -0
  25. package/dist/contract/guidance.js +42 -0
  26. package/dist/contract/guidance.js.map +1 -0
  27. package/dist/contract/image.d.ts +29 -0
  28. package/dist/contract/image.d.ts.map +1 -0
  29. package/dist/contract/image.js +46 -0
  30. package/dist/contract/image.js.map +1 -0
  31. package/dist/contract/keys.d.ts +15 -0
  32. package/dist/contract/keys.d.ts.map +1 -0
  33. package/dist/contract/keys.js +118 -0
  34. package/dist/contract/keys.js.map +1 -0
  35. package/dist/contract/outcome.d.ts +61 -0
  36. package/dist/contract/outcome.d.ts.map +1 -0
  37. package/dist/contract/outcome.js +63 -0
  38. package/dist/contract/outcome.js.map +1 -0
  39. package/dist/contract/progress.d.ts +27 -0
  40. package/dist/contract/progress.d.ts.map +1 -0
  41. package/dist/contract/progress.js +36 -0
  42. package/dist/contract/progress.js.map +1 -0
  43. package/dist/contract/tools.d.ts +338 -0
  44. package/dist/contract/tools.d.ts.map +1 -0
  45. package/dist/contract/tools.js +195 -0
  46. package/dist/contract/tools.js.map +1 -0
  47. package/dist/contract/untrusted.d.ts +3 -0
  48. package/dist/contract/untrusted.d.ts.map +1 -0
  49. package/dist/contract/untrusted.js +8 -0
  50. package/dist/contract/untrusted.js.map +1 -0
  51. package/dist/helper/artifact.d.ts +41 -0
  52. package/dist/helper/artifact.d.ts.map +1 -0
  53. package/dist/helper/artifact.js +189 -0
  54. package/dist/helper/artifact.js.map +1 -0
  55. package/dist/helper/protocol.d.ts +80 -0
  56. package/dist/helper/protocol.d.ts.map +1 -0
  57. package/dist/helper/protocol.js +49 -0
  58. package/dist/helper/protocol.js.map +1 -0
  59. package/dist/helper/transport.d.ts +43 -0
  60. package/dist/helper/transport.d.ts.map +1 -0
  61. package/dist/{windows → helper}/transport.js +45 -19
  62. package/dist/helper/transport.js.map +1 -0
  63. package/dist/index.d.ts +9 -22
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.js +35 -42
  66. package/dist/index.js.map +1 -1
  67. package/dist/jev/ladder.d.ts +32 -0
  68. package/dist/jev/ladder.d.ts.map +1 -0
  69. package/dist/jev/ladder.js +66 -0
  70. package/dist/jev/ladder.js.map +1 -0
  71. package/dist/jev/memory.d.ts +11 -0
  72. package/dist/jev/memory.d.ts.map +1 -0
  73. package/dist/jev/memory.js +16 -0
  74. package/dist/jev/memory.js.map +1 -0
  75. package/dist/jev/run.d.ts +93 -0
  76. package/dist/jev/run.d.ts.map +1 -0
  77. package/dist/jev/run.js +403 -0
  78. package/dist/jev/run.js.map +1 -0
  79. package/dist/jev/train.d.ts +97 -0
  80. package/dist/jev/train.d.ts.map +1 -0
  81. package/dist/jev/train.js +28 -0
  82. package/dist/jev/train.js.map +1 -0
  83. package/dist/linux/profile.d.ts +6 -0
  84. package/dist/linux/profile.d.ts.map +1 -0
  85. package/dist/linux/profile.js +21 -0
  86. package/dist/linux/profile.js.map +1 -0
  87. package/dist/macos/profile.d.ts +5 -0
  88. package/dist/macos/profile.d.ts.map +1 -0
  89. package/dist/macos/profile.js +17 -0
  90. package/dist/macos/profile.js.map +1 -0
  91. package/dist/preview/controller.d.ts +127 -0
  92. package/dist/preview/controller.d.ts.map +1 -0
  93. package/dist/preview/controller.js +203 -0
  94. package/dist/preview/controller.js.map +1 -0
  95. package/dist/preview/surface.d.ts +9 -0
  96. package/dist/preview/surface.d.ts.map +1 -0
  97. package/dist/preview/surface.js +50 -0
  98. package/dist/preview/surface.js.map +1 -0
  99. package/dist/windows/maintenance.d.ts +1 -1
  100. package/dist/windows/maintenance.d.ts.map +1 -1
  101. package/dist/windows/maintenance.js +6 -5
  102. package/dist/windows/maintenance.js.map +1 -1
  103. package/dist/windows/profile.d.ts +5 -0
  104. package/dist/windows/profile.d.ts.map +1 -0
  105. package/dist/windows/profile.js +17 -0
  106. package/dist/windows/profile.js.map +1 -0
  107. package/learned/README.md +26 -0
  108. package/learned/cases/com.apple.calculator.json +395 -0
  109. package/learned/cases/com.apple.finder.json +115 -0
  110. package/learned/cases/com.apple.safari.json +112 -0
  111. package/learned/cases/com.apple.systempreferences.json +255 -0
  112. package/learned/com.apple.calculator-eace95fc.json +334 -0
  113. package/learned/com.apple.finder-27cf6ce8.json +260 -0
  114. package/learned/com.apple.safari-7cd9df4f.json +143 -0
  115. package/learned/com.apple.systempreferences-02cf0b8b.json +535 -0
  116. package/package.json +13 -7
  117. package/scripts/promote-learned.mjs +8 -0
  118. package/scripts/summarize-trial.mjs +68 -0
  119. package/scripts/train-learned.mjs +64 -0
  120. package/skills/computer-apps/blender.md +29 -0
  121. package/skills/computer-apps/browsers.md +50 -0
  122. package/skills/computer-apps/design-tools.md +37 -0
  123. package/skills/computer-apps/finder.md +23 -0
  124. package/skills/computer-apps/office.md +46 -0
  125. package/skills/computer-apps/video-editors.md +44 -0
  126. package/skills/computer-control.md +154 -191
  127. package/src/backend/access.test.ts +158 -0
  128. package/src/backend/access.ts +168 -0
  129. package/src/backend/app-hints.test.ts +59 -0
  130. package/src/backend/app-hints.ts +39 -0
  131. package/src/backend/backend.test.ts +725 -0
  132. package/src/backend/backend.ts +482 -0
  133. package/src/backend/contract-helper.fixture.mjs +138 -0
  134. package/src/backend/helper.fixture.ts +38 -0
  135. package/src/backend/rpc.ts +90 -0
  136. package/src/backend/turn-controls.test.ts +178 -0
  137. package/src/backend/turn-controls.ts +120 -0
  138. package/src/contract/guidance.test.ts +75 -0
  139. package/src/contract/guidance.ts +48 -0
  140. package/src/contract/image.test.ts +74 -0
  141. package/src/contract/image.ts +54 -0
  142. package/src/contract/keys.test.ts +103 -0
  143. package/src/contract/keys.ts +117 -0
  144. package/src/contract/outcome.test.ts +71 -0
  145. package/src/contract/outcome.ts +72 -0
  146. package/src/contract/progress.test.ts +56 -0
  147. package/src/contract/progress.ts +43 -0
  148. package/src/contract/tools.test.ts +210 -0
  149. package/src/contract/tools.ts +212 -0
  150. package/src/contract/untrusted.test.ts +20 -0
  151. package/src/contract/untrusted.ts +8 -0
  152. package/src/helper/artifact.test.ts +131 -0
  153. package/src/helper/artifact.ts +182 -0
  154. package/src/helper/protocol.test.ts +29 -0
  155. package/src/helper/protocol.ts +50 -0
  156. package/src/{windows → helper}/transport.test.ts +76 -19
  157. package/src/{windows → helper}/transport.ts +54 -16
  158. package/src/index.test.ts +112 -0
  159. package/src/index.ts +36 -53
  160. package/src/jev/ladder.test.ts +113 -0
  161. package/src/jev/ladder.ts +88 -0
  162. package/src/jev/memory.ts +20 -0
  163. package/src/jev/run.test.ts +700 -0
  164. package/src/jev/run.ts +443 -0
  165. package/src/jev/train.test.ts +40 -0
  166. package/src/jev/train.ts +41 -0
  167. package/src/linux/helper.test.ts +481 -0
  168. package/src/linux/profile.ts +25 -0
  169. package/src/macos/helper.test.ts +988 -0
  170. package/src/macos/profile.ts +19 -0
  171. package/src/preview/controller.test.ts +308 -0
  172. package/src/preview/controller.ts +262 -0
  173. package/src/preview/surface.test.ts +68 -0
  174. package/src/preview/surface.ts +45 -0
  175. package/src/skill.test.ts +34 -0
  176. package/src/windows/maintenance.ts +8 -7
  177. package/src/windows/profile.ts +19 -0
  178. package/dist/shell.d.ts +0 -56
  179. package/dist/shell.d.ts.map +0 -1
  180. package/dist/shell.js +0 -189
  181. package/dist/shell.js.map +0 -1
  182. package/dist/temporary-files.d.ts +0 -2
  183. package/dist/temporary-files.d.ts.map +0 -1
  184. package/dist/temporary-files.js +0 -10
  185. package/dist/temporary-files.js.map +0 -1
  186. package/dist/tools/applescript.d.ts +0 -2
  187. package/dist/tools/applescript.d.ts.map +0 -1
  188. package/dist/tools/applescript.js +0 -50
  189. package/dist/tools/applescript.js.map +0 -1
  190. package/dist/tools/click.d.ts +0 -2
  191. package/dist/tools/click.d.ts.map +0 -1
  192. package/dist/tools/click.js +0 -58
  193. package/dist/tools/click.js.map +0 -1
  194. package/dist/tools/clipboard.d.ts +0 -2
  195. package/dist/tools/clipboard.d.ts.map +0 -1
  196. package/dist/tools/clipboard.js +0 -71
  197. package/dist/tools/clipboard.js.map +0 -1
  198. package/dist/tools/key.d.ts +0 -11
  199. package/dist/tools/key.d.ts.map +0 -1
  200. package/dist/tools/key.js +0 -143
  201. package/dist/tools/key.js.map +0 -1
  202. package/dist/tools/open.d.ts +0 -2
  203. package/dist/tools/open.d.ts.map +0 -1
  204. package/dist/tools/open.js +0 -91
  205. package/dist/tools/open.js.map +0 -1
  206. package/dist/tools/screenshot.d.ts +0 -2
  207. package/dist/tools/screenshot.d.ts.map +0 -1
  208. package/dist/tools/screenshot.js +0 -162
  209. package/dist/tools/screenshot.js.map +0 -1
  210. package/dist/tools/type.d.ts +0 -8
  211. package/dist/tools/type.d.ts.map +0 -1
  212. package/dist/tools/type.js +0 -65
  213. package/dist/tools/type.js.map +0 -1
  214. package/dist/windows/artifact.d.ts +0 -3
  215. package/dist/windows/artifact.d.ts.map +0 -1
  216. package/dist/windows/artifact.js +0 -57
  217. package/dist/windows/artifact.js.map +0 -1
  218. package/dist/windows/backend.d.ts +0 -11
  219. package/dist/windows/backend.d.ts.map +0 -1
  220. package/dist/windows/backend.js +0 -122
  221. package/dist/windows/backend.js.map +0 -1
  222. package/dist/windows/contracts.d.ts +0 -1627
  223. package/dist/windows/contracts.d.ts.map +0 -1
  224. package/dist/windows/contracts.js +0 -137
  225. package/dist/windows/contracts.js.map +0 -1
  226. package/dist/windows/control-service.d.ts +0 -12
  227. package/dist/windows/control-service.d.ts.map +0 -1
  228. package/dist/windows/control-service.js +0 -65
  229. package/dist/windows/control-service.js.map +0 -1
  230. package/dist/windows/guidance.d.ts +0 -3
  231. package/dist/windows/guidance.d.ts.map +0 -1
  232. package/dist/windows/guidance.js +0 -20
  233. package/dist/windows/guidance.js.map +0 -1
  234. package/dist/windows/protocol.d.ts +0 -9
  235. package/dist/windows/protocol.d.ts.map +0 -1
  236. package/dist/windows/protocol.js +0 -32
  237. package/dist/windows/protocol.js.map +0 -1
  238. package/dist/windows/transport.d.ts +0 -23
  239. package/dist/windows/transport.d.ts.map +0 -1
  240. package/dist/windows/transport.js.map +0 -1
  241. package/src/shell.test.ts +0 -186
  242. package/src/shell.ts +0 -213
  243. package/src/temporary-files.test.ts +0 -18
  244. package/src/temporary-files.ts +0 -9
  245. package/src/tools/applescript-serialize.test.ts +0 -74
  246. package/src/tools/applescript.ts +0 -53
  247. package/src/tools/click.ts +0 -60
  248. package/src/tools/clipboard.ts +0 -72
  249. package/src/tools/key.ts +0 -155
  250. package/src/tools/open.ts +0 -96
  251. package/src/tools/screenshot.test.ts +0 -137
  252. package/src/tools/screenshot.ts +0 -180
  253. package/src/tools/type.ts +0 -68
  254. package/src/tools.test.ts +0 -94
  255. package/src/windows/action-contracts.test.ts +0 -13
  256. package/src/windows/artifact.test.ts +0 -16
  257. package/src/windows/artifact.ts +0 -57
  258. package/src/windows/backend.test.ts +0 -41
  259. package/src/windows/backend.ts +0 -122
  260. package/src/windows/contracts.test.ts +0 -81
  261. package/src/windows/contracts.ts +0 -143
  262. package/src/windows/control-service.test.ts +0 -58
  263. package/src/windows/control-service.ts +0 -68
  264. package/src/windows/guidance.test.ts +0 -29
  265. package/src/windows/guidance.ts +0 -21
  266. package/src/windows/model-contract.test.ts +0 -37
  267. package/src/windows/protocol.ts +0 -27
  268. package/src/windows/text-contracts.test.ts +0 -14
  269. package/src/windows/window-typing.test.ts +0 -14
@@ -0,0 +1,168 @@
1
+ import type { EventLogReader } from '@moxxy/sdk';
2
+ import { z } from 'zod';
3
+ import { clipboardFlagsFor, isSystemKeyCombo, parseKeyCombo, type KeyPlatform } from '../contract/keys.js';
4
+ import { ComputerUseError } from '../contract/outcome.js';
5
+ import type { ComputerAction } from '../contract/tools.js';
6
+
7
+ export const REQUEST_ACCESS_TOOL = 'computer_request_access';
8
+
9
+ export const accessTiers = ['read', 'click', 'full'] as const;
10
+ export type AccessTier = (typeof accessTiers)[number];
11
+ export type AppCategory = 'browser' | 'terminal' | 'trading';
12
+
13
+ const rank = (tier: AccessTier) => accessTiers.indexOf(tier);
14
+
15
+ export function maxTier(...tiers: AccessTier[]): AccessTier {
16
+ return tiers.reduce((highest, tier) => (rank(tier) > rank(highest) ? tier : highest), 'read');
17
+ }
18
+
19
+ /**
20
+ * What one approved `computer_request_access` call granted. It is the tool's
21
+ * own result, so the session log already records it for every client and the
22
+ * current access is a pure fold over that log.
23
+ */
24
+ export const accessGrantSchema = z.object({
25
+ kind: z.literal('computer_access'),
26
+ granted: z.array(z.object({ id: z.string().min(1).max(512), name: z.string().max(512), tier: z.enum(accessTiers) }).strict()).max(32),
27
+ unresolved: z.array(z.object({
28
+ app: z.string().max(512),
29
+ reason: z.enum(['not_found', 'ambiguous']),
30
+ candidates: z.array(z.object({ id: z.string().max(512), name: z.string().max(512) }).strict()).max(16).optional(),
31
+ }).strict()).max(32),
32
+ clipboard_read: z.boolean(),
33
+ clipboard_write: z.boolean(),
34
+ system_key_combos: z.boolean(),
35
+ }).strict();
36
+ export type AccessGrant = z.infer<typeof accessGrantSchema>;
37
+ export type AppGrant = AccessGrant['granted'][number];
38
+
39
+ export interface AccessFlags { readonly clipboardRead: boolean; readonly clipboardWrite: boolean; readonly systemKeyCombos: boolean }
40
+ export interface AccessState { readonly apps: readonly AppGrant[]; readonly flags: AccessFlags }
41
+
42
+ /** Current access in a session: approved grants in log order, a later grant of an app replacing the earlier one. */
43
+ export function accessFromLog(log: EventLogReader): AccessState {
44
+ const requested = new Set(log.ofType('tool_call_requested').filter((event) => event.name === REQUEST_ACCESS_TOOL).map((event) => event.callId));
45
+ const approved = new Set(log.ofType('tool_call_approved').map((event) => event.callId));
46
+ const apps = new Map<string, AppGrant>();
47
+ const flags = { clipboardRead: false, clipboardWrite: false, systemKeyCombos: false };
48
+ for (const result of log.ofType('tool_result')) {
49
+ if (!result.ok || !requested.has(result.callId) || !approved.has(result.callId)) continue;
50
+ const grant = accessGrantSchema.safeParse(result.output);
51
+ if (!grant.success) continue;
52
+ for (const app of grant.data.granted) apps.set(app.id, app);
53
+ flags.clipboardRead ||= grant.data.clipboard_read;
54
+ flags.clipboardWrite ||= grant.data.clipboard_write;
55
+ flags.systemKeyCombos ||= grant.data.system_key_combos;
56
+ }
57
+ return { apps: [...apps.values()], flags };
58
+ }
59
+
60
+ export const RUN_TOOL = 'computer_run';
61
+
62
+ /**
63
+ * The apps of the runs a person approved as that very call, in log order: approving a run on an app is consent
64
+ * to the app. A run let through by a standing rule names nothing, since nobody saw which app it was for.
65
+ */
66
+ export function approvedThroughRun(log: EventLogReader): string[] {
67
+ const decided = new Set(log.ofType('tool_call_approved').filter((event) => event.decidedNow === true).map((event) => event.callId));
68
+ const names = new Set<string>();
69
+ for (const event of log.ofType('tool_call_requested')) {
70
+ if (event.name !== RUN_TOOL || !decided.has(event.callId)) continue;
71
+ const app = (event.input as { app?: unknown } | null)?.app;
72
+ if (typeof app === 'string' && app.length > 0) names.add(app);
73
+ }
74
+ return [...names];
75
+ }
76
+
77
+ /** The grant for `app` (identifier or display name) if it allows `needed`. */
78
+ export function checkAccess(access: AccessState, app: string, needed: AccessTier): AppGrant {
79
+ const wanted = app.toLowerCase();
80
+ const byId = access.apps.find((grant) => grant.id.toLowerCase() === wanted);
81
+ const byName = access.apps.filter((grant) => grant.name.toLowerCase() === wanted);
82
+ if (!byId && byName.length > 1) {
83
+ throw new ComputerUseError('ambiguous_app', `Several granted apps are named "${app}": ${byName.map((grant) => grant.id).join(', ')}`);
84
+ }
85
+ const grant = byId ?? byName[0];
86
+ if (!grant) throw new ComputerUseError('app_not_allowed', `"${app}" is not granted in this conversation`);
87
+ if (rank(grant.tier) < rank(needed)) {
88
+ throw new ComputerUseError('tier_insufficient', `${grant.name} is granted at the "${grant.tier}" level; this action needs "${needed}"`);
89
+ }
90
+ return grant;
91
+ }
92
+
93
+ /** Lowest grant level that allows a step: reading, a plain left click or scroll, or full control. */
94
+ export function requiredTier(step: ComputerAction): AccessTier {
95
+ switch (step.action) {
96
+ case 'scroll': return 'click';
97
+ case 'click': return step.mouse_button === 'left' && step.modifiers === undefined ? 'click' : 'full';
98
+ default: return 'full';
99
+ }
100
+ }
101
+
102
+ /** Refuse system chords and clipboard chords the conversation has not been granted. */
103
+ export function checkKeys(step: ComputerAction, flags: AccessFlags, platform: KeyPlatform): void {
104
+ if (step.action !== 'press_key') return;
105
+ const combo = parseKeyCombo(step.key);
106
+ if (isSystemKeyCombo(combo, platform) && !flags.systemKeyCombos) {
107
+ throw new ComputerUseError('system_key_combo', `"${step.key}" acts on the whole system`);
108
+ }
109
+ const missing = clipboardFlagsFor(combo).filter((flag) => !flags[flag]);
110
+ if (missing.length > 0) throw new ComputerUseError('clipboard_not_granted', `"${step.key}" needs ${missing.join(' and ')}`);
111
+ }
112
+
113
+ // Bundle identifiers / Windows executables / Linux desktop ids (a prefix matches its sub-identifiers) and display names
114
+ // (a name matches exactly or as the first words) of apps that get a restricted default level.
115
+ const KNOWN: Readonly<Record<AppCategory, { readonly ids: readonly string[]; readonly names: readonly string[] }>> = {
116
+ browser: {
117
+ ids: [
118
+ 'com.apple.safari', 'com.apple.safaritechnologypreview', 'com.google.chrome', 'com.microsoft.edgemac', 'org.mozilla.firefox',
119
+ 'org.mozilla.nightly', 'com.brave.browser', 'com.operasoftware', 'com.vivaldi.vivaldi', 'company.thebrowser', 'org.chromium',
120
+ 'app.zen-browser', 'org.torproject.torbrowser', 'com.duckduckgo.macos.browser', 'com.openai.atlas', 'ai.perplexity.comet',
121
+ 'com.kagi.kagimacos', 'google-chrome', 'chromium', 'chromium-browser', 'firefox', 'firefox-esr', 'brave-browser',
122
+ 'microsoft-edge', 'vivaldi-stable', 'org.gnome.epiphany', 'org.kde.falkon', 'chrome.exe', 'msedge.exe', 'firefox.exe', 'brave.exe', 'opera.exe', 'vivaldi.exe', 'arc.exe',
123
+ ],
124
+ names: [
125
+ 'safari', 'google chrome', 'chrome', 'chromium', 'firefox', 'microsoft edge', 'brave', 'brave browser', 'opera', 'vivaldi',
126
+ 'arc', 'dia', 'web', 'falkon', 'tor browser', 'duckduckgo', 'zen', 'zen browser', 'orion', 'atlas', 'comet',
127
+ ],
128
+ },
129
+ terminal: {
130
+ ids: [
131
+ 'com.apple.terminal', 'com.googlecode.iterm2', 'dev.warp', 'com.github.wez.wezterm', 'org.alacritty', 'io.alacritty',
132
+ 'net.kovidgoyal.kitty', 'co.zeit.hyper', 'com.mitchellh.ghostty', 'com.microsoft.vscode', 'com.vscodium',
133
+ 'com.todesktop.230313mzl4w4u92', 'com.exafunction.windsurf', 'dev.zed', 'com.jetbrains', 'com.google.android.studio',
134
+ 'com.sublimetext', 'org.vim.macvim', 'org.gnu.emacs', 'com.apple.dt.xcode', 'com.apple.scripteditor2', 'com.apple.automator',
135
+ 'com.apple.shortcuts', 'org.gnome.terminal', 'org.gnome.console', 'org.gnome.ptyxis', 'org.kde.konsole', 'org.kde.kate',
136
+ 'org.xfce.terminal', 'xfce4-terminal', 'com.gexperts.tilix', 'xterm', 'code', 'codium', 'org.gnome.builder', 'windowsterminal.exe', 'powershell.exe', 'pwsh.exe', 'cmd.exe', 'code.exe', 'cursor.exe',
137
+ ],
138
+ names: [
139
+ 'terminal', 'windows terminal', 'gnome terminal', 'konsole', 'xterm', 'tilix', 'ptyxis', 'iterm', 'iterm2', 'warp', 'wezterm', 'alacritty', 'kitty', 'hyper', 'ghostty',
140
+ 'visual studio code', 'code', 'cursor', 'windsurf', 'zed', 'intellij idea', 'pycharm', 'webstorm', 'clion', 'goland', 'rider',
141
+ 'android studio', 'xcode', 'sublime text', 'macvim', 'emacs', 'powershell', 'command prompt', 'script editor', 'automator', 'shortcuts',
142
+ ],
143
+ },
144
+ trading: {
145
+ ids: ['com.webull', 'com.tastytrade', 'com.tradingview', 'com.fidelity.activetrader', 'com.binance', 'com.electron.exodus', 'com.ledger.live', 'io.trezor'],
146
+ names: [
147
+ 'robinhood', 'webull', 'thinkorswim', 'tradingview', 'tastytrade', 'interactive brokers', 'trader workstation', 'metatrader',
148
+ 'binance', 'coinbase', 'kraken', 'ledger live', 'trezor suite', 'exodus', 'electrum',
149
+ ],
150
+ },
151
+ };
152
+
153
+ export function categorize(app: { readonly id: string; readonly name: string }): AppCategory | null {
154
+ const id = app.id.toLowerCase();
155
+ const name = app.name.toLowerCase().trim();
156
+ for (const [category, known] of Object.entries(KNOWN) as Array<[AppCategory, (typeof KNOWN)[AppCategory]]>) {
157
+ if (known.ids.some((prefix) => id === prefix || id.startsWith(`${prefix}.`))) return category;
158
+ if (known.names.some((word) => name === word || name.startsWith(`${word} `))) return category;
159
+ }
160
+ return null;
161
+ }
162
+
163
+ /** Browsers and trading apps are read-only and terminals click-only unless the user approves full access. */
164
+ export function defaultTier(category: AppCategory | null): AccessTier {
165
+ if (category === 'browser' || category === 'trading') return 'read';
166
+ if (category === 'terminal') return 'click';
167
+ return 'full';
168
+ }
@@ -0,0 +1,59 @@
1
+ import { mkdtempSync, writeFileSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { parseFrontmatterFile, skillFrontmatterSchema } from '@moxxy/sdk';
5
+ import { readdirSync, readFileSync } from 'node:fs';
6
+ import { describe, expect, it } from 'vitest';
7
+ import { appHintsDirectory, hintFor, loadAppHints } from './app-hints.js';
8
+ import { removeDirSync } from '@moxxy/vitest-preset/fs';
9
+
10
+ describe('app hints', () => {
11
+ it('reads each hint file as the apps it is for and the text to show', () => {
12
+ const directory = mkdtempSync(join(tmpdir(), 'moxxy-hints-'));
13
+ try {
14
+ writeFileSync(join(directory, 'notes.md'), '---\nname: computer-app-notes\ndescription: Notes\napps:\n - com.apple.Notes\n - Notes\n---\n\nUse the search field.\n');
15
+ writeFileSync(join(directory, 'broken.md'), 'no frontmatter here');
16
+ writeFileSync(join(directory, 'ignored.txt'), 'x');
17
+ const hints = loadAppHints(directory);
18
+ expect(hints).toEqual([{ apps: ['com.apple.notes', 'notes'], text: 'Use the search field.' }]);
19
+ expect(hintFor(hints, { id: 'com.apple.Notes', name: 'Whatever' })).toBe('Use the search field.');
20
+ expect(hintFor(hints, { id: 'x.y.z', name: 'NOTES' })).toBe('Use the search field.');
21
+ expect(hintFor(hints, { id: 'com.apple.Notes.2', name: '' })).toBe('Use the search field.');
22
+ expect(hintFor(hints, { id: 'x', name: 'Notes 2026' })).toBe('Use the search field.');
23
+ expect(hintFor(hints, { id: 'com.apple.NotesHelper', name: 'Notesy' })).toBeUndefined();
24
+ expect(hintFor(hints, { id: 'com.apple.TextEdit', name: 'TextEdit' })).toBeUndefined();
25
+ } finally { removeDirSync(directory); }
26
+ });
27
+
28
+ it('returns no hints for a directory that does not exist', () => {
29
+ expect(loadAppHints(join(tmpdir(), 'moxxy-no-such-hints'))).toEqual([]);
30
+ });
31
+
32
+ it('ships hints for browsers, Finder, office suites, video editors, design tools and 3D tools', () => {
33
+ const hints = loadAppHints();
34
+ const text = (id: string) => hintFor(hints, { id, name: '' });
35
+ expect(text('com.apple.Safari')).toMatch(/browser/i);
36
+ expect(text('com.google.Chrome')).toMatch(/browser/i);
37
+ expect(text('com.apple.finder')).toBeTruthy();
38
+ expect(text('com.microsoft.Excel')).toBeTruthy();
39
+ expect(text('com.apple.iWork.Pages')).toBeTruthy();
40
+ expect(text('com.blackmagic-design.DaVinciResolve')).toMatch(/timeline/i);
41
+ expect(text('com.apple.FinalCut')).toMatch(/timeline/i);
42
+ expect(text('com.adobe.PremierePro.25')).toMatch(/timeline/i);
43
+ expect(text('com.figma.Desktop')).toMatch(/canvas/i);
44
+ expect(text('com.adobe.Photoshop')).toMatch(/canvas/i);
45
+ expect(text('org.blenderfoundation.blender')).toMatch(/F3/);
46
+ expect(hintFor(hints, { id: 'blender', name: 'Blender' })).toMatch(/F3/);
47
+ });
48
+
49
+ it('keeps every shipped hint a valid skill and short enough to show inline', () => {
50
+ const files = readdirSync(appHintsDirectory).filter((file) => file.endsWith('.md'));
51
+ expect(files.length).toBeGreaterThanOrEqual(5);
52
+ for (const file of files) {
53
+ const { frontmatter, body } = parseFrontmatterFile(readFileSync(join(appHintsDirectory, file), 'utf8'));
54
+ expect(skillFrontmatterSchema.safeParse(frontmatter).success, file).toBe(true);
55
+ expect((frontmatter as { apps?: unknown[] }).apps?.length ?? 0, file).toBeGreaterThan(0);
56
+ expect(body.trim().length, file).toBeLessThanOrEqual(1500);
57
+ }
58
+ });
59
+ });
@@ -0,0 +1,39 @@
1
+ import { readdirSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { parseFrontmatterFile } from '@moxxy/sdk';
5
+ import { z } from 'zod';
6
+
7
+ /** What the model should know about one family of apps before it drives them. */
8
+ export interface AppHint {
9
+ /** Lower-case bundle identifiers, app ids or names this hint is for; a versioned identifier or name matches its base. */
10
+ readonly apps: readonly string[];
11
+ readonly text: string;
12
+ }
13
+
14
+ /** Each file is also a regular skill; `apps` in its frontmatter says whose hint it is. */
15
+ export const appHintsDirectory = fileURLToPath(new URL('../../skills/computer-apps', import.meta.url));
16
+
17
+ const appsSchema = z.object({ apps: z.array(z.string().min(1)).min(1) });
18
+
19
+ export function loadAppHints(directory: string = appHintsDirectory): AppHint[] {
20
+ let files: string[];
21
+ try {
22
+ files = readdirSync(directory).filter((file) => file.endsWith('.md')).sort();
23
+ } catch {
24
+ return [];
25
+ }
26
+ return files.flatMap((file) => {
27
+ const { frontmatter, body } = parseFrontmatterFile(readFileSync(join(directory, file), 'utf8'));
28
+ const parsed = appsSchema.safeParse(frontmatter);
29
+ const text = body.trim();
30
+ return parsed.success && text ? [{ apps: parsed.data.apps.map((app) => app.toLowerCase()), text }] : [];
31
+ });
32
+ }
33
+
34
+ export function hintFor(hints: ReadonlyArray<AppHint>, app: { id: string; name: string }): string | undefined {
35
+ const keys = [app.id.toLowerCase(), app.name.toLowerCase()];
36
+ // "com.adobe.PremierePro.25" and "Adobe Premiere Pro 2025" are still Premiere Pro.
37
+ const matches = (entry: string) => keys.some((key) => key === entry || key.startsWith(`${entry}.`) || key.startsWith(`${entry} `));
38
+ return hints.find((hint) => hint.apps.some(matches))?.text;
39
+ }