screenhand 0.1.1 → 0.3.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 (241) hide show
  1. package/README.md +193 -109
  2. package/bin/darwin-arm64/macos-bridge +0 -0
  3. package/dist/mcp-desktop.js +5876 -0
  4. package/dist/scripts/codex-monitor-daemon.js +335 -0
  5. package/dist/scripts/export-help-center.js +112 -0
  6. package/dist/scripts/marketing-loop.js +117 -0
  7. package/dist/scripts/observer-daemon.js +288 -0
  8. package/dist/scripts/orchestrator-daemon.js +399 -0
  9. package/dist/scripts/supervisor-daemon.js +272 -0
  10. package/dist/scripts/threads-campaign.js +208 -0
  11. package/dist/scripts/worker-daemon.js +228 -0
  12. package/dist/src/agent/cli.js +82 -0
  13. package/dist/src/agent/loop.js +274 -0
  14. package/dist/src/community/fetcher.js +109 -0
  15. package/dist/src/community/index.js +6 -0
  16. package/dist/src/community/publisher.js +191 -0
  17. package/dist/src/community/remote-api.js +121 -0
  18. package/dist/src/community/types.js +3 -0
  19. package/dist/src/community/validator.js +95 -0
  20. package/{src/config.ts → dist/src/config.js} +5 -10
  21. package/dist/src/context-tracker.js +489 -0
  22. package/{src/index.ts → dist/src/index.js} +32 -52
  23. package/dist/src/ingestion/coverage-auditor.js +233 -0
  24. package/dist/src/ingestion/doc-parser.js +164 -0
  25. package/dist/src/ingestion/index.js +8 -0
  26. package/dist/src/ingestion/menu-scanner.js +152 -0
  27. package/dist/src/ingestion/reference-merger.js +186 -0
  28. package/dist/src/ingestion/shortcut-extractor.js +180 -0
  29. package/dist/src/ingestion/tutorial-extractor.js +170 -0
  30. package/dist/src/ingestion/types.js +3 -0
  31. package/dist/src/jobs/manager.js +305 -0
  32. package/dist/src/jobs/runner.js +806 -0
  33. package/dist/src/jobs/store.js +102 -0
  34. package/dist/src/jobs/types.js +30 -0
  35. package/dist/src/jobs/worker.js +97 -0
  36. package/dist/src/learning/engine.js +356 -0
  37. package/dist/src/learning/index.js +9 -0
  38. package/dist/src/learning/locator-policy.js +120 -0
  39. package/dist/src/learning/pattern-policy.js +89 -0
  40. package/dist/src/learning/recovery-policy.js +116 -0
  41. package/dist/src/learning/sensor-policy.js +115 -0
  42. package/dist/src/learning/timing-model.js +204 -0
  43. package/dist/src/learning/topology-policy.js +90 -0
  44. package/dist/src/learning/types.js +9 -0
  45. package/dist/src/logging/timeline-logger.js +48 -0
  46. package/dist/src/mcp/mcp-stdio-server.js +464 -0
  47. package/dist/src/mcp/server.js +363 -0
  48. package/dist/src/mcp-entry.js +60 -0
  49. package/dist/src/memory/playbook-seeds.js +200 -0
  50. package/dist/src/memory/recall.js +222 -0
  51. package/dist/src/memory/research.js +104 -0
  52. package/dist/src/memory/seeds.js +101 -0
  53. package/dist/src/memory/service.js +446 -0
  54. package/dist/src/memory/session.js +169 -0
  55. package/dist/src/memory/store.js +451 -0
  56. package/{src/runtime/locator-cache.ts → dist/src/memory/types.js} +1 -17
  57. package/dist/src/monitor/codex-monitor.js +382 -0
  58. package/dist/src/monitor/task-queue.js +97 -0
  59. package/dist/src/monitor/types.js +62 -0
  60. package/dist/src/native/bridge-client.js +412 -0
  61. package/{src/native/macos-bridge-client.ts → dist/src/native/macos-bridge-client.js} +0 -1
  62. package/dist/src/observer/state.js +199 -0
  63. package/dist/src/observer/types.js +43 -0
  64. package/dist/src/orchestrator/state.js +68 -0
  65. package/dist/src/orchestrator/types.js +22 -0
  66. package/dist/src/perception/ax-source.js +162 -0
  67. package/dist/src/perception/cdp-source.js +162 -0
  68. package/dist/src/perception/coordinator.js +771 -0
  69. package/dist/src/perception/frame-differ.js +287 -0
  70. package/dist/src/perception/index.js +22 -0
  71. package/dist/src/perception/manager.js +199 -0
  72. package/dist/src/perception/types.js +47 -0
  73. package/dist/src/perception/vision-source.js +399 -0
  74. package/dist/src/planner/deterministic.js +298 -0
  75. package/dist/src/planner/executor.js +870 -0
  76. package/dist/src/planner/goal-store.js +92 -0
  77. package/dist/src/planner/index.js +21 -0
  78. package/dist/src/planner/planner.js +520 -0
  79. package/dist/src/planner/tool-registry.js +71 -0
  80. package/dist/src/planner/types.js +22 -0
  81. package/dist/src/platform/explorer.js +213 -0
  82. package/dist/src/platform/help-center-markdown.js +527 -0
  83. package/dist/src/platform/learner.js +257 -0
  84. package/dist/src/playbook/engine.js +486 -0
  85. package/dist/src/playbook/index.js +20 -0
  86. package/dist/src/playbook/mcp-recorder.js +204 -0
  87. package/dist/src/playbook/recorder.js +536 -0
  88. package/dist/src/playbook/runner.js +408 -0
  89. package/dist/src/playbook/store.js +312 -0
  90. package/dist/src/playbook/types.js +17 -0
  91. package/dist/src/recovery/detectors.js +156 -0
  92. package/dist/src/recovery/engine.js +327 -0
  93. package/dist/src/recovery/index.js +20 -0
  94. package/dist/src/recovery/strategies.js +274 -0
  95. package/dist/src/recovery/types.js +20 -0
  96. package/dist/src/runtime/accessibility-adapter.js +430 -0
  97. package/dist/src/runtime/app-adapter.js +64 -0
  98. package/dist/src/runtime/applescript-adapter.js +305 -0
  99. package/dist/src/runtime/ax-role-map.js +96 -0
  100. package/dist/src/runtime/browser-adapter.js +52 -0
  101. package/dist/src/runtime/cdp-chrome-adapter.js +521 -0
  102. package/dist/src/runtime/composite-adapter.js +221 -0
  103. package/dist/src/runtime/execution-contract.js +159 -0
  104. package/dist/src/runtime/executor.js +286 -0
  105. package/dist/src/runtime/locator-cache.js +50 -0
  106. package/dist/src/runtime/planning-loop.js +63 -0
  107. package/dist/src/runtime/service.js +432 -0
  108. package/dist/src/runtime/session-manager.js +63 -0
  109. package/dist/src/runtime/state-observer.js +121 -0
  110. package/dist/src/runtime/vision-adapter.js +225 -0
  111. package/dist/src/state/app-map-types.js +72 -0
  112. package/dist/src/state/app-map.js +1974 -0
  113. package/dist/src/state/entity-tracker.js +108 -0
  114. package/dist/src/state/fusion.js +96 -0
  115. package/dist/src/state/index.js +21 -0
  116. package/dist/src/state/ladder-generator.js +236 -0
  117. package/dist/src/state/persistence.js +156 -0
  118. package/dist/src/state/types.js +17 -0
  119. package/dist/src/state/world-model.js +1456 -0
  120. package/dist/src/supervisor/locks.js +186 -0
  121. package/dist/src/supervisor/supervisor.js +403 -0
  122. package/dist/src/supervisor/types.js +30 -0
  123. package/dist/src/test-mcp-protocol.js +154 -0
  124. package/dist/src/types.js +17 -0
  125. package/dist/src/util/atomic-write.js +133 -0
  126. package/dist/src/util/sanitize.js +146 -0
  127. package/dist-app-maps/com.figma.Desktop.json +959 -0
  128. package/dist-app-maps/com.hnc.Discord.json +1146 -0
  129. package/dist-app-maps/notion.id.json +2831 -0
  130. package/dist-playbooks/canva-screenhand-carousel.json +445 -0
  131. package/dist-playbooks/codex-desktop.json +76 -0
  132. package/dist-playbooks/competitor-research-stack.json +122 -0
  133. package/dist-playbooks/davinci-color-grade.json +153 -0
  134. package/dist-playbooks/davinci-edit-timeline.json +162 -0
  135. package/dist-playbooks/davinci-render.json +114 -0
  136. package/dist-playbooks/devto.json +52 -0
  137. package/dist-playbooks/discord.json +41 -0
  138. package/dist-playbooks/google-flow-create-project.json +59 -0
  139. package/dist-playbooks/google-flow-edit-image.json +90 -0
  140. package/dist-playbooks/google-flow-edit-video.json +90 -0
  141. package/dist-playbooks/google-flow-generate-image.json +68 -0
  142. package/dist-playbooks/google-flow-generate-video.json +191 -0
  143. package/dist-playbooks/google-flow-open-project.json +48 -0
  144. package/dist-playbooks/google-flow-open-scenebuilder.json +64 -0
  145. package/dist-playbooks/google-flow-search-assets.json +64 -0
  146. package/dist-playbooks/instagram.json +57 -0
  147. package/dist-playbooks/linkedin.json +52 -0
  148. package/dist-playbooks/n8n.json +43 -0
  149. package/dist-playbooks/reddit.json +52 -0
  150. package/dist-playbooks/threads.json +59 -0
  151. package/dist-playbooks/x-twitter.json +59 -0
  152. package/dist-playbooks/youtube.json +59 -0
  153. package/dist-references/canva.json +646 -0
  154. package/dist-references/codex-desktop.json +305 -0
  155. package/dist-references/davinci-resolve-keyboard.json +594 -0
  156. package/dist-references/davinci-resolve-menu-map.json +1139 -0
  157. package/dist-references/davinci-resolve-menus-batch1.json +116 -0
  158. package/dist-references/davinci-resolve-menus-batch2.json +372 -0
  159. package/dist-references/davinci-resolve-menus-batch3.json +330 -0
  160. package/dist-references/davinci-resolve-menus-batch4.json +297 -0
  161. package/dist-references/davinci-resolve-shortcuts.json +333 -0
  162. package/dist-references/devto.json +317 -0
  163. package/dist-references/discord.json +549 -0
  164. package/dist-references/figma.json +1186 -0
  165. package/dist-references/finder.json +146 -0
  166. package/dist-references/google-ads-transparency.json +95 -0
  167. package/dist-references/google-flow.json +649 -0
  168. package/dist-references/instagram.json +341 -0
  169. package/dist-references/linkedin.json +324 -0
  170. package/dist-references/meta-ad-library.json +86 -0
  171. package/dist-references/n8n.json +387 -0
  172. package/dist-references/notes.json +27 -0
  173. package/dist-references/notion.json +163 -0
  174. package/dist-references/reddit.json +341 -0
  175. package/dist-references/threads.json +337 -0
  176. package/dist-references/x-twitter.json +403 -0
  177. package/dist-references/youtube.json +373 -0
  178. package/native/macos-bridge/Package.swift +1 -0
  179. package/native/macos-bridge/Sources/AccessibilityBridge.swift +257 -36
  180. package/native/macos-bridge/Sources/AppManagement.swift +212 -2
  181. package/native/macos-bridge/Sources/CoreGraphicsBridge.swift +348 -53
  182. package/native/macos-bridge/Sources/StreamCapture.swift +136 -0
  183. package/native/macos-bridge/Sources/VisionBridge.swift +165 -7
  184. package/native/macos-bridge/Sources/main.swift +169 -16
  185. package/native/windows-bridge/Program.cs +5 -0
  186. package/native/windows-bridge/ScreenCapture.cs +124 -0
  187. package/package.json +29 -4
  188. package/scripts/postinstall.cjs +127 -0
  189. package/.claude/commands/automate.md +0 -28
  190. package/.claude/commands/debug-ui.md +0 -19
  191. package/.claude/commands/screenshot.md +0 -15
  192. package/.github/FUNDING.yml +0 -1
  193. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -27
  194. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
  195. package/.mcp.json +0 -8
  196. package/DESKTOP_MCP_GUIDE.md +0 -92
  197. package/SECURITY.md +0 -44
  198. package/docs/architecture.md +0 -47
  199. package/install-skills.sh +0 -19
  200. package/mcp-bridge.ts +0 -271
  201. package/mcp-desktop.ts +0 -1221
  202. package/playbooks/instagram.json +0 -41
  203. package/playbooks/instagram_v2.json +0 -201
  204. package/playbooks/x_v1.json +0 -211
  205. package/scripts/devpost-live-loop.mjs +0 -421
  206. package/src/logging/timeline-logger.ts +0 -55
  207. package/src/mcp/server.ts +0 -449
  208. package/src/memory/recall.ts +0 -191
  209. package/src/memory/research.ts +0 -146
  210. package/src/memory/seeds.ts +0 -123
  211. package/src/memory/session.ts +0 -201
  212. package/src/memory/store.ts +0 -434
  213. package/src/memory/types.ts +0 -69
  214. package/src/native/bridge-client.ts +0 -239
  215. package/src/runtime/accessibility-adapter.ts +0 -487
  216. package/src/runtime/app-adapter.ts +0 -169
  217. package/src/runtime/applescript-adapter.ts +0 -376
  218. package/src/runtime/ax-role-map.ts +0 -102
  219. package/src/runtime/browser-adapter.ts +0 -129
  220. package/src/runtime/cdp-chrome-adapter.ts +0 -676
  221. package/src/runtime/composite-adapter.ts +0 -274
  222. package/src/runtime/executor.ts +0 -396
  223. package/src/runtime/planning-loop.ts +0 -81
  224. package/src/runtime/service.ts +0 -448
  225. package/src/runtime/session-manager.ts +0 -50
  226. package/src/runtime/state-observer.ts +0 -136
  227. package/src/runtime/vision-adapter.ts +0 -297
  228. package/src/types.ts +0 -297
  229. package/tests/bridge-client.test.ts +0 -176
  230. package/tests/browser-stealth.test.ts +0 -210
  231. package/tests/composite-adapter.test.ts +0 -64
  232. package/tests/mcp-server.test.ts +0 -151
  233. package/tests/memory-recall.test.ts +0 -339
  234. package/tests/memory-research.test.ts +0 -159
  235. package/tests/memory-seeds.test.ts +0 -120
  236. package/tests/memory-store.test.ts +0 -392
  237. package/tests/types.test.ts +0 -92
  238. package/tsconfig.check.json +0 -17
  239. package/tsconfig.json +0 -19
  240. package/vitest.config.ts +0 -8
  241. /package/{playbooks → dist-references}/devpost.json +0 -0
@@ -1,64 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
-
3
- // ── Routing logic tests ──
4
- // We test the routing constants and logic without instantiating the full adapter
5
- // (which requires a real bridge subprocess).
6
-
7
- describe("CompositeAdapter routing constants", () => {
8
- it("BROWSER_BUNDLES contains major Chromium browsers (macOS)", async () => {
9
- // Import the module to verify the constants are defined correctly
10
- const mod = await import("../src/runtime/composite-adapter.js");
11
- // The constants are module-level, not exported — so we test the behavior
12
- // by checking that the module loads without error
13
- expect(mod.CompositeAdapter).toBeDefined();
14
- });
15
-
16
- it("correctly identifies browser process names for Windows routing", () => {
17
- // These are the process names that should route to CDP on Windows
18
- const browserProcessNames = new Set([
19
- "chrome", "chrome.exe",
20
- "brave", "brave.exe",
21
- "msedge", "msedge.exe",
22
- "vivaldi", "vivaldi.exe",
23
- "chromium", "chromium.exe",
24
- ]);
25
-
26
- // Positive cases
27
- expect(browserProcessNames.has("chrome")).toBe(true);
28
- expect(browserProcessNames.has("chrome.exe")).toBe(true);
29
- expect(browserProcessNames.has("msedge")).toBe(true);
30
- expect(browserProcessNames.has("brave.exe")).toBe(true);
31
-
32
- // Negative cases — these should NOT route to CDP
33
- expect(browserProcessNames.has("notepad")).toBe(false);
34
- expect(browserProcessNames.has("firefox")).toBe(false);
35
- expect(browserProcessNames.has("explorer")).toBe(false);
36
- expect(browserProcessNames.has("safari")).toBe(false);
37
- });
38
-
39
- it("correctly identifies browser bundle IDs for macOS routing", () => {
40
- const browserBundles = new Set([
41
- "com.google.Chrome",
42
- "com.google.Chrome.canary",
43
- "com.brave.Browser",
44
- "com.microsoft.edgemac",
45
- "com.vivaldi.Vivaldi",
46
- "org.chromium.Chromium",
47
- ]);
48
-
49
- expect(browserBundles.has("com.google.Chrome")).toBe(true);
50
- expect(browserBundles.has("com.apple.Safari")).toBe(false);
51
- expect(browserBundles.has("com.apple.Notes")).toBe(false);
52
- });
53
- });
54
-
55
- describe("Platform-aware routing", () => {
56
- it("isWindows flag matches process.platform", () => {
57
- const isWindows = process.platform === "win32";
58
-
59
- if (process.platform === "darwin") {
60
- expect(isWindows).toBe(false);
61
- }
62
- // On Windows CI this would be true
63
- });
64
- });
@@ -1,151 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { spawn } from "node:child_process";
3
- import { createInterface } from "node:readline";
4
- import path from "node:path";
5
-
6
- const MCP_DESKTOP_PATH = path.resolve(
7
- import.meta.dirname ?? process.cwd(),
8
- "../mcp-desktop.ts",
9
- );
10
-
11
- /**
12
- * Spawn the MCP server and send a JSON-RPC initialize + tool list request.
13
- * Validates that the server starts and responds with expected tools.
14
- */
15
- function spawnMcpServer(): Promise<{
16
- responses: any[];
17
- exitCode: number | null;
18
- }> {
19
- return new Promise((resolve, reject) => {
20
- const child = spawn("npx", ["tsx", MCP_DESKTOP_PATH], {
21
- stdio: ["pipe", "pipe", "pipe"],
22
- env: { ...process.env },
23
- });
24
-
25
- const responses: any[] = [];
26
- const rl = createInterface({ input: child.stdout! });
27
-
28
- rl.on("line", (line) => {
29
- try {
30
- responses.push(JSON.parse(line));
31
- } catch {
32
- // Ignore non-JSON lines
33
- }
34
- });
35
-
36
- // Send MCP initialize request
37
- const initRequest = {
38
- jsonrpc: "2.0",
39
- id: 1,
40
- method: "initialize",
41
- params: {
42
- protocolVersion: "2024-11-05",
43
- capabilities: {},
44
- clientInfo: { name: "test-client", version: "1.0.0" },
45
- },
46
- };
47
-
48
- child.stdin!.write(JSON.stringify(initRequest) + "\n");
49
-
50
- // After a brief wait, send tools/list request
51
- setTimeout(() => {
52
- const toolsRequest = {
53
- jsonrpc: "2.0",
54
- id: 2,
55
- method: "tools/list",
56
- params: {},
57
- };
58
- child.stdin!.write(JSON.stringify(toolsRequest) + "\n");
59
- }, 500);
60
-
61
- // Give it time to respond, then kill
62
- setTimeout(() => {
63
- child.kill();
64
- }, 3000);
65
-
66
- child.on("exit", (code) => {
67
- resolve({ responses, exitCode: code });
68
- });
69
-
70
- child.on("error", reject);
71
- });
72
- }
73
-
74
- describe("MCP server startup", () => {
75
- it("starts and responds to initialize + tools/list", async () => {
76
- const { responses } = await spawnMcpServer();
77
-
78
- // Should have at least 2 responses (initialize + tools/list)
79
- expect(responses.length).toBeGreaterThanOrEqual(2);
80
-
81
- // First response: initialize
82
- const initResponse = responses.find((r) => r.id === 1);
83
- expect(initResponse).toBeDefined();
84
- expect(initResponse.result).toBeDefined();
85
- expect(initResponse.result.serverInfo.name).toBe("screenhand");
86
-
87
- // Second response: tools/list
88
- const toolsResponse = responses.find((r) => r.id === 2);
89
- expect(toolsResponse).toBeDefined();
90
- expect(toolsResponse.result).toBeDefined();
91
- expect(toolsResponse.result.tools).toBeInstanceOf(Array);
92
-
93
- // Check key tools exist
94
- const toolNames = toolsResponse.result.tools.map((t: any) => t.name);
95
- expect(toolNames).toContain("apps");
96
- expect(toolNames).toContain("windows");
97
- expect(toolNames).toContain("screenshot");
98
- expect(toolNames).toContain("ui_tree");
99
- expect(toolNames).toContain("ui_press");
100
- expect(toolNames).toContain("click");
101
- expect(toolNames).toContain("type_text");
102
- expect(toolNames).toContain("key");
103
- expect(toolNames).toContain("browser_tabs");
104
- expect(toolNames).toContain("applescript");
105
-
106
- // Verify tool count is reasonable (25+ tools)
107
- expect(toolNames.length).toBeGreaterThanOrEqual(20);
108
- });
109
-
110
- it("exposes all expected tool categories", async () => {
111
- const { responses } = await spawnMcpServer();
112
- const toolsResponse = responses.find((r) => r.id === 2);
113
- const toolNames: string[] = toolsResponse?.result?.tools?.map((t: any) => t.name) ?? [];
114
-
115
- // App management
116
- expect(toolNames).toContain("apps");
117
- expect(toolNames).toContain("focus");
118
- expect(toolNames).toContain("launch");
119
-
120
- // Screen/OCR
121
- expect(toolNames).toContain("screenshot");
122
- expect(toolNames).toContain("screenshot_file");
123
- expect(toolNames).toContain("ocr");
124
-
125
- // Accessibility
126
- expect(toolNames).toContain("ui_tree");
127
- expect(toolNames).toContain("ui_find");
128
- expect(toolNames).toContain("ui_press");
129
- expect(toolNames).toContain("ui_set_value");
130
- expect(toolNames).toContain("menu_click");
131
-
132
- // Input
133
- expect(toolNames).toContain("click");
134
- expect(toolNames).toContain("click_text");
135
- expect(toolNames).toContain("type_text");
136
- expect(toolNames).toContain("key");
137
- expect(toolNames).toContain("drag");
138
- expect(toolNames).toContain("scroll");
139
-
140
- // Browser/CDP
141
- expect(toolNames).toContain("browser_tabs");
142
- expect(toolNames).toContain("browser_open");
143
- expect(toolNames).toContain("browser_navigate");
144
- expect(toolNames).toContain("browser_js");
145
- expect(toolNames).toContain("browser_dom");
146
- expect(toolNames).toContain("browser_click");
147
- expect(toolNames).toContain("browser_type");
148
- expect(toolNames).toContain("browser_wait");
149
- expect(toolNames).toContain("browser_page_info");
150
- });
151
- });
@@ -1,339 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
- import fs from "node:fs";
3
- import path from "node:path";
4
- import os from "node:os";
5
- import { MemoryStore } from "../src/memory/store.js";
6
- import { RecallEngine } from "../src/memory/recall.js";
7
- import { SessionTracker } from "../src/memory/session.js";
8
-
9
- let tmpDir: string;
10
- let store: MemoryStore;
11
- let recall: RecallEngine;
12
-
13
- beforeEach(() => {
14
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "screenhand-recall-"));
15
- // Pre-create memory dir so seeds don't auto-load (not first boot)
16
- fs.mkdirSync(path.join(tmpDir, ".screenhand", "memory"), { recursive: true });
17
- store = new MemoryStore(tmpDir);
18
- store.init();
19
- recall = new RecallEngine(store);
20
- });
21
-
22
- afterEach(() => {
23
- // Remove lock file first, then clean up
24
- const lockPath = path.join(tmpDir, ".screenhand", "memory", ".lock");
25
- try { fs.unlinkSync(lockPath); } catch { /* ignore */ }
26
- fs.rmSync(tmpDir, { recursive: true, force: true });
27
- });
28
-
29
- function addStrategy(task: string, tools: string[], tags: string[] = [], overrides: Partial<{ successCount: number; failCount: number }> = {}): void {
30
- store.appendStrategy({
31
- id: "str_" + Math.random().toString(36).slice(2, 8),
32
- task,
33
- steps: tools.map((t) => ({ tool: t, params: {} })),
34
- totalDurationMs: 100,
35
- successCount: overrides.successCount ?? 1,
36
- failCount: overrides.failCount ?? 0,
37
- lastUsed: new Date().toISOString(),
38
- tags: tags.length > 0 ? tags : task.toLowerCase().split(/\W+/).filter((w) => w.length >= 3),
39
- fingerprint: tools.join("→"),
40
- });
41
- }
42
-
43
- function addError(tool: string, error: string, resolution: string | null = null): void {
44
- store.appendError({
45
- id: "err_" + Math.random().toString(36).slice(2, 8),
46
- tool,
47
- params: {},
48
- error,
49
- resolution,
50
- occurrences: 1,
51
- lastSeen: new Date().toISOString(),
52
- });
53
- }
54
-
55
- describe("RecallEngine", () => {
56
- describe("recallStrategies", () => {
57
- it("returns empty when no strategies exist", () => {
58
- expect(recall.recallStrategies("take a photo")).toEqual([]);
59
- });
60
-
61
- it("finds strategies by keyword match", () => {
62
- addStrategy("take a photo with Photo Booth", ["apps", "focus", "ui_press"], ["photo", "booth", "camera"]);
63
- addStrategy("open Chrome and navigate", ["launch", "browser_navigate"], ["chrome", "browser"]);
64
-
65
- const results = recall.recallStrategies("photo");
66
- expect(results.length).toBeGreaterThan(0);
67
- expect(results[0]!.task).toContain("photo");
68
- });
69
-
70
- it("ranks by relevance — more keyword matches = higher score", () => {
71
- addStrategy("take a photo with Photo Booth", ["apps", "focus", "ui_press"], ["photo", "booth", "camera"]);
72
- addStrategy("open Photos app", ["launch"], ["photos", "app"]);
73
-
74
- const results = recall.recallStrategies("take photo booth");
75
- expect(results[0]!.task).toContain("Photo Booth");
76
- });
77
-
78
- it("respects limit parameter", () => {
79
- addStrategy("task one", ["apps"]);
80
- addStrategy("task two", ["apps"]);
81
- addStrategy("task three", ["apps"]);
82
-
83
- const results = recall.recallStrategies("task", 2);
84
- expect(results).toHaveLength(2);
85
- });
86
- });
87
-
88
- describe("recallByFingerprint", () => {
89
- it("returns null when no match", () => {
90
- expect(recall.recallByFingerprint(["apps", "focus"])).toBeNull();
91
- });
92
-
93
- it("returns exact match by tool sequence", () => {
94
- addStrategy("photo workflow", ["apps", "focus", "ui_press"]);
95
- const result = recall.recallByFingerprint(["apps", "focus", "ui_press"]);
96
- expect(result).not.toBeNull();
97
- expect(result!.task).toBe("photo workflow");
98
- });
99
-
100
- it("skips strategies that fail more than succeed", () => {
101
- addStrategy("unreliable", ["apps", "focus"], [], { successCount: 2, failCount: 5 });
102
- expect(recall.recallByFingerprint(["apps", "focus"])).toBeNull();
103
- });
104
-
105
- it("returns strategy when failures are within tolerance", () => {
106
- addStrategy("mostly works", ["apps", "focus"], [], { successCount: 5, failCount: 3 });
107
- const result = recall.recallByFingerprint(["apps", "focus"]);
108
- expect(result).not.toBeNull();
109
- });
110
- });
111
-
112
- describe("reliability penalty in recallStrategies", () => {
113
- it("penalizes strategies with high fail rates", () => {
114
- addStrategy("reliable photo", ["apps", "focus", "ui_press"], ["photo"], { successCount: 10, failCount: 1 });
115
- addStrategy("unreliable photo", ["launch", "ui_press"], ["photo"], { successCount: 2, failCount: 8 });
116
-
117
- const results = recall.recallStrategies("photo");
118
- expect(results.length).toBe(2);
119
- // Reliable strategy should rank higher
120
- expect(results[0]!.task).toBe("reliable photo");
121
- });
122
- });
123
-
124
- describe("quickErrorCheck", () => {
125
- it("returns null when no errors", () => {
126
- expect(recall.quickErrorCheck("launch")).toBeNull();
127
- });
128
-
129
- it("returns null when no resolution exists", () => {
130
- addError("launch", "timed out", null);
131
- expect(recall.quickErrorCheck("launch")).toBeNull();
132
- });
133
-
134
- it("returns error with resolution", () => {
135
- addError("launch", "timed out", "use focus() instead");
136
- const result = recall.quickErrorCheck("launch");
137
- expect(result).not.toBeNull();
138
- expect(result!.resolution).toBe("use focus() instead");
139
- });
140
-
141
- it("returns highest-occurrence error", () => {
142
- addError("launch", "error A", "fix A");
143
- // Add same error again to bump occurrences
144
- store.appendError({
145
- id: "err_bump", tool: "launch", params: {},
146
- error: "error A", resolution: "fix A",
147
- occurrences: 1, lastSeen: new Date().toISOString(),
148
- });
149
- addError("launch", "error B", "fix B");
150
-
151
- const result = recall.quickErrorCheck("launch");
152
- expect(result!.error).toBe("error A");
153
- expect(result!.occurrences).toBe(2);
154
- });
155
- });
156
-
157
- describe("quickStrategyHint", () => {
158
- it("returns null when no strategies", () => {
159
- expect(recall.quickStrategyHint(["apps"])).toBeNull();
160
- });
161
-
162
- it("suggests next step when mid-strategy", () => {
163
- addStrategy("photo workflow", ["apps", "focus", "ui_press"]);
164
-
165
- const hint = recall.quickStrategyHint(["apps", "focus"]);
166
- expect(hint).not.toBeNull();
167
- expect(hint!.nextStep.tool).toBe("ui_press");
168
- expect(hint!.fingerprint).toBe("apps→focus→ui_press");
169
- });
170
-
171
- it("skips unreliable strategies", () => {
172
- addStrategy("bad workflow", ["apps", "focus", "ui_press"], [], { successCount: 1, failCount: 5 });
173
- expect(recall.quickStrategyHint(["apps", "focus"])).toBeNull();
174
- });
175
-
176
- it("returns null when sequence doesn't match", () => {
177
- addStrategy("photo workflow", ["apps", "focus", "ui_press"]);
178
- expect(recall.quickStrategyHint(["launch", "focus"])).toBeNull();
179
- });
180
-
181
- it("returns null when strategy is already complete", () => {
182
- addStrategy("photo workflow", ["apps", "focus"]);
183
- expect(recall.quickStrategyHint(["apps", "focus"])).toBeNull();
184
- });
185
- });
186
-
187
- describe("recallErrors", () => {
188
- it("returns all errors when no tool filter", () => {
189
- addError("launch", "timed out", "use focus() instead");
190
- addError("ui_press", "element not found");
191
-
192
- const results = recall.recallErrors();
193
- expect(results).toHaveLength(2);
194
- });
195
-
196
- it("filters by tool name", () => {
197
- addError("launch", "timed out");
198
- addError("ui_press", "element not found");
199
-
200
- const results = recall.recallErrors("launch");
201
- expect(results).toHaveLength(1);
202
- expect(results[0]!.tool).toBe("launch");
203
- });
204
- });
205
- });
206
-
207
- describe("SessionTracker", () => {
208
- it("starts a session and returns an ID", () => {
209
- const tracker = new SessionTracker(store);
210
- const id = tracker.startSession("test task");
211
- expect(id).toMatch(/^s_/);
212
- });
213
-
214
- it("saves a strategy on successful endSession", () => {
215
- const tracker = new SessionTracker(store);
216
- tracker.startSession("take a photo");
217
-
218
- tracker.recordAction({
219
- id: "a_1", timestamp: new Date().toISOString(), sessionId: "s_test",
220
- tool: "focus", params: { bundleId: "com.apple.PhotoBooth" },
221
- durationMs: 30, success: true, result: "Focused", error: null,
222
- });
223
- tracker.recordAction({
224
- id: "a_2", timestamp: new Date().toISOString(), sessionId: "s_test",
225
- tool: "ui_press", params: { pid: 1234, title: "Take Photo" },
226
- durationMs: 50, success: true, result: "Pressed", error: null,
227
- });
228
-
229
- const strategy = tracker.endSession(true);
230
- expect(strategy).not.toBeNull();
231
- expect(strategy!.task).toBe("take a photo");
232
- expect(strategy!.steps).toHaveLength(2);
233
-
234
- expect(store.readStrategies()).toHaveLength(1);
235
- });
236
-
237
- it("returns null on failed endSession", () => {
238
- const tracker = new SessionTracker(store);
239
- tracker.startSession("test");
240
- tracker.recordAction({
241
- id: "a_1", timestamp: new Date().toISOString(), sessionId: "s_test",
242
- tool: "apps", params: {}, durationMs: 10, success: true, result: "ok", error: null,
243
- });
244
-
245
- expect(tracker.endSession(false)).toBeNull();
246
- expect(store.readStrategies()).toHaveLength(0);
247
- });
248
-
249
- it("allows passing task description at endSession", () => {
250
- const tracker = new SessionTracker(store);
251
- tracker.startSession();
252
- tracker.recordAction({
253
- id: "a_1", timestamp: new Date().toISOString(), sessionId: "s_test",
254
- tool: "apps", params: {}, durationMs: 10, success: true, result: "ok", error: null,
255
- });
256
-
257
- const result = tracker.endSession(true, "list running apps");
258
- expect(result).not.toBeNull();
259
- expect(result!.task).toBe("list running apps");
260
- });
261
-
262
- it("provides recent tool names for strategy matching", () => {
263
- const tracker = new SessionTracker(store);
264
- tracker.startSession("test");
265
-
266
- tracker.recordAction({
267
- id: "a_1", timestamp: new Date().toISOString(), sessionId: "s_test",
268
- tool: "apps", params: {}, durationMs: 10, success: true, result: "ok", error: null,
269
- });
270
- tracker.recordAction({
271
- id: "a_2", timestamp: new Date().toISOString(), sessionId: "s_test",
272
- tool: "focus", params: {}, durationMs: 10, success: true, result: "ok", error: null,
273
- });
274
-
275
- expect(tracker.getRecentToolNames()).toEqual(["apps", "focus"]);
276
- });
277
-
278
- it("auto-saves strategy when starting a new session after 3+ successes", () => {
279
- const tracker = new SessionTracker(store);
280
- tracker.startSession("first task");
281
-
282
- // Record 3 successful actions
283
- for (let i = 0; i < 3; i++) {
284
- tracker.recordAction({
285
- id: `a_${i}`, timestamp: new Date().toISOString(), sessionId: "s_test",
286
- tool: ["apps", "focus", "ui_press"][i]!, params: {},
287
- durationMs: 10, success: true, result: "ok", error: null,
288
- });
289
- }
290
-
291
- // Starting a new session triggers auto-save of the previous one
292
- tracker.startSession("second task");
293
-
294
- const strategies = store.readStrategies();
295
- expect(strategies).toHaveLength(1);
296
- expect(strategies[0]!.task).toBe("first task");
297
- expect(strategies[0]!.steps).toHaveLength(3);
298
- });
299
-
300
- it("does NOT auto-save when fewer than 3 successful actions", () => {
301
- const tracker = new SessionTracker(store);
302
- tracker.startSession("short task");
303
-
304
- tracker.recordAction({
305
- id: "a_1", timestamp: new Date().toISOString(), sessionId: "s_test",
306
- tool: "apps", params: {}, durationMs: 10, success: true, result: "ok", error: null,
307
- });
308
- tracker.recordAction({
309
- id: "a_2", timestamp: new Date().toISOString(), sessionId: "s_test",
310
- tool: "focus", params: {}, durationMs: 10, success: true, result: "ok", error: null,
311
- });
312
-
313
- tracker.startSession("next task");
314
- expect(store.readStrategies()).toHaveLength(0);
315
- });
316
-
317
- it("auto-infers task description from tool sequence when no explicit description", () => {
318
- const tracker = new SessionTracker(store);
319
- tracker.startSession(); // no description
320
-
321
- for (let i = 0; i < 3; i++) {
322
- tracker.recordAction({
323
- id: `a_${i}`, timestamp: new Date().toISOString(), sessionId: "s_test",
324
- tool: ["apps", "focus", "ui_press"][i]!,
325
- params: i === 1 ? { bundleId: "com.apple.Safari" } : {},
326
- durationMs: 10, success: true, result: "ok", error: null,
327
- });
328
- }
329
-
330
- tracker.startSession("new session");
331
-
332
- const strategies = store.readStrategies();
333
- expect(strategies).toHaveLength(1);
334
- // Should contain tool names and key params
335
- expect(strategies[0]!.task).toContain("apps");
336
- expect(strategies[0]!.task).toContain("focus");
337
- expect(strategies[0]!.task).toContain("Safari");
338
- });
339
- });