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
@@ -0,0 +1,446 @@
1
+ // Copyright (C) 2025 Clazro Technology Private Limited
2
+ // SPDX-License-Identifier: AGPL-3.0-only
3
+ //
4
+ // This file is part of ScreenHand.
5
+ //
6
+ // ScreenHand is free software: you can redistribute it and/or modify
7
+ // it under the terms of the GNU Affero General Public License as
8
+ // published by the Free Software Foundation, version 3.
9
+ //
10
+ // ScreenHand is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with ScreenHand. If not, see <https://www.gnu.org/licenses/>.
17
+ /**
18
+ * MemoryService — unified facade over MemoryStore, SessionTracker, and RecallEngine
19
+ *
20
+ * Single entry-point for all memory operations. Adds:
21
+ * - state.json snapshot (debounced, written on every action)
22
+ * - learnings.jsonl (verified patterns, separate from strategies)
23
+ * - MemoryPolicy (error/stall thresholds)
24
+ * - Mission tracking
25
+ */
26
+ import fs from "node:fs";
27
+ import path from "node:path";
28
+ import { writeFileAtomicSync } from "../util/atomic-write.js";
29
+ import { MemoryStore } from "./store.js";
30
+ import { SessionTracker } from "./session.js";
31
+ import { RecallEngine } from "./recall.js";
32
+ import { seedLearningsFromPlaybooks } from "./playbook-seeds.js";
33
+ // ── Defaults ─────────────────────────────────────
34
+ const DEFAULT_POLICY = {
35
+ maxConsecutiveErrors: 5,
36
+ stallThresholdMs: 300_000,
37
+ escalateAfterRetries: 3,
38
+ pauseBetweenActionsMs: 500,
39
+ };
40
+ const SNAPSHOT_DEBOUNCE_MS = 200;
41
+ const MAX_LEARNINGS = 1000;
42
+ // ── Service ──────────────────────────────────────
43
+ export class MemoryService {
44
+ store;
45
+ session;
46
+ recall;
47
+ baseDir;
48
+ memDir;
49
+ learningsCache = [];
50
+ snapshot;
51
+ snapshotTimer = null;
52
+ consecutiveErrors = 0;
53
+ lastError = null;
54
+ actionsFailed = 0;
55
+ actionsTotal = 0;
56
+ sessionStartedAt;
57
+ initialized = false;
58
+ constructor(baseDir) {
59
+ this.baseDir = baseDir;
60
+ this.memDir = path.join(baseDir, ".screenhand", "memory");
61
+ this.store = new MemoryStore(baseDir);
62
+ this.session = new SessionTracker(this.store);
63
+ this.recall = new RecallEngine(this.store);
64
+ this.sessionStartedAt = new Date().toISOString();
65
+ this.snapshot = {
66
+ session: {
67
+ id: this.session.getSessionId(),
68
+ client: "unknown",
69
+ startedAt: this.sessionStartedAt,
70
+ lastActionAt: this.sessionStartedAt,
71
+ },
72
+ mission: { current: null, phase: "idle" },
73
+ health: {
74
+ actionsTotal: 0,
75
+ actionsFailed: 0,
76
+ successRate: 1,
77
+ lastError: null,
78
+ consecutiveErrors: 0,
79
+ },
80
+ patterns: { topWorking: [], topFailing: [], knownBlockers: [] },
81
+ policy: { ...DEFAULT_POLICY },
82
+ };
83
+ }
84
+ // ── Initialization ───────────────────────────────
85
+ /** Load all caches from disk. Call once at startup. */
86
+ init() {
87
+ if (this.initialized)
88
+ return;
89
+ this.initialized = true;
90
+ this.store.init();
91
+ this.ensureMemDir();
92
+ // Load learnings
93
+ this.learningsCache = this.readJsonlSafe("learnings.jsonl");
94
+ // Seed learnings from playbooks — selectors, detection, policy notes.
95
+ // These have scope prefixes like "chrome/figma" so they merge cleanly
96
+ // with runtime-discovered learnings without duplicating.
97
+ const playbooksDir = path.join(this.baseDir, "references");
98
+ const pbLearnings = seedLearningsFromPlaybooks(playbooksDir);
99
+ if (pbLearnings.length > 0) {
100
+ const existingKeys = new Set(this.learningsCache.map(l => `${l.scope}::${l.pattern}::${l.method}`));
101
+ let added = 0;
102
+ for (const pl of pbLearnings) {
103
+ const key = `${pl.scope}::${pl.pattern}::${pl.method}`;
104
+ if (!existingKeys.has(key)) {
105
+ this.learningsCache.push({
106
+ id: `pb_lrn_${added}`,
107
+ ...pl,
108
+ });
109
+ existingKeys.add(key);
110
+ added++;
111
+ }
112
+ }
113
+ }
114
+ // Load existing snapshot if present (to restore mission/policy across restarts)
115
+ const snapPath = this.filePath("state.json");
116
+ if (fs.existsSync(snapPath)) {
117
+ try {
118
+ const raw = fs.readFileSync(snapPath, "utf-8");
119
+ const loaded = JSON.parse(raw);
120
+ // Restore mission and policy from previous run
121
+ if (loaded.mission)
122
+ this.snapshot.mission = loaded.mission;
123
+ if (loaded.policy)
124
+ this.snapshot.policy = { ...DEFAULT_POLICY, ...loaded.policy };
125
+ }
126
+ catch {
127
+ // Corrupted snapshot — start fresh
128
+ }
129
+ }
130
+ // Sync stats from store
131
+ const stats = this.store.getStats();
132
+ this.actionsTotal = stats.totalActions;
133
+ this.actionsFailed = stats.totalActions - Math.round(stats.successRate * stats.totalActions);
134
+ this.rebuildPatterns();
135
+ this.updateHealthInSnapshot();
136
+ this.writeSnapshotSync();
137
+ }
138
+ // ── Snapshot ─────────────────────────────────────
139
+ /** Get the current in-memory snapshot (zero-cost). */
140
+ getSnapshot() {
141
+ return this.snapshot;
142
+ }
143
+ scheduleSnapshotWrite() {
144
+ if (this.snapshotTimer)
145
+ return;
146
+ this.snapshotTimer = setTimeout(() => {
147
+ this.snapshotTimer = null;
148
+ this.writeSnapshotSync();
149
+ }, SNAPSHOT_DEBOUNCE_MS);
150
+ }
151
+ writeSnapshotSync() {
152
+ this.ensureMemDir();
153
+ try {
154
+ writeFileAtomicSync(this.filePath("state.json"), JSON.stringify(this.snapshot, null, 2));
155
+ }
156
+ catch {
157
+ // Non-critical
158
+ }
159
+ }
160
+ updateHealthInSnapshot() {
161
+ this.snapshot.health = {
162
+ actionsTotal: this.actionsTotal,
163
+ actionsFailed: this.actionsFailed,
164
+ successRate: this.actionsTotal > 0 ? (this.actionsTotal - this.actionsFailed) / this.actionsTotal : 1,
165
+ lastError: this.lastError,
166
+ consecutiveErrors: this.consecutiveErrors,
167
+ };
168
+ }
169
+ rebuildPatterns() {
170
+ // Top working: learnings with high confidence, sorted desc
171
+ const working = this.learningsCache
172
+ .filter((l) => l.confidence >= 0.6 && l.successCount > l.failCount)
173
+ .sort((a, b) => b.confidence - a.confidence)
174
+ .slice(0, 10)
175
+ .map((l) => `${l.scope}: ${l.pattern} (${l.method})`);
176
+ // Top failing: learnings with low confidence or high fail count
177
+ const failing = this.learningsCache
178
+ .filter((l) => l.failCount > l.successCount)
179
+ .sort((a, b) => b.failCount - a.failCount)
180
+ .slice(0, 10)
181
+ .map((l) => `${l.scope}: ${l.pattern} (${l.method})`);
182
+ // Known blockers: errors with no resolution
183
+ const errors = this.store.readErrors();
184
+ const blockers = errors
185
+ .filter((e) => !e.resolution && e.occurrences >= 2)
186
+ .sort((a, b) => b.occurrences - a.occurrences)
187
+ .slice(0, 10)
188
+ .map((e) => `${e.tool}: ${e.error}`);
189
+ this.snapshot.patterns = {
190
+ topWorking: working,
191
+ topFailing: failing,
192
+ knownBlockers: blockers,
193
+ };
194
+ }
195
+ // ── Recording ────────────────────────────────────
196
+ /** Record an action event. Delegates to store + session tracker, updates snapshot. */
197
+ recordEvent(entry) {
198
+ this.store.appendAction(entry);
199
+ this.session.recordAction(entry);
200
+ this.actionsTotal++;
201
+ if (!entry.success) {
202
+ this.actionsFailed++;
203
+ this.consecutiveErrors++;
204
+ this.lastError = entry.error;
205
+ }
206
+ else {
207
+ this.consecutiveErrors = 0;
208
+ }
209
+ this.snapshot.session.id = this.session.getSessionId();
210
+ this.snapshot.session.lastActionAt = entry.timestamp;
211
+ this.updateHealthInSnapshot();
212
+ this.scheduleSnapshotWrite();
213
+ }
214
+ /** Record an error pattern. Delegates to store, optionally creates a learning. */
215
+ recordError(tool, error, fix, scope) {
216
+ const pattern = {
217
+ id: "err_" + Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
218
+ tool,
219
+ params: {},
220
+ error,
221
+ resolution: fix,
222
+ occurrences: 1,
223
+ lastSeen: new Date().toISOString(),
224
+ };
225
+ this.store.appendError(pattern);
226
+ // If a fix is provided, record it as a learning
227
+ if (fix && scope) {
228
+ this.recordLearning({
229
+ scope,
230
+ pattern: error,
231
+ method: "ax",
232
+ confidence: 0.5,
233
+ successCount: 0,
234
+ failCount: 1,
235
+ lastSeen: new Date().toISOString(),
236
+ fix,
237
+ });
238
+ }
239
+ this.rebuildPatterns();
240
+ this.scheduleSnapshotWrite();
241
+ }
242
+ // ── Learnings ────────────────────────────────────
243
+ /** Append a verified learning to learnings.jsonl. */
244
+ recordLearning(learning) {
245
+ const full = {
246
+ id: "lrn_" + Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
247
+ ...learning,
248
+ };
249
+ // Check for existing learning with same scope + pattern + method
250
+ const idx = this.learningsCache.findIndex((l) => l.scope === full.scope && l.pattern === full.pattern && l.method === full.method);
251
+ if (idx >= 0) {
252
+ const existing = this.learningsCache[idx];
253
+ this.learningsCache[idx] = {
254
+ ...existing,
255
+ successCount: existing.successCount + full.successCount,
256
+ failCount: existing.failCount + full.failCount,
257
+ confidence: this.computeConfidence(existing.successCount + full.successCount, existing.failCount + full.failCount),
258
+ lastSeen: full.lastSeen,
259
+ fix: full.fix ?? existing.fix,
260
+ };
261
+ }
262
+ else {
263
+ this.learningsCache.push(full);
264
+ this.enforceLearningsLimit();
265
+ }
266
+ this.writeLearningsAsync();
267
+ this.rebuildPatterns();
268
+ this.scheduleSnapshotWrite();
269
+ }
270
+ /** Search learnings by scope and/or method. */
271
+ queryPatterns(scope, method) {
272
+ let results = this.learningsCache;
273
+ if (scope) {
274
+ results = results.filter((l) => l.scope === scope || l.scope.startsWith(scope + "/"));
275
+ }
276
+ if (method) {
277
+ results = results.filter((l) => l.method === method);
278
+ }
279
+ return results;
280
+ }
281
+ computeConfidence(success, fail) {
282
+ const total = success + fail;
283
+ if (total === 0)
284
+ return 0;
285
+ return success / total;
286
+ }
287
+ enforceLearningsLimit() {
288
+ if (this.learningsCache.length <= MAX_LEARNINGS)
289
+ return;
290
+ // Evict lowest-confidence, oldest learnings
291
+ this.learningsCache.sort((a, b) => {
292
+ const confDiff = a.confidence - b.confidence;
293
+ if (Math.abs(confDiff) > 0.1)
294
+ return confDiff;
295
+ return new Date(a.lastSeen).getTime() - new Date(b.lastSeen).getTime();
296
+ });
297
+ this.learningsCache = this.learningsCache.slice(-MAX_LEARNINGS);
298
+ }
299
+ writeLearningsAsync() {
300
+ this.ensureMemDir();
301
+ const data = this.learningsCache.map((l) => JSON.stringify(l)).join("\n") + (this.learningsCache.length ? "\n" : "");
302
+ fs.writeFile(this.filePath("learnings.jsonl"), data, () => { });
303
+ }
304
+ // ── Recall (delegates to RecallEngine) ───────────
305
+ /** Search error patterns, optionally filtered by tool. */
306
+ queryErrors(tool) {
307
+ return this.recall.recallErrors(tool);
308
+ }
309
+ /** Fuzzy-match strategies by query string. Optionally filter by current app. */
310
+ recallStrategies(query, limit, currentBundleId) {
311
+ return this.recall.recallStrategies(query, limit, currentBundleId);
312
+ }
313
+ /** Quick error check for interceptor (~0ms). */
314
+ quickErrorCheck(tool) {
315
+ return this.recall.quickErrorCheck(tool);
316
+ }
317
+ /** Quick strategy hint for interceptor (~0ms). */
318
+ quickStrategyHint(recentTools, currentBundleId) {
319
+ return this.recall.quickStrategyHint(recentTools, currentBundleId);
320
+ }
321
+ /** Record strategy outcome for feedback loop. */
322
+ recordStrategyOutcome(fingerprint, success) {
323
+ this.store.recordStrategyOutcome(fingerprint, success);
324
+ }
325
+ // ── Session / Strategy ───────────────────────────
326
+ /** Get the current session ID. */
327
+ getSessionId() {
328
+ return this.session.getSessionId();
329
+ }
330
+ /** Get recent tool names from session buffer. */
331
+ getRecentToolNames(limit) {
332
+ return this.session.getRecentToolNames(limit);
333
+ }
334
+ /** End current session and save a strategy if successful. */
335
+ saveStrategy(task, tags) {
336
+ const strategy = this.session.endSession(true, task);
337
+ if (strategy && tags && tags.length > 0) {
338
+ // Merge additional tags
339
+ const merged = new Set([...strategy.tags, ...tags]);
340
+ strategy.tags = [...merged];
341
+ }
342
+ return strategy;
343
+ }
344
+ /** Read raw actions from store (for exports/playbooks). */
345
+ readActions() {
346
+ return this.store.readActions();
347
+ }
348
+ /** Read raw errors from store. */
349
+ readErrors() {
350
+ return this.store.readErrors();
351
+ }
352
+ /** Read raw strategies from store. */
353
+ readStrategies() {
354
+ return this.store.readStrategies();
355
+ }
356
+ /** Append an error pattern directly (for interceptor compatibility). */
357
+ appendError(pattern) {
358
+ this.store.appendError(pattern);
359
+ }
360
+ /** Append a strategy directly. */
361
+ appendStrategy(strategy) {
362
+ this.store.appendStrategy(strategy);
363
+ }
364
+ // ── Stats ────────────────────────────────────────
365
+ /** Get aggregate memory stats. */
366
+ getStats() {
367
+ return this.store.getStats();
368
+ }
369
+ // ── Mission ──────────────────────────────────────
370
+ /** Set the current mission and optionally a phase. */
371
+ setMission(mission, phase) {
372
+ this.snapshot.mission.current = mission;
373
+ if (phase)
374
+ this.snapshot.mission.phase = phase;
375
+ this.scheduleSnapshotWrite();
376
+ }
377
+ /** Set the client identifier (e.g., "claude-code", "mcp-desktop"). */
378
+ setClient(client) {
379
+ this.snapshot.session.client = client;
380
+ this.scheduleSnapshotWrite();
381
+ }
382
+ // ── Clear ────────────────────────────────────────
383
+ /** Clear specific memory categories or everything. */
384
+ clear(what) {
385
+ if (what === "learnings" || what === "all") {
386
+ this.learningsCache = [];
387
+ const fp = this.filePath("learnings.jsonl");
388
+ if (fs.existsSync(fp))
389
+ fs.writeFileSync(fp, "");
390
+ }
391
+ if (what !== "learnings") {
392
+ // Delegate non-learnings clears to the store
393
+ const storeWhat = what === "all" ? "all" : what;
394
+ this.store.clear(storeWhat);
395
+ }
396
+ if (what === "all" || what === "actions") {
397
+ this.actionsTotal = 0;
398
+ this.actionsFailed = 0;
399
+ this.consecutiveErrors = 0;
400
+ this.lastError = null;
401
+ }
402
+ this.updateHealthInSnapshot();
403
+ this.rebuildPatterns();
404
+ this.writeSnapshotSync();
405
+ }
406
+ // ── Helpers ──────────────────────────────────────
407
+ ensureMemDir() {
408
+ if (!fs.existsSync(this.memDir)) {
409
+ fs.mkdirSync(this.memDir, { recursive: true });
410
+ }
411
+ }
412
+ filePath(name) {
413
+ return path.join(this.memDir, name);
414
+ }
415
+ readJsonlSafe(file) {
416
+ const fp = this.filePath(file);
417
+ if (!fs.existsSync(fp))
418
+ return [];
419
+ let text;
420
+ try {
421
+ // Guard against oversized files (same 10MB limit as LearningEngine)
422
+ const stat = fs.statSync(fp);
423
+ if (stat.size > 10 * 1024 * 1024)
424
+ return [];
425
+ text = fs.readFileSync(fp, "utf-8").trim();
426
+ }
427
+ catch {
428
+ return [];
429
+ }
430
+ if (!text)
431
+ return [];
432
+ const results = [];
433
+ for (const line of text.split("\n")) {
434
+ const trimmed = line.trim();
435
+ if (!trimmed)
436
+ continue;
437
+ try {
438
+ results.push(JSON.parse(trimmed));
439
+ }
440
+ catch {
441
+ // Skip corrupted line
442
+ }
443
+ }
444
+ return results;
445
+ }
446
+ }
@@ -0,0 +1,169 @@
1
+ // Copyright (C) 2025 Clazro Technology Private Limited
2
+ // SPDX-License-Identifier: AGPL-3.0-only
3
+ //
4
+ // This file is part of ScreenHand.
5
+ //
6
+ // ScreenHand is free software: you can redistribute it and/or modify
7
+ // it under the terms of the GNU Affero General Public License as
8
+ // published by the Free Software Foundation, version 3.
9
+ //
10
+ // ScreenHand is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with ScreenHand. If not, see <https://www.gnu.org/licenses/>.
17
+ import { MemoryStore } from "./store.js";
18
+ const SESSION_GAP_MS = 60_000; // 60s gap = new session
19
+ const MAX_BUFFER_SIZE = 100;
20
+ const MIN_AUTO_SAVE_STEPS = 3; // Need at least 3 successful steps to auto-save
21
+ export class SessionTracker {
22
+ store;
23
+ sessionId;
24
+ taskDescription = null;
25
+ buffer = [];
26
+ lastActionTime = 0;
27
+ constructor(store) {
28
+ this.store = store;
29
+ this.sessionId = SessionTracker.generateId();
30
+ }
31
+ static generateId() {
32
+ return "s_" + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
33
+ }
34
+ /** Start (or restart) a named task session */
35
+ startSession(taskDescription) {
36
+ // Auto-save previous session if it had successful actions
37
+ this.tryAutoSave();
38
+ this.sessionId = SessionTracker.generateId();
39
+ this.taskDescription = taskDescription ?? null;
40
+ this.buffer = [];
41
+ this.lastActionTime = Date.now();
42
+ return this.sessionId;
43
+ }
44
+ /** Get the current session ID, auto-rotating if stale */
45
+ getSessionId() {
46
+ const now = Date.now();
47
+ if (this.lastActionTime > 0 && now - this.lastActionTime > SESSION_GAP_MS) {
48
+ // Session gap detected — auto-save previous sequence then start fresh
49
+ this.tryAutoSave();
50
+ this.sessionId = SessionTracker.generateId();
51
+ this.buffer = [];
52
+ this.taskDescription = null;
53
+ }
54
+ return this.sessionId;
55
+ }
56
+ /** Record an action into the current session buffer */
57
+ recordAction(entry) {
58
+ const now = Date.now();
59
+ if (this.lastActionTime > 0 && now - this.lastActionTime > SESSION_GAP_MS) {
60
+ // Gap detected — auto-save then start new session
61
+ this.tryAutoSave();
62
+ this.sessionId = SessionTracker.generateId();
63
+ this.buffer = [];
64
+ this.taskDescription = null;
65
+ }
66
+ this.lastActionTime = now;
67
+ this.buffer.push(entry);
68
+ if (this.buffer.length > MAX_BUFFER_SIZE) {
69
+ this.buffer.shift();
70
+ }
71
+ }
72
+ /** End the session and save a strategy if successful */
73
+ endSession(success, taskDescription) {
74
+ const task = taskDescription ?? this.taskDescription;
75
+ if (!success || !task || this.buffer.length === 0) {
76
+ this.buffer = [];
77
+ return null;
78
+ }
79
+ const strategy = this.buildStrategy(task, this.buffer);
80
+ this.store.appendStrategy(strategy);
81
+ this.buffer = [];
82
+ return strategy;
83
+ }
84
+ /** Get the current session's action buffer */
85
+ getBuffer() {
86
+ return [...this.buffer];
87
+ }
88
+ /** Get recent tool names (for strategy hint matching) */
89
+ getRecentToolNames(limit = 10) {
90
+ return this.buffer.slice(-limit).map((a) => a.tool);
91
+ }
92
+ /** Get current task description */
93
+ getTaskDescription() {
94
+ return this.taskDescription;
95
+ }
96
+ // ── auto-save logic ────────────────────────────
97
+ /**
98
+ * Try to auto-save the current buffer as a strategy.
99
+ * Only saves if there are MIN_AUTO_SAVE_STEPS+ consecutive successes.
100
+ * Uses tool sequence as task description if no explicit one was given.
101
+ */
102
+ tryAutoSave() {
103
+ if (this.buffer.length < MIN_AUTO_SAVE_STEPS)
104
+ return;
105
+ // Find the longest trailing streak of successes
106
+ let successStreak = [];
107
+ for (let i = this.buffer.length - 1; i >= 0; i--) {
108
+ if (this.buffer[i].success) {
109
+ successStreak.unshift(this.buffer[i]);
110
+ }
111
+ else {
112
+ break;
113
+ }
114
+ }
115
+ if (successStreak.length < MIN_AUTO_SAVE_STEPS)
116
+ return;
117
+ // Build a task description from the tool sequence if none provided
118
+ const task = this.taskDescription ?? this.inferTaskDescription(successStreak);
119
+ const strategy = this.buildStrategy(task, successStreak);
120
+ this.store.appendStrategy(strategy);
121
+ }
122
+ /** Infer a task description from a sequence of actions */
123
+ inferTaskDescription(actions) {
124
+ const tools = [...new Set(actions.map((a) => a.tool))];
125
+ // Extract key param values (bundle IDs, titles, URLs, etc.)
126
+ const keyParams = [];
127
+ for (const a of actions) {
128
+ for (const [key, val] of Object.entries(a.params)) {
129
+ if (typeof val === "string" && val.length > 2 && val.length < 60) {
130
+ if (["bundleId", "title", "url", "text", "script", "selector", "menuPath"].includes(key)) {
131
+ keyParams.push(val);
132
+ }
133
+ }
134
+ }
135
+ }
136
+ const paramHint = keyParams.length > 0 ? ` (${keyParams.slice(0, 3).join(", ")})` : "";
137
+ return `${tools.join(" → ")}${paramHint}`;
138
+ }
139
+ buildStrategy(task, actions) {
140
+ const steps = actions.map((a) => ({
141
+ tool: a.tool,
142
+ params: a.params,
143
+ }));
144
+ const totalDurationMs = actions.reduce((sum, a) => sum + a.durationMs, 0);
145
+ const tags = extractTags(task, steps);
146
+ const fingerprint = MemoryStore.makeFingerprint(steps.map((s) => s.tool));
147
+ return {
148
+ id: "str_" + Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
149
+ task,
150
+ steps,
151
+ totalDurationMs,
152
+ successCount: 1,
153
+ failCount: 0,
154
+ lastUsed: new Date().toISOString(),
155
+ tags,
156
+ fingerprint,
157
+ };
158
+ }
159
+ }
160
+ /** Extract tags from task description and tool names */
161
+ function extractTags(task, steps) {
162
+ const tags = new Set();
163
+ const words = task.toLowerCase().split(/\W+/).filter((w) => w.length >= 3);
164
+ for (const w of words)
165
+ tags.add(w);
166
+ for (const s of steps)
167
+ tags.add(s.tool);
168
+ return [...tags];
169
+ }