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.
- package/README.md +458 -93
- package/dist/.audit-log.jsonl +55 -0
- package/dist/.screenhand/memory/.lock +1 -0
- package/dist/.screenhand/memory/actions.jsonl +85 -0
- package/dist/.screenhand/memory/errors.jsonl +5 -0
- package/dist/.screenhand/memory/errors.jsonl.bak +4 -0
- package/dist/.screenhand/memory/state.json +35 -0
- package/dist/.screenhand/memory/state.json.bak +35 -0
- package/dist/.screenhand/memory/strategies.jsonl +12 -0
- package/dist/agent/cli.js +73 -0
- package/dist/agent/loop.js +258 -0
- package/dist/config.js +9 -0
- package/dist/index.js +56 -0
- package/dist/logging/timeline-logger.js +29 -0
- package/dist/mcp/mcp-stdio-server.js +448 -0
- package/dist/mcp/server.js +347 -0
- package/dist/mcp-desktop.js +2731 -0
- package/dist/mcp-entry.js +59 -0
- package/dist/memory/recall.js +160 -0
- package/dist/memory/research.js +98 -0
- package/dist/memory/seeds.js +89 -0
- package/dist/memory/session.js +161 -0
- package/dist/memory/store.js +391 -0
- package/dist/memory/types.js +4 -0
- package/dist/monitor/codex-monitor.js +377 -0
- package/dist/monitor/task-queue.js +84 -0
- package/dist/monitor/types.js +49 -0
- package/dist/native/bridge-client.js +174 -0
- package/dist/native/macos-bridge-client.js +5 -0
- package/dist/npm-publish-helper.js +117 -0
- package/dist/npm-token-cdp.js +113 -0
- package/dist/npm-token-create.js +135 -0
- package/dist/npm-token-finish.js +126 -0
- package/dist/playbook/engine.js +193 -0
- package/dist/playbook/index.js +4 -0
- package/dist/playbook/recorder.js +519 -0
- package/dist/playbook/runner.js +392 -0
- package/dist/playbook/store.js +166 -0
- package/dist/playbook/types.js +4 -0
- package/dist/runtime/accessibility-adapter.js +377 -0
- package/dist/runtime/app-adapter.js +48 -0
- package/dist/runtime/applescript-adapter.js +283 -0
- package/dist/runtime/ax-role-map.js +80 -0
- package/dist/runtime/browser-adapter.js +36 -0
- package/dist/runtime/cdp-chrome-adapter.js +505 -0
- package/dist/runtime/composite-adapter.js +205 -0
- package/dist/runtime/executor.js +250 -0
- package/dist/runtime/locator-cache.js +12 -0
- package/dist/runtime/planning-loop.js +47 -0
- package/dist/runtime/service.js +372 -0
- package/dist/runtime/session-manager.js +28 -0
- package/dist/runtime/state-observer.js +105 -0
- package/dist/runtime/vision-adapter.js +208 -0
- package/dist/scripts/codex-monitor-daemon.js +335 -0
- package/dist/scripts/supervisor-daemon.js +272 -0
- package/dist/scripts/worker-daemon.js +228 -0
- package/dist/src/agent/cli.js +82 -0
- package/dist/src/agent/loop.js +274 -0
- package/{src/config.ts → dist/src/config.js} +5 -10
- package/{src/index.ts → dist/src/index.js} +32 -52
- package/dist/src/jobs/manager.js +237 -0
- package/dist/src/jobs/runner.js +683 -0
- package/dist/src/jobs/store.js +102 -0
- package/dist/src/jobs/types.js +30 -0
- package/dist/src/jobs/worker.js +97 -0
- package/dist/src/logging/timeline-logger.js +45 -0
- package/dist/src/mcp/mcp-stdio-server.js +464 -0
- package/dist/src/mcp/server.js +363 -0
- package/dist/src/mcp-entry.js +60 -0
- package/dist/src/memory/recall.js +170 -0
- package/dist/src/memory/research.js +104 -0
- package/dist/src/memory/seeds.js +101 -0
- package/dist/src/memory/service.js +421 -0
- package/dist/src/memory/session.js +169 -0
- package/dist/src/memory/store.js +422 -0
- package/dist/src/memory/types.js +17 -0
- package/dist/src/monitor/codex-monitor.js +382 -0
- package/dist/src/monitor/task-queue.js +97 -0
- package/dist/src/monitor/types.js +62 -0
- package/dist/src/native/bridge-client.js +190 -0
- package/{src/native/macos-bridge-client.ts → dist/src/native/macos-bridge-client.js} +0 -1
- package/dist/src/playbook/engine.js +201 -0
- package/dist/src/playbook/index.js +20 -0
- package/dist/src/playbook/recorder.js +535 -0
- package/dist/src/playbook/runner.js +408 -0
- package/dist/src/playbook/store.js +183 -0
- package/dist/src/playbook/types.js +17 -0
- package/dist/src/runtime/accessibility-adapter.js +393 -0
- package/dist/src/runtime/app-adapter.js +64 -0
- package/dist/src/runtime/applescript-adapter.js +299 -0
- package/dist/src/runtime/ax-role-map.js +96 -0
- package/dist/src/runtime/browser-adapter.js +52 -0
- package/dist/src/runtime/cdp-chrome-adapter.js +521 -0
- package/dist/src/runtime/composite-adapter.js +221 -0
- package/dist/src/runtime/execution-contract.js +159 -0
- package/dist/src/runtime/executor.js +266 -0
- package/{src/runtime/locator-cache.ts → dist/src/runtime/locator-cache.js} +10 -15
- package/dist/src/runtime/planning-loop.js +63 -0
- package/dist/src/runtime/service.js +388 -0
- package/dist/src/runtime/session-manager.js +60 -0
- package/dist/src/runtime/state-observer.js +121 -0
- package/dist/src/runtime/vision-adapter.js +224 -0
- package/dist/src/supervisor/locks.js +186 -0
- package/dist/src/supervisor/supervisor.js +403 -0
- package/dist/src/supervisor/types.js +30 -0
- package/dist/src/test-mcp-protocol.js +154 -0
- package/dist/src/types.js +17 -0
- package/dist/src/util/atomic-write.js +118 -0
- package/dist/test-mcp-protocol.js +138 -0
- package/dist/types.js +1 -0
- package/package.json +18 -4
- package/.claude/commands/automate.md +0 -28
- package/.claude/commands/debug-ui.md +0 -19
- package/.claude/commands/screenshot.md +0 -15
- package/.github/FUNDING.yml +0 -1
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -27
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
- package/.mcp.json +0 -8
- package/DESKTOP_MCP_GUIDE.md +0 -92
- package/SECURITY.md +0 -44
- package/docs/architecture.md +0 -47
- package/install-skills.sh +0 -19
- package/mcp-bridge.ts +0 -271
- package/mcp-desktop.ts +0 -1221
- package/native/macos-bridge/Package.swift +0 -21
- package/native/macos-bridge/Sources/AccessibilityBridge.swift +0 -261
- package/native/macos-bridge/Sources/AppManagement.swift +0 -129
- package/native/macos-bridge/Sources/CoreGraphicsBridge.swift +0 -242
- package/native/macos-bridge/Sources/ObserverBridge.swift +0 -120
- package/native/macos-bridge/Sources/VisionBridge.swift +0 -80
- package/native/macos-bridge/Sources/main.swift +0 -345
- package/native/windows-bridge/AppManagement.cs +0 -234
- package/native/windows-bridge/InputBridge.cs +0 -436
- package/native/windows-bridge/Program.cs +0 -265
- package/native/windows-bridge/ScreenCapture.cs +0 -329
- package/native/windows-bridge/UIAutomationBridge.cs +0 -571
- package/native/windows-bridge/WindowsBridge.csproj +0 -17
- package/playbooks/devpost.json +0 -186
- package/playbooks/instagram.json +0 -41
- package/playbooks/instagram_v2.json +0 -201
- package/playbooks/x_v1.json +0 -211
- package/scripts/devpost-live-loop.mjs +0 -421
- package/src/logging/timeline-logger.ts +0 -55
- package/src/mcp/server.ts +0 -449
- package/src/memory/recall.ts +0 -191
- package/src/memory/research.ts +0 -146
- package/src/memory/seeds.ts +0 -123
- package/src/memory/session.ts +0 -201
- package/src/memory/store.ts +0 -434
- package/src/memory/types.ts +0 -69
- package/src/native/bridge-client.ts +0 -239
- package/src/runtime/accessibility-adapter.ts +0 -487
- package/src/runtime/app-adapter.ts +0 -169
- package/src/runtime/applescript-adapter.ts +0 -376
- package/src/runtime/ax-role-map.ts +0 -102
- package/src/runtime/browser-adapter.ts +0 -129
- package/src/runtime/cdp-chrome-adapter.ts +0 -676
- package/src/runtime/composite-adapter.ts +0 -274
- package/src/runtime/executor.ts +0 -396
- package/src/runtime/planning-loop.ts +0 -81
- package/src/runtime/service.ts +0 -448
- package/src/runtime/session-manager.ts +0 -50
- package/src/runtime/state-observer.ts +0 -136
- package/src/runtime/vision-adapter.ts +0 -297
- package/src/types.ts +0 -297
- package/tests/bridge-client.test.ts +0 -176
- package/tests/browser-stealth.test.ts +0 -210
- package/tests/composite-adapter.test.ts +0 -64
- package/tests/mcp-server.test.ts +0 -151
- package/tests/memory-recall.test.ts +0 -339
- package/tests/memory-research.test.ts +0 -159
- package/tests/memory-seeds.test.ts +0 -120
- package/tests/memory-store.test.ts +0 -392
- package/tests/types.test.ts +0 -92
- package/tsconfig.check.json +0 -17
- package/tsconfig.json +0 -19
- 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
|
-
});
|
package/tests/mcp-server.test.ts
DELETED
|
@@ -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
|
-
});
|