@doppelgangerdev/doppelganger 0.4.0 → 0.5.2

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 (77) hide show
  1. package/README.md +1 -1
  2. package/agent.js +446 -240
  3. package/dist/assets/index-BXRKDZ1_.css +1 -0
  4. package/dist/assets/index-Deb2QMGx.js +19 -0
  5. package/dist/captures/run_1769042882403_589_agent_1769042884446_initial.png +0 -0
  6. package/dist/captures/run_1769042882403_589_agent_1769042887058.png +0 -0
  7. package/dist/captures/run_1769042882403_589_agent_1769042888468.webm +0 -0
  8. package/dist/captures/run_1769043202318_943_agent_1769043206237.png +0 -0
  9. package/dist/captures/run_1769043202318_943_agent_1769043207415.webm +0 -0
  10. package/dist/captures/run_1769043449517_97_agent_1769043451350_initial.png +0 -0
  11. package/dist/captures/run_1769043449517_97_agent_1769043455038.png +0 -0
  12. package/dist/captures/run_1769043449517_97_agent_1769043456476.webm +0 -0
  13. package/dist/captures/run_1769043471164_239_agent_1769043472720_initial.png +0 -0
  14. package/dist/captures/run_1769043471164_239_agent_1769043474022.png +0 -0
  15. package/dist/captures/run_1769043471164_239_agent_1769043476419.png +0 -0
  16. package/dist/captures/run_1769043471164_239_agent_1769043477795.webm +0 -0
  17. package/dist/captures/run_1769080585290_151_agent_1769080595110.png +0 -0
  18. package/dist/captures/run_1769080585290_151_agent_1769080596335.webm +0 -0
  19. package/dist/index.html +3 -3
  20. package/dist/screenshots/agent_1769037343598.png +0 -0
  21. package/dist/screenshots/agent_1769037357541.png +0 -0
  22. package/dist/screenshots/scrape_1769037382254.png +0 -0
  23. package/dist/screenshots/scrape_1769037413189.png +0 -0
  24. package/dist/screenshots/scrape_1769037449707.png +0 -0
  25. package/dist/screenshots/scrape_1769037461756.png +0 -0
  26. package/dist/screenshots/scrape_1769037490581.png +0 -0
  27. package/dist/screenshots/scrape_1769038242368.png +0 -0
  28. package/headful.js +97 -42
  29. package/package.json +3 -1
  30. package/proxy-rotation.js +212 -154
  31. package/public/captures/run_1769042882403_589_agent_1769042884446_initial.png +0 -0
  32. package/public/captures/run_1769042882403_589_agent_1769042887058.png +0 -0
  33. package/public/captures/run_1769042882403_589_agent_1769042888468.webm +0 -0
  34. package/public/captures/run_1769043202318_943_agent_1769043206237.png +0 -0
  35. package/public/captures/run_1769043202318_943_agent_1769043207415.webm +0 -0
  36. package/public/captures/run_1769043449517_97_agent_1769043451350_initial.png +0 -0
  37. package/public/captures/run_1769043449517_97_agent_1769043455038.png +0 -0
  38. package/public/captures/run_1769043449517_97_agent_1769043456476.webm +0 -0
  39. package/public/captures/run_1769043471164_239_agent_1769043472720_initial.png +0 -0
  40. package/public/captures/run_1769043471164_239_agent_1769043474022.png +0 -0
  41. package/public/captures/run_1769043471164_239_agent_1769043476419.png +0 -0
  42. package/public/captures/run_1769043471164_239_agent_1769043477795.webm +0 -0
  43. package/public/captures/run_1769080585290_151_agent_1769080595110.png +0 -0
  44. package/public/captures/run_1769080585290_151_agent_1769080596335.webm +0 -0
  45. package/public/screenshots/agent_1769037343598.png +0 -0
  46. package/public/screenshots/agent_1769037357541.png +0 -0
  47. package/public/screenshots/scrape_1769037382254.png +0 -0
  48. package/public/screenshots/scrape_1769037413189.png +0 -0
  49. package/public/screenshots/scrape_1769037449707.png +0 -0
  50. package/public/screenshots/scrape_1769037461756.png +0 -0
  51. package/public/screenshots/scrape_1769037490581.png +0 -0
  52. package/public/screenshots/scrape_1769038242368.png +0 -0
  53. package/scrape.js +170 -73
  54. package/server.js +437 -335
  55. package/dist/assets/index-B7ntJ0pF.js +0 -18
  56. package/dist/assets/index-OOTrc7_f.css +0 -1
  57. package/dist/screenshots/agent_1768764625684.png +0 -0
  58. package/dist/screenshots/agent_1768765565909.png +0 -0
  59. package/dist/screenshots/agent_1768765581979.png +0 -0
  60. package/dist/screenshots/scrape_1768764416615.png +0 -0
  61. package/dist/screenshots/scrape_1768764452033.png +0 -0
  62. package/dist/screenshots/scrape_1768765064688.png +0 -0
  63. package/dist/screenshots/scrape_1768765090587.png +0 -0
  64. package/dist/screenshots/scrape_1768765100798.png +0 -0
  65. package/dist/screenshots/scrape_1768765114782.png +0 -0
  66. package/dist/screenshots/scrape_1768765127463.png +0 -0
  67. package/public/screenshots/agent_1768764625684.png +0 -0
  68. package/public/screenshots/agent_1768765565909.png +0 -0
  69. package/public/screenshots/agent_1768765581979.png +0 -0
  70. package/public/screenshots/scrape_1768764416615.png +0 -0
  71. package/public/screenshots/scrape_1768764452033.png +0 -0
  72. package/public/screenshots/scrape_1768765064688.png +0 -0
  73. package/public/screenshots/scrape_1768765090587.png +0 -0
  74. package/public/screenshots/scrape_1768765100798.png +0 -0
  75. package/public/screenshots/scrape_1768765114782.png +0 -0
  76. package/public/screenshots/scrape_1768765127463.png +0 -0
  77. package/public/screenshots/scrape_1768769463201.png +0 -0
package/proxy-rotation.js CHANGED
@@ -1,74 +1,81 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const crypto = require('crypto');
4
-
5
- const DATA_PROXY_FILE = path.join(__dirname, 'data', 'proxies.json');
6
- const PROXY_FILES = [
7
- DATA_PROXY_FILE,
8
- path.join(__dirname, 'proxies.json')
9
- ];
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const crypto = require('crypto');
4
+
5
+ const DATA_PROXY_FILE = path.join(__dirname, 'data', 'proxies.json');
6
+ const PROXY_FILES = [
7
+ DATA_PROXY_FILE,
8
+ path.join(__dirname, 'proxies.json')
9
+ ];
10
+
11
+ const ROTATION_MODES = new Set(['round-robin', 'random']);
10
12
 
11
13
  let cached = {
12
14
  file: null,
13
15
  mtimeMs: 0,
14
- config: { proxies: [], defaultProxyId: null, includeDefaultInRotation: false }
16
+ config: { proxies: [], defaultProxyId: null, includeDefaultInRotation: false, rotationMode: 'round-robin' }
15
17
  };
16
18
  let rotationIndex = 0;
17
-
18
- const normalizeServer = (raw) => {
19
- if (!raw) return '';
20
- let server = String(raw).trim();
21
- if (!server) return '';
22
- if (!server.includes('://')) {
23
- server = `http://${server}`;
24
- }
25
- return server;
26
- };
27
-
28
- const createProxyId = (seed) => {
29
- const hash = crypto.createHash('sha1').update(String(seed)).digest('hex').slice(0, 12);
30
- return `proxy_${hash}`;
31
- };
32
-
33
- const normalizeProxy = (entry) => {
34
- if (!entry) return null;
35
- if (typeof entry === 'string') {
36
- let raw = entry.trim();
37
- if (!raw) return null;
38
- if (!raw.includes('://')) {
39
- raw = `http://${raw}`;
40
- }
41
- try {
42
- const parsed = new URL(raw);
43
- const server = `${parsed.protocol}//${parsed.host}`;
44
- const username = parsed.username ? decodeURIComponent(parsed.username) : undefined;
45
- const password = parsed.password ? decodeURIComponent(parsed.password) : undefined;
46
- return {
47
- id: createProxyId(`${server}|${username || ''}|${password || ''}`),
48
- server,
49
- username,
50
- password
51
- };
52
- } catch {
53
- return null;
54
- }
55
- }
56
- if (typeof entry === 'object') {
57
- const serverRaw = entry.server || entry.url || entry.proxy;
58
- const server = normalizeServer(serverRaw);
59
- if (!server) return null;
60
- const username = entry.username || entry.user;
61
- const password = entry.password || entry.pass;
62
- const id = entry.id || createProxyId(`${server}|${username || ''}|${password || ''}`);
63
- return {
64
- id,
65
- server,
66
- username,
67
- password,
68
- label: entry.label
69
- };
70
- }
71
- return null;
19
+
20
+ const normalizeServer = (raw) => {
21
+ if (!raw) return '';
22
+ let server = String(raw).trim();
23
+ if (!server) return '';
24
+ if (!server.includes('://')) {
25
+ server = `http://${server}`;
26
+ }
27
+ return server;
28
+ };
29
+
30
+ const createProxyId = (seed) => {
31
+ const hash = crypto.createHash('sha1').update(String(seed)).digest('hex').slice(0, 12);
32
+ return `proxy_${hash}`;
33
+ };
34
+
35
+ const normalizeProxy = (entry) => {
36
+ if (!entry) return null;
37
+ if (typeof entry === 'string') {
38
+ let raw = entry.trim();
39
+ if (!raw) return null;
40
+ if (!raw.includes('://')) {
41
+ raw = `http://${raw}`;
42
+ }
43
+ try {
44
+ const parsed = new URL(raw);
45
+ const server = `${parsed.protocol}//${parsed.host}`;
46
+ const username = parsed.username ? decodeURIComponent(parsed.username) : undefined;
47
+ const password = parsed.password ? decodeURIComponent(parsed.password) : undefined;
48
+ return {
49
+ id: createProxyId(`${server}|${username || ''}|${password || ''}`),
50
+ server,
51
+ username,
52
+ password
53
+ };
54
+ } catch {
55
+ return null;
56
+ }
57
+ }
58
+ if (typeof entry === 'object') {
59
+ const serverRaw = entry.server || entry.url || entry.proxy;
60
+ const server = normalizeServer(serverRaw);
61
+ if (!server) return null;
62
+ const username = entry.username || entry.user;
63
+ const password = entry.password || entry.pass;
64
+ const id = entry.id || createProxyId(`${server}|${username || ''}|${password || ''}`);
65
+ return {
66
+ id,
67
+ server,
68
+ username,
69
+ password,
70
+ label: entry.label
71
+ };
72
+ }
73
+ return null;
74
+ };
75
+
76
+ const normalizeRotationMode = (mode) => {
77
+ if (ROTATION_MODES.has(mode)) return mode;
78
+ return 'round-robin';
72
79
  };
73
80
 
74
81
  const loadProxyFile = (filePath) => {
@@ -76,51 +83,53 @@ const loadProxyFile = (filePath) => {
76
83
  const raw = fs.readFileSync(filePath, 'utf8');
77
84
  const parsed = JSON.parse(raw);
78
85
  if (Array.isArray(parsed)) {
79
- return { proxies: parsed, defaultProxyId: null, includeDefaultInRotation: false };
86
+ return { proxies: parsed, defaultProxyId: null, includeDefaultInRotation: false, rotationMode: 'round-robin' };
80
87
  }
81
88
  const proxies = Array.isArray(parsed.proxies) ? parsed.proxies : [];
82
89
  const defaultProxyId = parsed.defaultProxyId || null;
83
90
  const includeDefaultInRotation = !!parsed.includeDefaultInRotation;
84
- return { proxies, defaultProxyId, includeDefaultInRotation };
91
+ const rotationMode = normalizeRotationMode(parsed.rotationMode);
92
+ return { proxies, defaultProxyId, includeDefaultInRotation, rotationMode };
85
93
  } catch {
86
- return { proxies: [], defaultProxyId: null, includeDefaultInRotation: false };
94
+ return { proxies: [], defaultProxyId: null, includeDefaultInRotation: false, rotationMode: 'round-robin' };
87
95
  }
88
96
  };
89
-
90
- const loadProxyConfig = () => {
91
- const filePath = PROXY_FILES.find((candidate) => {
92
- try {
93
- return fs.existsSync(candidate);
94
- } catch {
95
- return false;
96
- }
97
- });
98
-
99
- if (!filePath) {
100
- cached = { file: null, mtimeMs: 0, config: { proxies: [], defaultProxyId: null, includeDefaultInRotation: false } };
97
+
98
+ const loadProxyConfig = () => {
99
+ const filePath = PROXY_FILES.find((candidate) => {
100
+ try {
101
+ return fs.existsSync(candidate);
102
+ } catch {
103
+ return false;
104
+ }
105
+ });
106
+
107
+ if (!filePath) {
108
+ cached = { file: null, mtimeMs: 0, config: { proxies: [], defaultProxyId: null, includeDefaultInRotation: false, rotationMode: 'round-robin' } };
101
109
  return cached.config;
102
110
  }
103
-
104
- try {
105
- const stat = fs.statSync(filePath);
106
- const mtimeMs = stat.mtimeMs || 0;
107
- if (cached.file === filePath && cached.mtimeMs === mtimeMs) {
108
- return cached.config;
109
- }
110
- const rawConfig = loadProxyFile(filePath);
111
- const proxies = rawConfig.proxies.map(normalizeProxy).filter(Boolean);
112
- const defaultProxyId = rawConfig.defaultProxyId && proxies.some((proxy) => proxy.id === rawConfig.defaultProxyId)
113
- ? rawConfig.defaultProxyId
114
- : null;
111
+
112
+ try {
113
+ const stat = fs.statSync(filePath);
114
+ const mtimeMs = stat.mtimeMs || 0;
115
+ if (cached.file === filePath && cached.mtimeMs === mtimeMs) {
116
+ return cached.config;
117
+ }
118
+ const rawConfig = loadProxyFile(filePath);
119
+ const proxies = rawConfig.proxies.map(normalizeProxy).filter(Boolean);
120
+ const defaultProxyId = rawConfig.defaultProxyId && proxies.some((proxy) => proxy.id === rawConfig.defaultProxyId)
121
+ ? rawConfig.defaultProxyId
122
+ : null;
115
123
  const config = {
116
124
  proxies,
117
125
  defaultProxyId,
118
- includeDefaultInRotation: !!rawConfig.includeDefaultInRotation
126
+ includeDefaultInRotation: !!rawConfig.includeDefaultInRotation,
127
+ rotationMode: normalizeRotationMode(rawConfig.rotationMode)
119
128
  };
120
129
  cached = { file: filePath, mtimeMs, config };
121
130
  return config;
122
131
  } catch {
123
- cached = { file: filePath, mtimeMs: 0, config: { proxies: [], defaultProxyId: null, includeDefaultInRotation: false } };
132
+ cached = { file: filePath, mtimeMs: 0, config: { proxies: [], defaultProxyId: null, includeDefaultInRotation: false, rotationMode: 'round-robin' } };
124
133
  return cached.config;
125
134
  }
126
135
  };
@@ -130,32 +139,34 @@ const saveProxyConfig = (config) => {
130
139
  const payload = {
131
140
  defaultProxyId: config.defaultProxyId || null,
132
141
  proxies: Array.isArray(config.proxies) ? config.proxies : [],
133
- includeDefaultInRotation: !!config.includeDefaultInRotation
134
- };
135
- fs.writeFileSync(target, JSON.stringify(payload, null, 2));
136
- try {
137
- const stat = fs.statSync(target);
138
- cached = { file: target, mtimeMs: stat.mtimeMs || 0, config: payload };
139
- } catch {
140
- cached = { file: target, mtimeMs: 0, config: payload };
141
- }
142
- return payload;
143
- };
144
-
145
- const listProxies = () => {
146
- const config = loadProxyConfig();
147
- const hostEntry = {
148
- id: 'host',
149
- server: 'host_ip',
150
- label: 'Host IP (no proxy)'
142
+ includeDefaultInRotation: !!config.includeDefaultInRotation,
143
+ rotationMode: normalizeRotationMode(config.rotationMode)
151
144
  };
145
+ fs.writeFileSync(target, JSON.stringify(payload, null, 2));
146
+ try {
147
+ const stat = fs.statSync(target);
148
+ cached = { file: target, mtimeMs: stat.mtimeMs || 0, config: payload };
149
+ } catch {
150
+ cached = { file: target, mtimeMs: 0, config: payload };
151
+ }
152
+ return payload;
153
+ };
154
+
155
+ const listProxies = () => {
156
+ const config = loadProxyConfig();
157
+ const hostEntry = {
158
+ id: 'host',
159
+ server: 'host_ip',
160
+ label: 'Host IP (no proxy)'
161
+ };
152
162
  return {
153
163
  proxies: [hostEntry, ...(config.proxies || [])],
154
164
  defaultProxyId: config.defaultProxyId || 'host',
155
- includeDefaultInRotation: !!config.includeDefaultInRotation
165
+ includeDefaultInRotation: !!config.includeDefaultInRotation,
166
+ rotationMode: normalizeRotationMode(config.rotationMode)
156
167
  };
157
168
  };
158
-
169
+
159
170
  const addProxy = (entry) => {
160
171
  const normalized = normalizeProxy(entry);
161
172
  if (!normalized) return null;
@@ -165,86 +176,133 @@ const addProxy = (entry) => {
165
176
  return saveProxyConfig(next);
166
177
  };
167
178
 
168
- const updateProxy = (id, entry) => {
169
- if (!id) return null;
170
- const normalized = normalizeProxy(entry);
171
- if (!normalized) return null;
179
+ const addProxies = (entries) => {
180
+ if (!Array.isArray(entries)) return null;
181
+ const normalizedEntries = entries.map(normalizeProxy).filter(Boolean);
182
+ if (normalizedEntries.length === 0) return null;
172
183
  const config = loadProxyConfig();
173
- const proxies = config.proxies.map((proxy) => {
174
- if (proxy.id !== id) return proxy;
175
- return { ...proxy, ...normalized, id };
184
+ const existingByServer = new Map(
185
+ config.proxies.map((proxy) => [String(proxy.server || '').toLowerCase(), proxy])
186
+ );
187
+ const seenServers = new Set();
188
+ const updates = [];
189
+ const additions = [];
190
+
191
+ normalizedEntries.forEach((proxy) => {
192
+ const serverKey = String(proxy.server || '').toLowerCase();
193
+ if (!serverKey || seenServers.has(serverKey)) return;
194
+ seenServers.add(serverKey);
195
+ const existing = existingByServer.get(serverKey);
196
+ if (existing) {
197
+ updates.push({ ...existing, ...proxy, id: existing.id });
198
+ } else {
199
+ additions.push({ ...proxy, id: `proxy_${crypto.randomBytes(6).toString('hex')}` });
200
+ }
176
201
  });
177
- if (!proxies.some((proxy) => proxy.id === id)) return null;
178
- return saveProxyConfig({ ...config, proxies });
179
- };
180
202
 
203
+ const merged = config.proxies.map((proxy) => {
204
+ const serverKey = String(proxy.server || '').toLowerCase();
205
+ const replacement = updates.find((item) => String(item.server || '').toLowerCase() === serverKey);
206
+ return replacement || proxy;
207
+ });
208
+
209
+ const proxies = [...merged, ...additions];
210
+ const next = { ...config, proxies };
211
+ return saveProxyConfig(next);
212
+ };
213
+
214
+ const updateProxy = (id, entry) => {
215
+ if (!id) return null;
216
+ const normalized = normalizeProxy(entry);
217
+ if (!normalized) return null;
218
+ const config = loadProxyConfig();
219
+ const proxies = config.proxies.map((proxy) => {
220
+ if (proxy.id !== id) return proxy;
221
+ return { ...proxy, ...normalized, id };
222
+ });
223
+ if (!proxies.some((proxy) => proxy.id === id)) return null;
224
+ return saveProxyConfig({ ...config, proxies });
225
+ };
226
+
181
227
  const deleteProxy = (id) => {
182
228
  if (!id) return null;
183
229
  const config = loadProxyConfig();
184
230
  const proxies = config.proxies.filter((proxy) => proxy.id !== id);
185
231
  const defaultProxyId = config.defaultProxyId === id ? null : config.defaultProxyId;
186
- return saveProxyConfig({ proxies, defaultProxyId });
232
+ return saveProxyConfig({ ...config, proxies, defaultProxyId });
187
233
  };
188
-
189
- const setDefaultProxy = (id) => {
234
+
235
+ const setDefaultProxy = (id) => {
236
+ const config = loadProxyConfig();
237
+ if (!id) {
238
+ return saveProxyConfig({ ...config, defaultProxyId: null });
239
+ }
240
+ if (!config.proxies.some((proxy) => proxy.id === id)) return null;
241
+ return saveProxyConfig({ ...config, defaultProxyId: id });
242
+ };
243
+
244
+ const setIncludeDefaultInRotation = (enabled) => {
190
245
  const config = loadProxyConfig();
191
- if (!id) {
192
- return saveProxyConfig({ ...config, defaultProxyId: null });
193
- }
194
- if (!config.proxies.some((proxy) => proxy.id === id)) return null;
195
- return saveProxyConfig({ ...config, defaultProxyId: id });
246
+ return saveProxyConfig({ ...config, includeDefaultInRotation: !!enabled });
196
247
  };
197
248
 
198
- const setIncludeDefaultInRotation = (enabled) => {
249
+ const setRotationMode = (mode) => {
199
250
  const config = loadProxyConfig();
200
- return saveProxyConfig({ ...config, includeDefaultInRotation: !!enabled });
251
+ return saveProxyConfig({ ...config, rotationMode: normalizeRotationMode(mode) });
201
252
  };
202
253
 
203
- const getNextProxy = (proxies) => {
254
+ const getNextProxy = (proxies, mode) => {
204
255
  if (!proxies.length) return null;
256
+ if (mode === 'random') {
257
+ const index = Math.floor(Math.random() * proxies.length);
258
+ return proxies[index];
259
+ }
205
260
  const selected = proxies[rotationIndex % proxies.length];
206
261
  rotationIndex += 1;
207
262
  return selected;
208
263
  };
209
-
210
- const getProxySelection = (rotateProxies) => {
211
- const config = loadProxyConfig();
212
- const proxies = config.proxies || [];
213
- const hostEntry = { id: 'host', server: 'host_ip', label: 'Host IP (no proxy)' };
214
- const pool = [hostEntry, ...proxies];
215
- const defaultProxy = config.defaultProxyId
216
- ? proxies.find((proxy) => proxy.id === config.defaultProxyId) || null
217
- : null;
264
+
265
+ const getProxySelection = (rotateProxies) => {
266
+ const config = loadProxyConfig();
267
+ const proxies = config.proxies || [];
268
+ const hostEntry = { id: 'host', server: 'host_ip', label: 'Host IP (no proxy)' };
269
+ const pool = [hostEntry, ...proxies];
270
+ const defaultProxy = config.defaultProxyId
271
+ ? proxies.find((proxy) => proxy.id === config.defaultProxyId) || null
272
+ : null;
218
273
  const defaultIsHost = !config.defaultProxyId;
219
274
  const includeDefaultInRotation = !!config.includeDefaultInRotation;
275
+ const rotationMode = normalizeRotationMode(config.rotationMode);
220
276
 
221
277
  if (rotateProxies) {
222
278
  let rotationPool = pool;
223
279
  if (!includeDefaultInRotation) {
224
280
  if (defaultIsHost) {
225
- rotationPool = pool.filter((proxy) => proxy.id !== 'host');
226
- } else {
281
+ rotationPool = pool.filter((proxy) => proxy.id !== 'host');
282
+ } else {
227
283
  rotationPool = pool.filter((proxy) => proxy.id !== config.defaultProxyId);
228
284
  }
229
285
  }
230
286
  if (rotationPool.length > 0) {
231
- const picked = getNextProxy(rotationPool);
287
+ const picked = getNextProxy(rotationPool, rotationMode);
232
288
  return { proxy: picked && picked.id !== 'host' ? picked : null, mode: 'rotate' };
233
289
  }
234
290
  if (defaultProxy) return { proxy: defaultProxy, mode: 'default' };
235
291
  return { proxy: null, mode: 'host' };
236
- }
237
-
238
- if (defaultProxy) return { proxy: defaultProxy, mode: 'default' };
239
- return { proxy: null, mode: 'host' };
240
- };
241
-
292
+ }
293
+
294
+ if (defaultProxy) return { proxy: defaultProxy, mode: 'default' };
295
+ return { proxy: null, mode: 'host' };
296
+ };
297
+
242
298
  module.exports = {
243
299
  getProxySelection,
244
300
  listProxies,
245
301
  addProxy,
302
+ addProxies,
246
303
  updateProxy,
247
304
  deleteProxy,
248
305
  setDefaultProxy,
249
- setIncludeDefaultInRotation
306
+ setIncludeDefaultInRotation,
307
+ setRotationMode
250
308
  };