appium-uiwatchers-plugin 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.
package/.c8rc.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "all": true,
3
+ "include": ["lib/**/*.js"],
4
+ "exclude": ["test/**", "**/*.spec.js", "**/*.test.js", "**/*.e2e.spec.cjs", "lib/types.js"],
5
+ "reporter": ["text", "html", "lcov"],
6
+ "report-dir": "./coverage",
7
+ "check-coverage": true,
8
+ "lines": 80,
9
+ "statements": 80,
10
+ "functions": 80,
11
+ "branches": 80
12
+ }
@@ -0,0 +1,28 @@
1
+ name: Publish to NPM
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ publish:
9
+ runs-on: ubuntu-latest
10
+ permissions:
11
+ contents: read
12
+ id-token: write
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+
16
+ - uses: actions/setup-node@v4
17
+ with:
18
+ node-version: '20.x'
19
+ registry-url: 'https://registry.npmjs.org'
20
+
21
+ - name: Install latest npm
22
+ run: npm install -g npm@latest
23
+
24
+ - name: Install dependencies
25
+ run: npm ci
26
+
27
+ - name: Publish to npm
28
+ run: npm publish --provenance --access public
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env sh
2
+ . "$(dirname -- "$0")/_/husky.sh"
3
+
4
+ npx lint-staged
@@ -0,0 +1,4 @@
1
+ {
2
+ "*.{ts,js}": ["eslint --fix", "prettier --write"],
3
+ "*.{json,md}": ["prettier --write"]
4
+ }
package/.mocharc.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "require": ["@appium/support/build/lib/env"],
3
+ "timeout": 20000,
4
+ "slow": 10000,
5
+ "reporter": "spec",
6
+ "color": true,
7
+ "recursive": true,
8
+ "exit": true,
9
+ "spec": ["test/unit/**/*.spec.js", "test/integration/**/*.spec.js", "test/e2e/**/*.e2e.spec.cjs"]
10
+ }
@@ -0,0 +1,6 @@
1
+ node_modules
2
+ lib
3
+ coverage
4
+ *.log
5
+ .nyc_output
6
+ package-lock.json
package/.prettierrc ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "semi": true,
3
+ "trailingComma": "es5",
4
+ "singleQuote": true,
5
+ "printWidth": 100,
6
+ "tabWidth": 2,
7
+ "useTabs": false,
8
+ "arrowParens": "always",
9
+ "bracketSpacing": true,
10
+ "endOfLine": "lf"
11
+ }
package/README.md ADDED
@@ -0,0 +1,376 @@
1
+ # Appium UI Watchers Plugin
2
+
3
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
+ [![Appium 3.x](https://img.shields.io/badge/Appium-3.x-purple.svg)](https://appium.io/)
5
+ [![Node.js 18+](https://img.shields.io/badge/Node.js-18+-green.svg)](https://nodejs.org/)
6
+ [![npm version](https://img.shields.io/npm/v/appium-uiwatchers-plugin.svg)](https://www.npmjs.com/package/appium-uiwatchers-plugin)
7
+ [![Build Status](https://img.shields.io/github/actions/workflow/status/rajvinodh/appium-uiwatchers-plugin/ci.yml?branch=main)](https://github.com/rajvinodh/appium-uiwatchers-plugin/actions)
8
+
9
+ ## Introduction
10
+
11
+ Mobile apps often display unexpected UI elements — cookie consent dialogs, permission prompts, rating popups—that break test automation. Traditional approaches require explicit waits or try-catch blocks scattered throughout test code.
12
+
13
+ This plugin provides a centralized, declarative way to handle these interruptions automatically.
14
+
15
+ ## Features
16
+
17
+ - **Zero Wait Overhead** — No waiting for UI elements that may never appear
18
+ - **Centralized Management** — Register watchers once, apply across entire session
19
+ - **Priority-Based Execution** — Higher priority watchers are checked first
20
+ - **Cooldown Support** — Wait for stable UI after dismissing an element
21
+ - **Auto-Expiry** — Watchers automatically expire after specified duration
22
+ - **One-Shot or Continuous** — Stop after first trigger or keep watching
23
+ - **Transparent Interception** — Handles on findElement, findElements, and element actions automatically
24
+ - **Session-Scoped** — Each session maintains its own independent set of watchers
25
+
26
+ ## Installation
27
+
28
+ ### From npm (coming soon)
29
+
30
+ ```bash
31
+ appium plugin install appium-uiwatchers-plugin
32
+ ```
33
+
34
+ ### From local source
35
+
36
+ ```bash
37
+ appium plugin install --source=local /path/to/appium-uiwatchers-plugin
38
+ ```
39
+
40
+ ### Verify installation
41
+
42
+ ```bash
43
+ appium plugin list
44
+ ```
45
+
46
+ You should see `uiwatchers` in the installed plugins list.
47
+
48
+ ### Activate the plugin
49
+
50
+ ```bash
51
+ appium --use-plugins=uiwatchers
52
+ ```
53
+
54
+ ## Quick Start
55
+
56
+ > **Note:** Ensure Appium server is started with `--use-plugins=uiwatchers`
57
+
58
+ **JavaScript**
59
+
60
+ ```javascript
61
+ // Register a watcher for cookie consent
62
+ await driver.execute('mobile: registerUIWatcher', {
63
+ name: 'cookie-consent',
64
+ referenceLocator: { using: 'id', value: 'com.app:id/cookie_banner' },
65
+ actionLocator: { using: 'id', value: 'com.app:id/accept_button' },
66
+ });
67
+
68
+ // Run tests - watchers trigger automatically on findElement/findElements
69
+ await driver.findElement('id', 'com.app:id/login_button');
70
+ ```
71
+
72
+ **Java**
73
+
74
+ ```java
75
+ // Register a watcher for cookie consent
76
+ Map<String, Object> watcherParams = new HashMap<>();
77
+ watcherParams.put("name", "cookie-consent");
78
+ watcherParams.put("referenceLocator", Map.of("using", "id", "value", "com.app:id/cookie_banner"));
79
+ watcherParams.put("actionLocator", Map.of("using", "id", "value", "com.app:id/accept_button"));
80
+
81
+ driver.executeScript("mobile: registerUIWatcher", watcherParams);
82
+
83
+ // Run tests - watchers trigger automatically on findElement/findElements
84
+ driver.findElement(By.id("com.app:id/login_button"));
85
+ ```
86
+
87
+ ## API Reference
88
+
89
+ ### mobile: registerUIWatcher
90
+
91
+ Registers a new UI watcher for automatic element handling.
92
+
93
+ **Parameters**
94
+
95
+ | Parameter | Type | Required | Default | Description |
96
+ | ---------------- | ------- | -------- | ------- | ---------------------------------------- |
97
+ | name | string | Yes | - | Unique watcher identifier |
98
+ | referenceLocator | object | Yes | - | Element to detect (trigger condition) |
99
+ | actionLocator | object | Yes | - | Element to click when triggered |
100
+ | duration | number | Yes | - | Auto-expiry time in ms (max: 60000) |
101
+ | priority | number | No | 0 | Execution order (higher = first) |
102
+ | stopOnFound | boolean | No | false | Deactivate after first trigger |
103
+ | cooldownMs | number | No | 0 | Wait time after action before re-trigger |
104
+
105
+ **Locator Object**
106
+
107
+ | Property | Type | Description |
108
+ | -------- | ------ | ------------------------------------------------- |
109
+ | using | string | Strategy: `id`, `xpath`, `accessibility id`, etc. |
110
+ | value | string | Locator value |
111
+
112
+ **Example**
113
+
114
+ ```json
115
+ {
116
+ "name": "cookie-consent",
117
+ "priority": 10,
118
+ "referenceLocator": {
119
+ "using": "id",
120
+ "value": "com.app:id/cookie_banner"
121
+ },
122
+ "actionLocator": {
123
+ "using": "id",
124
+ "value": "com.app:id/accept_button"
125
+ },
126
+ "duration": 60000,
127
+ "stopOnFound": false,
128
+ "cooldownMs": 5000
129
+ }
130
+ ```
131
+
132
+ **Response**
133
+
134
+ ```json
135
+ {
136
+ "success": true,
137
+ "watcher": {
138
+ "name": "cookie-consent",
139
+ "priority": 10,
140
+ "registeredAt": 1704067200000,
141
+ "expiresAt": 1704067260000,
142
+ "status": "active"
143
+ }
144
+ }
145
+ ```
146
+
147
+ ### mobile: unregisterUIWatcher
148
+
149
+ Removes a registered watcher by name.
150
+
151
+ **Parameters**
152
+
153
+ | Parameter | Type | Required | Description |
154
+ | --------- | ------ | -------- | ----------------------------- |
155
+ | name | string | Yes | Name of the watcher to remove |
156
+
157
+ **Example**
158
+
159
+ ```json
160
+ {
161
+ "name": "cookie-consent"
162
+ }
163
+ ```
164
+
165
+ **Response**
166
+
167
+ ```json
168
+ {
169
+ "success": true,
170
+ "removed": "cookie-consent"
171
+ }
172
+ ```
173
+
174
+ ### mobile: listUIWatchers
175
+
176
+ Returns all registered watchers with their current state.
177
+
178
+ **Parameters**
179
+
180
+ None.
181
+
182
+ **Response**
183
+
184
+ ```json
185
+ {
186
+ "success": true,
187
+ "watchers": [
188
+ {
189
+ "name": "cookie-consent",
190
+ "priority": 10,
191
+ "referenceLocator": { "using": "id", "value": "com.app:id/cookie_banner" },
192
+ "actionLocator": { "using": "id", "value": "com.app:id/accept_button" },
193
+ "duration": 60000,
194
+ "stopOnFound": false,
195
+ "cooldownMs": 5000,
196
+ "registeredAt": 1704067200000,
197
+ "expiresAt": 1704067260000,
198
+ "status": "active",
199
+ "triggerCount": 2,
200
+ "lastTriggeredAt": 1704067230000
201
+ }
202
+ ],
203
+ "totalCount": 1
204
+ }
205
+ ```
206
+
207
+ ### mobile: clearAllUIWatchers
208
+
209
+ Removes all registered watchers for the current session.
210
+
211
+ **Parameters**
212
+
213
+ None.
214
+
215
+ **Response**
216
+
217
+ ```json
218
+ {
219
+ "success": true,
220
+ "removedCount": 3
221
+ }
222
+ ```
223
+
224
+ ### mobile: enableUIWatchers
225
+
226
+ Enables watcher checking (enabled by default).
227
+
228
+ **Parameters**
229
+
230
+ None.
231
+
232
+ **Response**
233
+
234
+ ```json
235
+ {
236
+ "success": true,
237
+ "message": "UI Watchers enabled"
238
+ }
239
+ ```
240
+
241
+ ### mobile: disableUIWatchers
242
+
243
+ Temporarily disables watcher checking without removing watchers.
244
+
245
+ **Parameters**
246
+
247
+ None.
248
+
249
+ **Response**
250
+
251
+ ```json
252
+ {
253
+ "success": true,
254
+ "message": "UI Watchers disabled"
255
+ }
256
+ ```
257
+
258
+ ## Configuration
259
+
260
+ Plugin behavior can be customized via CLI options when starting Appium server.
261
+
262
+ | Option | Type | Range | Default | Description |
263
+ | ------------------------------------- | ------- | ----------- | ------- | --------------------------------- |
264
+ | --plugin-uiwatchers-max-watchers | integer | 1-20 | 5 | Maximum watchers per session |
265
+ | --plugin-uiwatchers-max-duration-ms | integer | 1000-600000 | 60000 | Maximum watcher duration in ms |
266
+ | --plugin-uiwatchers-max-cache-entries | integer | 10-200 | 50 | Maximum cached element references |
267
+ | --plugin-uiwatchers-element-ttl-ms | integer | 5000-300000 | 60000 | Cache entry TTL in ms |
268
+
269
+ **Example**
270
+
271
+ ```bash
272
+ appium --use-plugins=uiwatchers \
273
+ --plugin-uiwatchers-max-watchers=10 \
274
+ --plugin-uiwatchers-max-duration-ms=120000 \
275
+ --plugin-uiwatchers-max-cache-entries=100 \
276
+ --plugin-uiwatchers-element-ttl-ms=30000
277
+ ```
278
+
279
+ ## How It Works
280
+
281
+ ### Watcher Checking Flow
282
+
283
+ ```mermaid
284
+ flowchart TD
285
+ A[findElement / findElements] --> B{Command Success?}
286
+ B -->|Yes| C[Cache element reference]
287
+ C --> D[Return result]
288
+ B -->|No / Empty| E[Check watchers by priority]
289
+ E --> F{Reference element found?}
290
+ F -->|No| G{More watchers?}
291
+ G -->|Yes| E
292
+ F -->|Yes| H[Click action element]
293
+ H --> I[Apply cooldown]
294
+ I --> G
295
+ G -->|No| J[Retry original command]
296
+ J --> D
297
+ ```
298
+
299
+ ### Stale Element Recovery Flow
300
+
301
+ ```mermaid
302
+ flowchart TD
303
+ A[Element action: click, getText, etc.] --> B{StaleElementReferenceException?}
304
+ B -->|No| C[Return result]
305
+ B -->|Yes| D[Check watchers]
306
+ D --> E{Watcher triggered?}
307
+ E -->|No| F[Throw original exception]
308
+ E -->|Yes| G[Lookup cached locator]
309
+ G --> H{Found in cache?}
310
+ H -->|No| F
311
+ H -->|Yes| I[Re-find element using locator]
312
+ I --> J[Map old ID → new ID]
313
+ J --> K[Retry action with new element]
314
+ K --> C
315
+ ```
316
+
317
+ ## Development
318
+
319
+ ### Prerequisites
320
+
321
+ - Node.js 18+
322
+ - Appium 3.x
323
+
324
+ ### Setup
325
+
326
+ ```bash
327
+ # Clone the repository
328
+ git clone https://github.com/rajvinodh/appium-uiwatchers-plugin.git
329
+ cd appium-uiwatchers-plugin
330
+
331
+ # Install dependencies
332
+ npm install
333
+
334
+ # Build
335
+ npm run build
336
+ ```
337
+
338
+ ### Scripts
339
+
340
+ | Script | Description |
341
+ | ----------------------- | ---------------------------------- |
342
+ | `npm run build` | Compile TypeScript to JavaScript |
343
+ | `npm run test` | Run all tests (unit + integration) |
344
+ | `npm run test:unit` | Run unit tests only |
345
+ | `npm run test:coverage` | Run tests with coverage report |
346
+ | `npm run lint` | Run ESLint and Prettier checks |
347
+ | `npm run lint:fix` | Auto-fix linting issues |
348
+
349
+ ### Project Structure
350
+
351
+ ```
352
+ ├── src/ # TypeScript source code
353
+ │ ├── plugin.ts # Main plugin class
354
+ │ ├── watcher-store.ts # Watcher state management
355
+ │ ├── watcher-checker.ts# Watcher execution logic
356
+ │ ├── element-cache.ts # Element reference caching
357
+ │ ├── commands/ # Command implementations
358
+ │ ├── config.ts # Configuration defaults
359
+ │ ├── types.ts # Type definitions
360
+ │ └── utils.ts # Utility functions
361
+ ├── test/
362
+ │ ├── unit/ # Unit tests
363
+ │ └── integration/ # Integration tests
364
+ └── lib/ # Compiled output
365
+ ```
366
+
367
+ ### Contributing
368
+
369
+ 1. Fork the repository
370
+ 2. Create a feature branch (`git checkout -b feature/my-feature`)
371
+ 3. Make your changes
372
+ 4. Run tests (`npm run test`)
373
+ 5. Run linting (`npm run lint`)
374
+ 6. Commit your changes
375
+ 7. Push to your branch
376
+ 8. Open a Pull Request
@@ -0,0 +1,65 @@
1
+ import js from '@eslint/js';
2
+ import tseslint from '@typescript-eslint/eslint-plugin';
3
+ import tsparser from '@typescript-eslint/parser';
4
+ import prettier from 'eslint-config-prettier';
5
+
6
+ export default [
7
+ js.configs.recommended,
8
+ {
9
+ files: ['src/**/*.ts', 'test/**/*.js'],
10
+ languageOptions: {
11
+ parser: tsparser,
12
+ parserOptions: {
13
+ ecmaVersion: 2020,
14
+ sourceType: 'module',
15
+ },
16
+ globals: {
17
+ console: 'readonly',
18
+ process: 'readonly',
19
+ __dirname: 'readonly',
20
+ __filename: 'readonly',
21
+ Buffer: 'readonly',
22
+ module: 'readonly',
23
+ require: 'readonly',
24
+ exports: 'readonly',
25
+ },
26
+ },
27
+ plugins: {
28
+ '@typescript-eslint': tseslint,
29
+ },
30
+ rules: {
31
+ ...tseslint.configs.recommended.rules,
32
+ '@typescript-eslint/no-unused-vars': [
33
+ 'error',
34
+ {
35
+ argsIgnorePattern: '^_',
36
+ varsIgnorePattern: '^_',
37
+ },
38
+ ],
39
+ '@typescript-eslint/explicit-module-boundary-types': 'off',
40
+ '@typescript-eslint/no-explicit-any': 'warn',
41
+ 'no-console': 'warn',
42
+ 'prefer-const': 'error',
43
+ 'no-var': 'error',
44
+ },
45
+ },
46
+ {
47
+ files: ['test/**/*.js', 'test/**/*.cjs'],
48
+ languageOptions: {
49
+ globals: {
50
+ describe: 'readonly',
51
+ it: 'readonly',
52
+ before: 'readonly',
53
+ after: 'readonly',
54
+ beforeEach: 'readonly',
55
+ afterEach: 'readonly',
56
+ setTimeout: 'readonly',
57
+ },
58
+ },
59
+ rules: {
60
+ '@typescript-eslint/no-unused-expressions': 'off',
61
+ 'no-undef': 'off',
62
+ },
63
+ },
64
+ prettier,
65
+ ];
package/package.json ADDED
@@ -0,0 +1,114 @@
1
+ {
2
+ "name": "appium-uiwatchers-plugin",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "main": "./lib/plugin.js",
6
+ "types": "./lib/plugin.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "watch": "tsc --watch",
10
+ "clean": "rm -rf lib coverage",
11
+ "prepublishOnly": "npm run build",
12
+ "test": "npm run test:unit && npm run test:integration",
13
+ "test:unit": "mocha --config .mocharc.json \"test/unit/**/*.spec.js\"",
14
+ "test:integration": "mocha --config .mocharc.json \"test/integration/**/*.spec.js\"",
15
+ "test:e2e": "mocha --config .mocharc.json \"test/e2e/**/*.e2e.spec.cjs\"",
16
+ "test:coverage": "c8 npm run test",
17
+ "test:watch": "mocha --config .mocharc.json --watch",
18
+ "lint": "eslint src test && prettier --check .",
19
+ "lint:fix": "eslint src test --fix && prettier --write .",
20
+ "format": "prettier --write .",
21
+ "prepare": "husky",
22
+ "install:local": "npm run build && appium plugin install --source=local ."
23
+ },
24
+ "keywords": [
25
+ "appium",
26
+ "appium-plugin",
27
+ "automation",
28
+ "testing",
29
+ "mobile",
30
+ "ui-watchers",
31
+ "popup-handler"
32
+ ],
33
+ "author": "Vinodh Raj R",
34
+ "license": "MIT",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/rajvinodh/uiwatchers.git"
38
+ },
39
+ "bugs": {
40
+ "url": "https://github.com/rajvinodh/uiwatchers/issues"
41
+ },
42
+ "homepage": "https://github.com/rajvinodh/uiwatchers#readme",
43
+ "description": "Appium plugin to automatically handle unexpected UI elements (popups, banners, dialogs) during test execution",
44
+ "peerDependencies": {
45
+ "appium": "^3.1.2"
46
+ },
47
+ "appium": {
48
+ "pluginName": "uiwatchers",
49
+ "mainClass": "UIWatchersPlugin",
50
+ "schema": {
51
+ "$schema": "http://json-schema.org/draft-07/schema",
52
+ "type": "object",
53
+ "properties": {
54
+ "max-watchers": {
55
+ "type": "integer",
56
+ "minimum": 1,
57
+ "maximum": 20,
58
+ "default": 5,
59
+ "description": "Maximum number of UI watchers allowed per session"
60
+ },
61
+ "max-duration-ms": {
62
+ "type": "integer",
63
+ "minimum": 1000,
64
+ "maximum": 600000,
65
+ "default": 60000,
66
+ "description": "Maximum duration for each UI watcher in milliseconds"
67
+ },
68
+ "max-cache-entries": {
69
+ "type": "integer",
70
+ "minimum": 1,
71
+ "maximum": 200,
72
+ "default": 50,
73
+ "description": "Maximum element references to cache for stale element recovery"
74
+ },
75
+ "element-ttl-ms": {
76
+ "type": "integer",
77
+ "minimum": 5000,
78
+ "maximum": 300000,
79
+ "default": 60000,
80
+ "description": "TTL for cached element references in milliseconds"
81
+ }
82
+ }
83
+ }
84
+ },
85
+ "dependencies": {
86
+ "@appium/base-plugin": "^3.0.5",
87
+ "@appium/support": "^7.0.4"
88
+ },
89
+ "devDependencies": {
90
+ "@appium/fake-driver": "^6.0.1",
91
+ "@appium/plugin-test-support": "^1.0.4",
92
+ "@appium/test-support": "^4.0.4",
93
+ "@types/chai": "^5.2.3",
94
+ "@types/chai-as-promised": "^8.0.2",
95
+ "@types/mocha": "^10.0.10",
96
+ "@types/node": "^25.0.3",
97
+ "@types/sinon": "^21.0.0",
98
+ "@typescript-eslint/eslint-plugin": "^8.51.0",
99
+ "@typescript-eslint/parser": "^8.51.0",
100
+ "appium-inspector-plugin": "^2025.11.1",
101
+ "c8": "^10.1.3",
102
+ "chai": "^6.2.2",
103
+ "chai-as-promised": "^8.0.2",
104
+ "eslint": "^9.39.2",
105
+ "eslint-config-prettier": "^10.1.8",
106
+ "husky": "^9.1.7",
107
+ "lint-staged": "^16.2.7",
108
+ "mocha": "^11.7.5",
109
+ "prettier": "^3.7.4",
110
+ "sinon": "^21.0.1",
111
+ "typescript": "^5.9.3",
112
+ "webdriverio": "^9.22.0"
113
+ }
114
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * clearAllUIWatchers command implementation
3
+ */
4
+
5
+ import { logger } from '@appium/support';
6
+ import type { WatcherStore } from '../watcher-store.js';
7
+ import type { ClearAllWatchersResult } from '../types.js';
8
+
9
+ const log = logger.getLogger('AppiumUIWatchers');
10
+
11
+ /**
12
+ * Clear all UI watchers from the session
13
+ * @param store - Watcher store instance
14
+ * @returns Clear result with count of removed watchers
15
+ */
16
+ export async function clearAllUIWatchers(store: WatcherStore): Promise<ClearAllWatchersResult> {
17
+ // Get count before clearing
18
+ const count = store.clear();
19
+
20
+ // Log successful clear
21
+ log.info(`[UIWatchers] All UI watchers cleared (removed ${count} watchers)`);
22
+
23
+ // Return success response
24
+ return {
25
+ success: true,
26
+ removedCount: count,
27
+ };
28
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * listUIWatchers command implementation
3
+ */
4
+
5
+ import type { WatcherStore } from '../watcher-store.js';
6
+ import type { ListWatchersResult } from '../types.js';
7
+
8
+ /**
9
+ * List all active UI watchers with their state
10
+ * @param store - Watcher store instance
11
+ * @returns List of all active watchers
12
+ */
13
+ export async function listUIWatchers(store: WatcherStore): Promise<ListWatchersResult> {
14
+ // Get all active watchers (this automatically filters out expired ones)
15
+ const activeWatchers = store.getActiveWatchers();
16
+
17
+ // Return success response
18
+ return {
19
+ success: true,
20
+ watchers: activeWatchers,
21
+ totalCount: activeWatchers.length,
22
+ };
23
+ }