handhold-sdk 1.0.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 (50) hide show
  1. package/README.md +427 -0
  2. package/dist/d44f1fe8001e32f8e74d.svg +3056 -0
  3. package/dist/fonts/Phosphor.ttf +0 -0
  4. package/dist/fonts/Phosphor.woff +0 -0
  5. package/dist/fonts/Phosphor.woff2 +0 -0
  6. package/dist/handhold.min.js +1 -0
  7. package/package.json +114 -0
  8. package/src/HandHold.js +1599 -0
  9. package/src/agent/VENDOR.md +203 -0
  10. package/src/agent/agent/AgentLoop.js +366 -0
  11. package/src/agent/agent/prompts.js +163 -0
  12. package/src/agent/agent/tools.js +148 -0
  13. package/src/agent/controller.js +235 -0
  14. package/src/agent/dom/actions.js +456 -0
  15. package/src/agent/dom/domTree.js +1761 -0
  16. package/src/agent/dom/redact.js +98 -0
  17. package/src/agent/dom/serializer.js +332 -0
  18. package/src/agent/dom/utils.js +99 -0
  19. package/src/agent/index.js +169 -0
  20. package/src/agent/llm/connector.js +143 -0
  21. package/src/agent/package.json +3 -0
  22. package/src/agent/policy/PolicyGuard.js +172 -0
  23. package/src/agent/site/manifest.js +145 -0
  24. package/src/agent/ui/ChatWidget.js +219 -0
  25. package/src/agent-mode/AgentMode.js +429 -0
  26. package/src/api/APIClient.js +258 -0
  27. package/src/core/Config.js +207 -0
  28. package/src/core/UiState.js +83 -0
  29. package/src/core/defaults.js +20 -0
  30. package/src/events.js +59 -0
  31. package/src/index.js +77 -0
  32. package/src/intent/ActionVerbMap.js +324 -0
  33. package/src/intent/IntentExtractor.js +317 -0
  34. package/src/observers/ElementScanner.js +284 -0
  35. package/src/observers/NavigationObserver.js +182 -0
  36. package/src/observers/PageObserver.js +293 -0
  37. package/src/session/SessionManager.js +402 -0
  38. package/src/session/SessionStorage.js +148 -0
  39. package/src/ui/HelpWidget.js +1189 -0
  40. package/src/ui/UICoach.js +1725 -0
  41. package/src/ui/components/Button.css +137 -0
  42. package/src/ui/components/Button.js +102 -0
  43. package/src/ui/widget.css +599 -0
  44. package/src/utils/Logger.js +132 -0
  45. package/src/utils/MarkdownRenderer.js +39 -0
  46. package/src/utils/domUtils.js +350 -0
  47. package/src/utils/eventBus.js +102 -0
  48. package/src/utils/index.js +8 -0
  49. package/src/utils/stringUtils.js +202 -0
  50. package/types/index.d.ts +538 -0
@@ -0,0 +1,324 @@
1
+ /**
2
+ * Hand Hold SDK - Action Verb Map
3
+ * Maps common button/link text to canonical action verbs
4
+ */
5
+
6
+ const ACTION_VERB_MAP = {
7
+ // Approval actions
8
+ 'approve': 'approve',
9
+ 'confirm': 'confirm',
10
+ 'accept': 'approve',
11
+ 'authorize': 'approve',
12
+ 'validate': 'approve',
13
+ 'verify': 'approve',
14
+ 'ok': 'confirm',
15
+ 'yes': 'confirm',
16
+
17
+ // Creation actions
18
+ 'create': 'create',
19
+ 'add': 'create',
20
+ 'new': 'create',
21
+ 'insert': 'create',
22
+ 'plus': 'create',
23
+ '+': 'create',
24
+
25
+ // Submission actions
26
+ 'submit': 'submit',
27
+ 'send': 'submit',
28
+ 'post': 'submit',
29
+ 'publish': 'submit',
30
+ 'apply': 'submit',
31
+
32
+ // Save actions
33
+ 'save': 'save',
34
+ 'store': 'save',
35
+ 'keep': 'save',
36
+ 'preserve': 'save',
37
+
38
+ // Update actions
39
+ 'update': 'update',
40
+ 'edit': 'edit',
41
+ 'modify': 'edit',
42
+ 'change': 'edit',
43
+ 'revise': 'edit',
44
+
45
+ // Delete actions
46
+ 'delete': 'delete',
47
+ 'remove': 'delete',
48
+ 'trash': 'delete',
49
+ 'discard': 'delete',
50
+ 'clear': 'delete',
51
+ 'erase': 'delete',
52
+
53
+ // Download/Export actions
54
+ 'download': 'download',
55
+ 'export': 'export',
56
+ 'extract': 'export',
57
+ 'get': 'download',
58
+
59
+ // Upload/Import actions
60
+ 'upload': 'upload',
61
+ 'import': 'import',
62
+ 'load': 'import',
63
+
64
+ // Execution actions
65
+ 'run': 'run',
66
+ 'execute': 'run',
67
+ 'process': 'run',
68
+ 'start': 'run',
69
+ 'begin': 'run',
70
+ 'launch': 'run',
71
+ 'trigger': 'run',
72
+
73
+ // View actions
74
+ 'view': 'view',
75
+ 'see': 'view',
76
+ 'show': 'view',
77
+ 'display': 'view',
78
+ 'open': 'view',
79
+ 'preview': 'view',
80
+ 'look': 'view',
81
+
82
+ // Search actions
83
+ 'search': 'search',
84
+ 'find': 'search',
85
+ 'lookup': 'search',
86
+ 'query': 'search',
87
+
88
+ // Filter actions
89
+ 'filter': 'filter',
90
+ 'sort': 'filter',
91
+ 'refine': 'filter',
92
+
93
+ // Cancel actions
94
+ 'cancel': 'cancel',
95
+ 'abort': 'cancel',
96
+ 'stop': 'cancel',
97
+ 'close': 'cancel',
98
+ 'dismiss': 'cancel',
99
+ 'back': 'cancel',
100
+ 'exit': 'cancel',
101
+ 'no': 'cancel',
102
+
103
+ // Print actions
104
+ 'print': 'print',
105
+
106
+ // Review actions
107
+ 'review': 'review',
108
+ 'check': 'review',
109
+ 'inspect': 'review',
110
+ 'examine': 'review',
111
+
112
+ // Generate actions
113
+ 'generate': 'generate',
114
+ 'produce': 'generate',
115
+ 'build': 'generate',
116
+ 'make': 'generate',
117
+
118
+ // Copy actions
119
+ 'copy': 'copy',
120
+ 'duplicate': 'copy',
121
+ 'clone': 'copy',
122
+
123
+ // Share actions
124
+ 'share': 'share',
125
+ 'invite': 'share',
126
+
127
+ // Refresh actions
128
+ 'refresh': 'refresh',
129
+ 'reload': 'refresh',
130
+ 'sync': 'refresh',
131
+
132
+ // Navigate actions
133
+ 'navigate': 'navigate',
134
+ 'go': 'navigate',
135
+ 'next': 'navigate',
136
+ 'previous': 'navigate',
137
+ 'prev': 'navigate',
138
+ 'continue': 'navigate',
139
+
140
+ // Help actions
141
+ 'help': 'help',
142
+ 'support': 'help',
143
+ 'assist': 'help',
144
+
145
+ // Login/Logout actions
146
+ 'login': 'login',
147
+ 'signin': 'login',
148
+ 'sign-in': 'login',
149
+ 'logout': 'logout',
150
+ 'signout': 'logout',
151
+ 'sign-out': 'logout',
152
+
153
+ // Enable/Disable actions
154
+ 'enable': 'enable',
155
+ 'activate': 'enable',
156
+ 'disable': 'disable',
157
+ 'deactivate': 'disable',
158
+
159
+ // Connect/Disconnect actions
160
+ 'connect': 'connect',
161
+ 'link': 'connect',
162
+ 'disconnect': 'disconnect',
163
+ 'unlink': 'disconnect',
164
+
165
+ // Schedule actions
166
+ 'schedule': 'schedule',
167
+ 'plan': 'schedule',
168
+ 'book': 'schedule',
169
+ 'reserve': 'schedule',
170
+
171
+ // Assign actions
172
+ 'assign': 'assign',
173
+ 'delegate': 'assign',
174
+ 'transfer': 'assign',
175
+
176
+ // Archive actions
177
+ 'archive': 'archive',
178
+ 'store': 'archive',
179
+
180
+ // Restore actions
181
+ 'restore': 'restore',
182
+ 'recover': 'restore',
183
+ 'undo': 'restore',
184
+
185
+ // Compare actions
186
+ 'compare': 'compare',
187
+ 'diff': 'compare',
188
+
189
+ // Select actions
190
+ 'select': 'select',
191
+ 'choose': 'select',
192
+ 'pick': 'select',
193
+
194
+ // Pay/Purchase actions
195
+ 'pay': 'pay',
196
+ 'purchase': 'pay',
197
+ 'buy': 'pay',
198
+ 'checkout': 'pay',
199
+ };
200
+
201
+ // Reverse map for label generation
202
+ const VERB_LABELS = {
203
+ 'approve': 'Approve',
204
+ 'confirm': 'Confirm',
205
+ 'create': 'Create',
206
+ 'submit': 'Submit',
207
+ 'save': 'Save',
208
+ 'update': 'Update',
209
+ 'edit': 'Edit',
210
+ 'delete': 'Delete',
211
+ 'download': 'Download',
212
+ 'export': 'Export',
213
+ 'upload': 'Upload',
214
+ 'import': 'Import',
215
+ 'run': 'Run',
216
+ 'view': 'View',
217
+ 'search': 'Search',
218
+ 'filter': 'Filter',
219
+ 'cancel': 'Cancel',
220
+ 'print': 'Print',
221
+ 'review': 'Review',
222
+ 'generate': 'Generate',
223
+ 'copy': 'Copy',
224
+ 'share': 'Share',
225
+ 'refresh': 'Refresh',
226
+ 'navigate': 'Navigate',
227
+ 'help': 'Help',
228
+ 'login': 'Login',
229
+ 'logout': 'Logout',
230
+ 'enable': 'Enable',
231
+ 'disable': 'Disable',
232
+ 'connect': 'Connect',
233
+ 'disconnect': 'Disconnect',
234
+ 'schedule': 'Schedule',
235
+ 'assign': 'Assign',
236
+ 'archive': 'Archive',
237
+ 'restore': 'Restore',
238
+ 'compare': 'Compare',
239
+ 'select': 'Select',
240
+ 'pay': 'Pay',
241
+ };
242
+
243
+ class ActionVerbMap {
244
+ /**
245
+ * Get canonical verb for a word
246
+ * @param {string} word - Input word
247
+ * @returns {string|null} - Canonical verb or null
248
+ */
249
+ static get(word) {
250
+ if (!word) return null;
251
+ return ACTION_VERB_MAP[word.toLowerCase()] || null;
252
+ }
253
+
254
+ /**
255
+ * Get label for a canonical verb
256
+ * @param {string} verb - Canonical verb
257
+ * @returns {string} - Human-readable label
258
+ */
259
+ static getLabel(verb) {
260
+ return VERB_LABELS[verb] || verb.charAt(0).toUpperCase() + verb.slice(1);
261
+ }
262
+
263
+ /**
264
+ * Check if a word is an action verb
265
+ * @param {string} word - Input word
266
+ * @returns {boolean}
267
+ */
268
+ static isActionVerb(word) {
269
+ if (!word) return false;
270
+ return ACTION_VERB_MAP.hasOwnProperty(word.toLowerCase());
271
+ }
272
+
273
+ /**
274
+ * Get all action verbs
275
+ * @returns {Object} - Map of verbs
276
+ */
277
+ static getAll() {
278
+ return { ...ACTION_VERB_MAP };
279
+ }
280
+
281
+ /**
282
+ * Get all canonical verbs
283
+ * @returns {string[]} - Array of canonical verbs
284
+ */
285
+ static getCanonicalVerbs() {
286
+ return Object.keys(VERB_LABELS);
287
+ }
288
+
289
+ /**
290
+ * Find matching verb in a text string
291
+ * @param {string} text - Text to search
292
+ * @returns {Object|null} - { verb, position } or null
293
+ */
294
+ static findInText(text) {
295
+ if (!text) return null;
296
+
297
+ const words = text.toLowerCase().split(/\s+/);
298
+
299
+ // Check first word (most likely to be the action)
300
+ if (words[0] && ACTION_VERB_MAP[words[0]]) {
301
+ return {
302
+ word: words[0],
303
+ verb: ACTION_VERB_MAP[words[0]],
304
+ position: 0,
305
+ };
306
+ }
307
+
308
+ // Check other words
309
+ for (let i = 1; i < words.length; i++) {
310
+ if (ACTION_VERB_MAP[words[i]]) {
311
+ return {
312
+ word: words[i],
313
+ verb: ACTION_VERB_MAP[words[i]],
314
+ position: i,
315
+ };
316
+ }
317
+ }
318
+
319
+ return null;
320
+ }
321
+ }
322
+
323
+ export default ActionVerbMap;
324
+ export { ACTION_VERB_MAP, VERB_LABELS };
@@ -0,0 +1,317 @@
1
+ /**
2
+ * Hand Hold SDK - Intent Extractor
3
+ * Rule-based intent extraction from UI elements (no LLM required)
4
+ */
5
+
6
+ import ActionVerbMap from './ActionVerbMap.js';
7
+ import { toSnakeCase, toTitleCase } from '../utils/stringUtils.js';
8
+
9
+ class IntentExtractor {
10
+ /**
11
+ * Extract intents from UI elements
12
+ * @param {Array} uiElements - Array of UI element objects
13
+ * @param {string} route - Current page route
14
+ * @param {Object} options - { maxIntents: 6 }
15
+ * @returns {Array} - Array of intent objects
16
+ */
17
+ static extract(uiElements, route, options = {}) {
18
+ const maxIntents = options.maxIntents || 6;
19
+ const intents = [];
20
+ const routeContext = this._extractRouteContext(route);
21
+
22
+ // Process buttons first (highest priority)
23
+ const buttons = uiElements.filter(el => el.role === 'button' && !el.disabled);
24
+ for (const button of buttons) {
25
+ const intent = this._extractIntentFromElement(button, routeContext);
26
+ if (intent) {
27
+ intents.push({
28
+ ...intent,
29
+ source_element: button.selector,
30
+ element_position: button.position,
31
+ element_type: 'button',
32
+ });
33
+ }
34
+ }
35
+
36
+ // Process links with action text (medium priority)
37
+ const links = uiElements.filter(el => el.role === 'link');
38
+ for (const link of links) {
39
+ const intent = this._extractIntentFromElement(link, routeContext);
40
+ if (intent) {
41
+ intent.confidence *= 0.85; // Lower confidence for links
42
+ intents.push({
43
+ ...intent,
44
+ source_element: link.selector,
45
+ element_position: link.position,
46
+ element_type: 'link',
47
+ });
48
+ }
49
+ }
50
+
51
+ // Process other interactive elements
52
+ const others = uiElements.filter(el =>
53
+ !['button', 'link'].includes(el.role) &&
54
+ ['menuitem', 'tab', 'checkbox', 'radio'].includes(el.role)
55
+ );
56
+ for (const element of others) {
57
+ const intent = this._extractIntentFromElement(element, routeContext);
58
+ if (intent) {
59
+ intent.confidence *= 0.75;
60
+ intents.push({
61
+ ...intent,
62
+ source_element: element.selector,
63
+ element_position: element.position,
64
+ element_type: element.role,
65
+ });
66
+ }
67
+ }
68
+
69
+ // Sort by confidence and position
70
+ intents.sort((a, b) => {
71
+ // Primary sort by confidence
72
+ if (Math.abs(a.confidence - b.confidence) > 0.1) {
73
+ return b.confidence - a.confidence;
74
+ }
75
+ // Secondary sort by vertical position (higher on page = more important)
76
+ const aY = a.element_position?.y || 9999;
77
+ const bY = b.element_position?.y || 9999;
78
+ return aY - bY;
79
+ });
80
+
81
+ // Deduplicate by intent key
82
+ const uniqueIntents = this._deduplicateIntents(intents);
83
+
84
+ // Return top N intents
85
+ return uniqueIntents.slice(0, maxIntents);
86
+ }
87
+
88
+ /**
89
+ * Extract route context (module name from URL)
90
+ * @param {string} route - Page route
91
+ * @returns {string} - Context word
92
+ */
93
+ static _extractRouteContext(route) {
94
+ // Extract meaningful segments
95
+ const segments = route.split('/').filter(s => s.length > 0 && !s.match(/^\d+$/));
96
+
97
+ if (segments.length === 0) {
98
+ return 'page';
99
+ }
100
+
101
+ // Use the first meaningful segment as context
102
+ // e.g., "/payroll/processing" -> "payroll"
103
+ // e.g., "/employees/123/edit" -> "employees"
104
+ return segments[0].toLowerCase();
105
+ }
106
+
107
+ /**
108
+ * Extract intent from a single UI element
109
+ * @param {Object} element - UI element object
110
+ * @param {string} routeContext - Route context word
111
+ * @returns {Object|null} - Intent object or null
112
+ */
113
+ static _extractIntentFromElement(element, routeContext) {
114
+ // Get text from element (prefer aria-label for accessibility)
115
+ const text = (element.aria_label || element.text || element.title || '').trim();
116
+
117
+ if (!text || text.length < 2 || text.length > 50) {
118
+ return null;
119
+ }
120
+
121
+ // Skip generic/navigation elements
122
+ const skipPatterns = [
123
+ /^home$/i, /^menu$/i, /^settings$/i, /^profile$/i,
124
+ /^\d+$/, /^[<>]$/, /^more$/i, /^options$/i,
125
+ ];
126
+ if (skipPatterns.some(p => p.test(text))) {
127
+ return null;
128
+ }
129
+
130
+ // Find action verb in text
131
+ const verbMatch = ActionVerbMap.findInText(text);
132
+
133
+ if (!verbMatch) {
134
+ return null;
135
+ }
136
+
137
+ // Extract object (what the action applies to)
138
+ const words = text.toLowerCase().split(/\s+/);
139
+ let objectName = '';
140
+
141
+ if (verbMatch.position === 0 && words.length > 1) {
142
+ // Action verb is first word, rest is the object
143
+ objectName = words.slice(1).join('_');
144
+ } else if (verbMatch.position > 0) {
145
+ // Action verb is not first, object might be before
146
+ objectName = words.slice(0, verbMatch.position).join('_');
147
+ } else {
148
+ // Single word action, use route context
149
+ objectName = routeContext;
150
+ }
151
+
152
+ // Clean up object name
153
+ objectName = objectName.replace(/[^\w_]/g, '').replace(/_+/g, '_');
154
+
155
+ if (!objectName) {
156
+ objectName = routeContext;
157
+ }
158
+
159
+ // Generate intent key and label
160
+ const intentKey = `${verbMatch.verb}_${objectName}`;
161
+ const intentLabel = this._generateLabel(verbMatch.verb, objectName);
162
+
163
+ return {
164
+ key: intentKey,
165
+ label: intentLabel,
166
+ verb: verbMatch.verb,
167
+ object: objectName,
168
+ confidence: this._calculateConfidence(element, verbMatch),
169
+ original_text: text,
170
+ };
171
+ }
172
+
173
+ /**
174
+ * Generate human-readable label for intent
175
+ * @param {string} verb - Canonical verb
176
+ * @param {string} object - Object name in snake_case
177
+ * @returns {string} - Human-readable label
178
+ */
179
+ static _generateLabel(verb, object) {
180
+ const verbLabel = ActionVerbMap.getLabel(verb);
181
+ const objectLabel = toTitleCase(object);
182
+ return `${verbLabel} ${objectLabel}`;
183
+ }
184
+
185
+ /**
186
+ * Calculate confidence score for an intent
187
+ * @param {Object} element - UI element
188
+ * @param {Object} verbMatch - Verb match result
189
+ * @returns {number} - Confidence score (0-1)
190
+ */
191
+ static _calculateConfidence(element, verbMatch) {
192
+ let confidence = 0.85; // Base confidence for rule-based extraction
193
+
194
+ // Boost if verb is first word
195
+ if (verbMatch.position === 0) {
196
+ confidence += 0.05;
197
+ }
198
+
199
+ // Boost if element has data-testid (indicates well-structured UI)
200
+ if (element.data_testid) {
201
+ confidence += 0.03;
202
+ }
203
+
204
+ // Boost if element has aria-label
205
+ if (element.aria_label) {
206
+ confidence += 0.02;
207
+ }
208
+
209
+ // Slight penalty for very long text (might be descriptive, not actionable)
210
+ const textLength = (element.text || '').length;
211
+ if (textLength > 30) {
212
+ confidence -= 0.05;
213
+ }
214
+
215
+ // Cap at 0.95
216
+ return Math.min(0.95, confidence);
217
+ }
218
+
219
+ /**
220
+ * Deduplicate intents by key, keeping highest confidence
221
+ * @param {Array} intents - Array of intents
222
+ * @returns {Array} - Deduplicated array
223
+ */
224
+ static _deduplicateIntents(intents) {
225
+ const seen = new Map();
226
+
227
+ for (const intent of intents) {
228
+ const existing = seen.get(intent.key);
229
+ if (!existing || intent.confidence > existing.confidence) {
230
+ seen.set(intent.key, intent);
231
+ }
232
+ }
233
+
234
+ return Array.from(seen.values());
235
+ }
236
+
237
+ /**
238
+ * Match user query to available intents
239
+ * @param {string} query - User's free-text query
240
+ * @param {Array} availableIntents - Array of intent objects
241
+ * @returns {Object|null} - Best matching intent or null
242
+ */
243
+ static matchQueryToIntent(query, availableIntents) {
244
+ if (!query || !availableIntents?.length) {
245
+ return null;
246
+ }
247
+
248
+ const queryLower = query.toLowerCase();
249
+ const queryWords = queryLower.split(/\s+/).filter(w => w.length > 2);
250
+
251
+ let bestMatch = null;
252
+ let bestScore = 0;
253
+
254
+ for (const intent of availableIntents) {
255
+ let score = 0;
256
+
257
+ // Check if query contains the verb
258
+ if (queryLower.includes(intent.verb)) {
259
+ score += 0.4;
260
+ }
261
+
262
+ // Check if query contains object words
263
+ const objectWords = intent.object.split('_');
264
+ const objectMatchCount = objectWords.filter(w => queryLower.includes(w)).length;
265
+ score += (objectMatchCount / objectWords.length) * 0.4;
266
+
267
+ // Check if label matches
268
+ if (queryLower.includes(intent.label.toLowerCase())) {
269
+ score += 0.3;
270
+ }
271
+
272
+ // Check keyword overlap
273
+ const intentKeywords = [intent.verb, ...objectWords];
274
+ const keywordMatches = queryWords.filter(w =>
275
+ intentKeywords.some(k => k.includes(w) || w.includes(k))
276
+ ).length;
277
+ score += (keywordMatches / Math.max(queryWords.length, 1)) * 0.2;
278
+
279
+ if (score > bestScore) {
280
+ bestScore = score;
281
+ bestMatch = { ...intent, match_score: score };
282
+ }
283
+ }
284
+
285
+ // Return match only if confidence is reasonable
286
+ if (bestScore >= 0.3) {
287
+ return bestMatch;
288
+ }
289
+
290
+ return null;
291
+ }
292
+
293
+ /**
294
+ * Get suggested clarification when query is ambiguous
295
+ * @param {string} query - User's query
296
+ * @param {Array} candidates - Possible matching intents
297
+ * @returns {Object} - { question, options }
298
+ */
299
+ static getClarification(query, candidates) {
300
+ if (!candidates || candidates.length < 2) {
301
+ return null;
302
+ }
303
+
304
+ // Take top 2-3 candidates
305
+ const topCandidates = candidates.slice(0, 3);
306
+
307
+ return {
308
+ question: 'Did you mean one of these?',
309
+ options: topCandidates.map(c => ({
310
+ key: c.key,
311
+ label: c.label,
312
+ })),
313
+ };
314
+ }
315
+ }
316
+
317
+ export default IntentExtractor;