screenhand 0.1.1 → 0.2.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 (177) hide show
  1. package/README.md +458 -93
  2. package/dist/.audit-log.jsonl +55 -0
  3. package/dist/.screenhand/memory/.lock +1 -0
  4. package/dist/.screenhand/memory/actions.jsonl +85 -0
  5. package/dist/.screenhand/memory/errors.jsonl +5 -0
  6. package/dist/.screenhand/memory/errors.jsonl.bak +4 -0
  7. package/dist/.screenhand/memory/state.json +35 -0
  8. package/dist/.screenhand/memory/state.json.bak +35 -0
  9. package/dist/.screenhand/memory/strategies.jsonl +12 -0
  10. package/dist/agent/cli.js +73 -0
  11. package/dist/agent/loop.js +258 -0
  12. package/dist/config.js +9 -0
  13. package/dist/index.js +56 -0
  14. package/dist/logging/timeline-logger.js +29 -0
  15. package/dist/mcp/mcp-stdio-server.js +448 -0
  16. package/dist/mcp/server.js +347 -0
  17. package/dist/mcp-desktop.js +2731 -0
  18. package/dist/mcp-entry.js +59 -0
  19. package/dist/memory/recall.js +160 -0
  20. package/dist/memory/research.js +98 -0
  21. package/dist/memory/seeds.js +89 -0
  22. package/dist/memory/session.js +161 -0
  23. package/dist/memory/store.js +391 -0
  24. package/dist/memory/types.js +4 -0
  25. package/dist/monitor/codex-monitor.js +377 -0
  26. package/dist/monitor/task-queue.js +84 -0
  27. package/dist/monitor/types.js +49 -0
  28. package/dist/native/bridge-client.js +174 -0
  29. package/dist/native/macos-bridge-client.js +5 -0
  30. package/dist/npm-publish-helper.js +117 -0
  31. package/dist/npm-token-cdp.js +113 -0
  32. package/dist/npm-token-create.js +135 -0
  33. package/dist/npm-token-finish.js +126 -0
  34. package/dist/playbook/engine.js +193 -0
  35. package/dist/playbook/index.js +4 -0
  36. package/dist/playbook/recorder.js +519 -0
  37. package/dist/playbook/runner.js +392 -0
  38. package/dist/playbook/store.js +166 -0
  39. package/dist/playbook/types.js +4 -0
  40. package/dist/runtime/accessibility-adapter.js +377 -0
  41. package/dist/runtime/app-adapter.js +48 -0
  42. package/dist/runtime/applescript-adapter.js +283 -0
  43. package/dist/runtime/ax-role-map.js +80 -0
  44. package/dist/runtime/browser-adapter.js +36 -0
  45. package/dist/runtime/cdp-chrome-adapter.js +505 -0
  46. package/dist/runtime/composite-adapter.js +205 -0
  47. package/dist/runtime/executor.js +250 -0
  48. package/dist/runtime/locator-cache.js +12 -0
  49. package/dist/runtime/planning-loop.js +47 -0
  50. package/dist/runtime/service.js +372 -0
  51. package/dist/runtime/session-manager.js +28 -0
  52. package/dist/runtime/state-observer.js +105 -0
  53. package/dist/runtime/vision-adapter.js +208 -0
  54. package/dist/scripts/codex-monitor-daemon.js +335 -0
  55. package/dist/scripts/supervisor-daemon.js +272 -0
  56. package/dist/scripts/worker-daemon.js +228 -0
  57. package/dist/src/agent/cli.js +82 -0
  58. package/dist/src/agent/loop.js +274 -0
  59. package/{src/config.ts → dist/src/config.js} +5 -10
  60. package/{src/index.ts → dist/src/index.js} +32 -52
  61. package/dist/src/jobs/manager.js +237 -0
  62. package/dist/src/jobs/runner.js +683 -0
  63. package/dist/src/jobs/store.js +102 -0
  64. package/dist/src/jobs/types.js +30 -0
  65. package/dist/src/jobs/worker.js +97 -0
  66. package/dist/src/logging/timeline-logger.js +45 -0
  67. package/dist/src/mcp/mcp-stdio-server.js +464 -0
  68. package/dist/src/mcp/server.js +363 -0
  69. package/dist/src/mcp-entry.js +60 -0
  70. package/dist/src/memory/recall.js +170 -0
  71. package/dist/src/memory/research.js +104 -0
  72. package/dist/src/memory/seeds.js +101 -0
  73. package/dist/src/memory/service.js +421 -0
  74. package/dist/src/memory/session.js +169 -0
  75. package/dist/src/memory/store.js +422 -0
  76. package/dist/src/memory/types.js +17 -0
  77. package/dist/src/monitor/codex-monitor.js +382 -0
  78. package/dist/src/monitor/task-queue.js +97 -0
  79. package/dist/src/monitor/types.js +62 -0
  80. package/dist/src/native/bridge-client.js +190 -0
  81. package/{src/native/macos-bridge-client.ts → dist/src/native/macos-bridge-client.js} +0 -1
  82. package/dist/src/playbook/engine.js +201 -0
  83. package/dist/src/playbook/index.js +20 -0
  84. package/dist/src/playbook/recorder.js +535 -0
  85. package/dist/src/playbook/runner.js +408 -0
  86. package/dist/src/playbook/store.js +183 -0
  87. package/dist/src/playbook/types.js +17 -0
  88. package/dist/src/runtime/accessibility-adapter.js +393 -0
  89. package/dist/src/runtime/app-adapter.js +64 -0
  90. package/dist/src/runtime/applescript-adapter.js +299 -0
  91. package/dist/src/runtime/ax-role-map.js +96 -0
  92. package/dist/src/runtime/browser-adapter.js +52 -0
  93. package/dist/src/runtime/cdp-chrome-adapter.js +521 -0
  94. package/dist/src/runtime/composite-adapter.js +221 -0
  95. package/dist/src/runtime/execution-contract.js +159 -0
  96. package/dist/src/runtime/executor.js +266 -0
  97. package/{src/runtime/locator-cache.ts → dist/src/runtime/locator-cache.js} +10 -15
  98. package/dist/src/runtime/planning-loop.js +63 -0
  99. package/dist/src/runtime/service.js +388 -0
  100. package/dist/src/runtime/session-manager.js +60 -0
  101. package/dist/src/runtime/state-observer.js +121 -0
  102. package/dist/src/runtime/vision-adapter.js +224 -0
  103. package/dist/src/supervisor/locks.js +186 -0
  104. package/dist/src/supervisor/supervisor.js +403 -0
  105. package/dist/src/supervisor/types.js +30 -0
  106. package/dist/src/test-mcp-protocol.js +154 -0
  107. package/dist/src/types.js +17 -0
  108. package/dist/src/util/atomic-write.js +118 -0
  109. package/dist/test-mcp-protocol.js +138 -0
  110. package/dist/types.js +1 -0
  111. package/package.json +18 -4
  112. package/.claude/commands/automate.md +0 -28
  113. package/.claude/commands/debug-ui.md +0 -19
  114. package/.claude/commands/screenshot.md +0 -15
  115. package/.github/FUNDING.yml +0 -1
  116. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -27
  117. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
  118. package/.mcp.json +0 -8
  119. package/DESKTOP_MCP_GUIDE.md +0 -92
  120. package/SECURITY.md +0 -44
  121. package/docs/architecture.md +0 -47
  122. package/install-skills.sh +0 -19
  123. package/mcp-bridge.ts +0 -271
  124. package/mcp-desktop.ts +0 -1221
  125. package/native/macos-bridge/Package.swift +0 -21
  126. package/native/macos-bridge/Sources/AccessibilityBridge.swift +0 -261
  127. package/native/macos-bridge/Sources/AppManagement.swift +0 -129
  128. package/native/macos-bridge/Sources/CoreGraphicsBridge.swift +0 -242
  129. package/native/macos-bridge/Sources/ObserverBridge.swift +0 -120
  130. package/native/macos-bridge/Sources/VisionBridge.swift +0 -80
  131. package/native/macos-bridge/Sources/main.swift +0 -345
  132. package/native/windows-bridge/AppManagement.cs +0 -234
  133. package/native/windows-bridge/InputBridge.cs +0 -436
  134. package/native/windows-bridge/Program.cs +0 -265
  135. package/native/windows-bridge/ScreenCapture.cs +0 -329
  136. package/native/windows-bridge/UIAutomationBridge.cs +0 -571
  137. package/native/windows-bridge/WindowsBridge.csproj +0 -17
  138. package/playbooks/devpost.json +0 -186
  139. package/playbooks/instagram.json +0 -41
  140. package/playbooks/instagram_v2.json +0 -201
  141. package/playbooks/x_v1.json +0 -211
  142. package/scripts/devpost-live-loop.mjs +0 -421
  143. package/src/logging/timeline-logger.ts +0 -55
  144. package/src/mcp/server.ts +0 -449
  145. package/src/memory/recall.ts +0 -191
  146. package/src/memory/research.ts +0 -146
  147. package/src/memory/seeds.ts +0 -123
  148. package/src/memory/session.ts +0 -201
  149. package/src/memory/store.ts +0 -434
  150. package/src/memory/types.ts +0 -69
  151. package/src/native/bridge-client.ts +0 -239
  152. package/src/runtime/accessibility-adapter.ts +0 -487
  153. package/src/runtime/app-adapter.ts +0 -169
  154. package/src/runtime/applescript-adapter.ts +0 -376
  155. package/src/runtime/ax-role-map.ts +0 -102
  156. package/src/runtime/browser-adapter.ts +0 -129
  157. package/src/runtime/cdp-chrome-adapter.ts +0 -676
  158. package/src/runtime/composite-adapter.ts +0 -274
  159. package/src/runtime/executor.ts +0 -396
  160. package/src/runtime/planning-loop.ts +0 -81
  161. package/src/runtime/service.ts +0 -448
  162. package/src/runtime/session-manager.ts +0 -50
  163. package/src/runtime/state-observer.ts +0 -136
  164. package/src/runtime/vision-adapter.ts +0 -297
  165. package/src/types.ts +0 -297
  166. package/tests/bridge-client.test.ts +0 -176
  167. package/tests/browser-stealth.test.ts +0 -210
  168. package/tests/composite-adapter.test.ts +0 -64
  169. package/tests/mcp-server.test.ts +0 -151
  170. package/tests/memory-recall.test.ts +0 -339
  171. package/tests/memory-research.test.ts +0 -159
  172. package/tests/memory-seeds.test.ts +0 -120
  173. package/tests/memory-store.test.ts +0 -392
  174. package/tests/types.test.ts +0 -92
  175. package/tsconfig.check.json +0 -17
  176. package/tsconfig.json +0 -19
  177. package/vitest.config.ts +0 -8
@@ -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
- });