@web-auto/camo 0.1.3 → 0.1.4

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 +137 -0
  2. package/package.json +2 -1
  3. package/scripts/check-file-size.mjs +80 -0
  4. package/scripts/file-size-policy.json +8 -0
  5. package/src/autoscript/action-providers/index.mjs +9 -0
  6. package/src/autoscript/action-providers/xhs/comments.mjs +412 -0
  7. package/src/autoscript/action-providers/xhs/common.mjs +77 -0
  8. package/src/autoscript/action-providers/xhs/detail.mjs +181 -0
  9. package/src/autoscript/action-providers/xhs/interaction.mjs +466 -0
  10. package/src/autoscript/action-providers/xhs/like-rules.mjs +57 -0
  11. package/src/autoscript/action-providers/xhs/persistence.mjs +167 -0
  12. package/src/autoscript/action-providers/xhs/search.mjs +174 -0
  13. package/src/autoscript/action-providers/xhs.mjs +133 -0
  14. package/src/autoscript/impact-engine.mjs +78 -0
  15. package/src/autoscript/runtime.mjs +1015 -0
  16. package/src/autoscript/schema.mjs +370 -0
  17. package/src/autoscript/xhs-unified-template.mjs +931 -0
  18. package/src/cli.mjs +185 -79
  19. package/src/commands/autoscript.mjs +1100 -0
  20. package/src/commands/browser.mjs +20 -4
  21. package/src/commands/container.mjs +298 -75
  22. package/src/commands/events.mjs +152 -0
  23. package/src/commands/lifecycle.mjs +17 -3
  24. package/src/commands/window.mjs +32 -1
  25. package/src/container/change-notifier.mjs +165 -24
  26. package/src/container/element-filter.mjs +51 -5
  27. package/src/container/runtime-core/checkpoint.mjs +195 -0
  28. package/src/container/runtime-core/index.mjs +21 -0
  29. package/src/container/runtime-core/operations/index.mjs +351 -0
  30. package/src/container/runtime-core/operations/selector-scripts.mjs +68 -0
  31. package/src/container/runtime-core/operations/tab-pool.mjs +544 -0
  32. package/src/container/runtime-core/operations/viewport.mjs +143 -0
  33. package/src/container/runtime-core/subscription.mjs +87 -0
  34. package/src/container/runtime-core/utils.mjs +94 -0
  35. package/src/container/runtime-core/validation.mjs +127 -0
  36. package/src/container/runtime-core.mjs +1 -0
  37. package/src/container/subscription-registry.mjs +459 -0
  38. package/src/core/actions.mjs +573 -0
  39. package/src/core/browser.mjs +270 -0
  40. package/src/core/index.mjs +53 -0
  41. package/src/core/utils.mjs +87 -0
  42. package/src/events/daemon-entry.mjs +33 -0
  43. package/src/events/daemon.mjs +80 -0
  44. package/src/events/progress-log.mjs +109 -0
  45. package/src/events/ws-server.mjs +239 -0
  46. package/src/lib/client.mjs +8 -5
  47. package/src/lifecycle/session-registry.mjs +8 -4
  48. package/src/lifecycle/session-watchdog.mjs +220 -0
  49. package/src/utils/browser-service.mjs +232 -9
  50. package/src/utils/help.mjs +26 -3
@@ -4,6 +4,7 @@ import { callAPI, ensureCamoufox, ensureBrowserService, getSessionByProfile, che
4
4
  import { resolveProfileId, ensureUrlScheme, looksLikeUrlToken, getPositionals } from '../utils/args.mjs';
5
5
  import { acquireLock, releaseLock, isLocked, getLockInfo, cleanupStaleLocks } from '../lifecycle/lock.mjs';
6
6
  import { registerSession, updateSession, getSessionInfo, unregisterSession, listRegisteredSessions, markSessionClosed, cleanupStaleSessions, recoverSession } from '../lifecycle/session-registry.mjs';
7
+ import { startSessionWatchdog, stopAllSessionWatchdogs, stopSessionWatchdog } from '../lifecycle/session-watchdog.mjs';
7
8
 
8
9
  export async function handleStartCommand(args) {
9
10
  ensureCamoufox();
@@ -56,6 +57,7 @@ export async function handleStartCommand(args) {
56
57
  message: 'Session already running',
57
58
  url: existing.current_url,
58
59
  }, null, 2));
60
+ startSessionWatchdog(profileId);
59
61
  return;
60
62
  }
61
63
 
@@ -84,6 +86,7 @@ export async function handleStartCommand(args) {
84
86
  url: targetUrl,
85
87
  headless,
86
88
  });
89
+ startSessionWatchdog(profileId);
87
90
  }
88
91
  console.log(JSON.stringify(result, null, 2));
89
92
  }
@@ -92,10 +95,20 @@ export async function handleStopCommand(args) {
92
95
  await ensureBrowserService();
93
96
  const profileId = resolveProfileId(args, 1, getDefaultProfile);
94
97
  if (!profileId) throw new Error('Usage: camo stop [profileId]');
95
-
96
- const result = await callAPI('stop', { profileId });
97
- releaseLock(profileId);
98
- markSessionClosed(profileId);
98
+
99
+ let result = null;
100
+ let stopError = null;
101
+ try {
102
+ result = await callAPI('stop', { profileId });
103
+ } catch (err) {
104
+ stopError = err;
105
+ } finally {
106
+ stopSessionWatchdog(profileId);
107
+ releaseLock(profileId);
108
+ markSessionClosed(profileId);
109
+ }
110
+
111
+ if (stopError) throw stopError;
99
112
  console.log(JSON.stringify(result, null, 2));
100
113
  }
101
114
 
@@ -405,6 +418,7 @@ export async function handleShutdownCommand() {
405
418
  } catch {
406
419
  // Best effort cleanup
407
420
  }
421
+ stopSessionWatchdog(session.profileId);
408
422
  releaseLock(session.profileId);
409
423
  markSessionClosed(session.profileId);
410
424
  }
@@ -413,10 +427,12 @@ export async function handleShutdownCommand() {
413
427
  const registered = listRegisteredSessions();
414
428
  for (const reg of registered) {
415
429
  if (reg.status !== 'closed') {
430
+ stopSessionWatchdog(reg.profileId);
416
431
  markSessionClosed(reg.profileId);
417
432
  releaseLock(reg.profileId);
418
433
  }
419
434
  }
435
+ stopAllSessionWatchdogs();
420
436
 
421
437
  const result = await callAPI('service:shutdown', {});
422
438
  console.log(JSON.stringify(result, null, 2));
@@ -1,124 +1,328 @@
1
- // Container commands - filter and watch elements
2
- import { resolveProfileId, getPositionals } from '../utils/args.mjs';
3
- import { callAPI, getSessionByProfile } from '../utils/browser-service.mjs';
1
+ // Container commands - filter, watch, and subscription targets
2
+ import { getDomSnapshotByProfile, getSessionByProfile, getViewportByProfile } from '../utils/browser-service.mjs';
4
3
  import { getDefaultProfile } from '../utils/config.mjs';
5
4
  import { getChangeNotifier } from '../container/change-notifier.mjs';
6
5
  import { createElementFilter } from '../container/element-filter.mjs';
6
+ import {
7
+ getRegisteredTargets,
8
+ initContainerSubscriptionDirectory,
9
+ listSubscriptionSets,
10
+ registerSubscriptionTargets,
11
+ } from '../container/subscription-registry.mjs';
12
+ import { safeAppendProgressEvent } from '../events/progress-log.mjs';
7
13
 
8
14
  const notifier = getChangeNotifier();
9
15
  const elementFilter = createElementFilter();
10
16
 
11
- export async function handleContainerFilterCommand(args) {
12
- const profileId = resolveProfileId(args);
17
+ const VALUE_FLAGS = new Set([
18
+ '--profile',
19
+ '-p',
20
+ '--selector',
21
+ '-s',
22
+ '--throttle',
23
+ '-t',
24
+ '--source',
25
+ '--site',
26
+ ]);
27
+
28
+ function readFlagValue(args, names) {
29
+ for (let i = 0; i < args.length; i += 1) {
30
+ if (!names.includes(args[i])) continue;
31
+ const value = args[i + 1];
32
+ if (!value || String(value).startsWith('-')) return null;
33
+ return value;
34
+ }
35
+ return null;
36
+ }
37
+
38
+ function collectPositionals(args, startIndex = 2) {
39
+ const out = [];
40
+ for (let i = startIndex; i < args.length; i += 1) {
41
+ const arg = args[i];
42
+ if (!arg) continue;
43
+ if (VALUE_FLAGS.has(arg)) {
44
+ i += 1;
45
+ continue;
46
+ }
47
+ if (String(arg).startsWith('-')) continue;
48
+ out.push(arg);
49
+ }
50
+ return out;
51
+ }
52
+
53
+ async function ensureSession(profileId) {
13
54
  const session = await getSessionByProfile(profileId);
14
55
  if (!session) {
15
56
  throw new Error(`No active session for profile: ${profileId || 'default'}`);
16
57
  }
58
+ return session;
59
+ }
17
60
 
18
- const selectors = [];
19
- for (let i = 1; i < args.length; i++) {
20
- const arg = args[i];
21
- if (arg === '--profile' || arg === '-p') { i++; continue; }
22
- if (arg.startsWith('--')) continue;
23
- selectors.push(arg);
61
+ function resolveWatchProfileAndSelectors(args) {
62
+ const explicitProfile = readFlagValue(args, ['--profile', '-p']);
63
+ const explicitSelector = readFlagValue(args, ['--selector', '-s']);
64
+ const positionals = collectPositionals(args);
65
+
66
+ if (explicitSelector) {
67
+ const profileId = explicitProfile || positionals[0] || getDefaultProfile();
68
+ return { profileId, selectors: [explicitSelector], source: 'manual' };
69
+ }
70
+
71
+ if (explicitProfile) {
72
+ return {
73
+ profileId: explicitProfile,
74
+ selectors: positionals,
75
+ source: positionals.length > 0 ? 'manual' : 'subscription',
76
+ };
77
+ }
78
+
79
+ if (positionals.length >= 2) {
80
+ return { profileId: positionals[0], selectors: positionals.slice(1), source: 'manual' };
81
+ }
82
+
83
+ if (positionals.length === 1) {
84
+ const candidateProfile = getRegisteredTargets(positionals[0])?.profile;
85
+ if (candidateProfile) {
86
+ return { profileId: positionals[0], selectors: [], source: 'subscription' };
87
+ }
88
+ return { profileId: getDefaultProfile(), selectors: [positionals[0]], source: 'manual' };
89
+ }
90
+
91
+ return { profileId: getDefaultProfile(), selectors: [], source: 'subscription' };
92
+ }
93
+
94
+ function resolveProfileAndSelectors(args) {
95
+ const explicitProfile = readFlagValue(args, ['--profile', '-p']);
96
+ const positionals = collectPositionals(args);
97
+ if (explicitProfile) {
98
+ return { profileId: explicitProfile, selectors: positionals };
99
+ }
100
+ if (positionals.length >= 2) {
101
+ return { profileId: positionals[0], selectors: positionals.slice(1) };
102
+ }
103
+ return { profileId: getDefaultProfile(), selectors: positionals };
104
+ }
105
+
106
+ function resolveListProfile(args) {
107
+ const explicitProfile = readFlagValue(args, ['--profile', '-p']);
108
+ if (explicitProfile) return explicitProfile;
109
+ const positionals = collectPositionals(args);
110
+ return positionals[0] || getDefaultProfile();
111
+ }
112
+
113
+ export async function handleContainerInitCommand(args) {
114
+ const source = readFlagValue(args, ['--source']);
115
+ const force = args.includes('--force');
116
+ const result = initContainerSubscriptionDirectory({
117
+ ...(source ? { containerLibraryRoot: source } : {}),
118
+ force,
119
+ });
120
+ console.log(JSON.stringify(result, null, 2));
121
+ }
122
+
123
+ export async function handleContainerSetsCommand(args) {
124
+ const site = readFlagValue(args, ['--site']);
125
+ const result = listSubscriptionSets({ ...(site ? { site } : {}) });
126
+ console.log(JSON.stringify(result, null, 2));
127
+ }
128
+
129
+ export async function handleContainerRegisterCommand(args) {
130
+ const explicitProfile = readFlagValue(args, ['--profile', '-p']);
131
+ const append = args.includes('--append');
132
+ const positionals = collectPositionals(args);
133
+
134
+ let profileId;
135
+ let setIds;
136
+ if (explicitProfile) {
137
+ profileId = explicitProfile;
138
+ setIds = positionals;
139
+ } else if (positionals.length >= 2) {
140
+ profileId = positionals[0];
141
+ setIds = positionals.slice(1);
142
+ } else {
143
+ profileId = getDefaultProfile();
144
+ setIds = positionals;
145
+ }
146
+
147
+ if (!profileId) {
148
+ throw new Error('Usage: camo container register [profileId] <setId...> [--append] [--profile <id>]');
149
+ }
150
+ if (!Array.isArray(setIds) || setIds.length === 0) {
151
+ throw new Error('Usage: camo container register [profileId] <setId...> [--append] [--profile <id>]');
24
152
  }
25
153
 
154
+ const result = registerSubscriptionTargets(profileId, setIds, { append });
155
+ console.log(JSON.stringify(result, null, 2));
156
+ }
157
+
158
+ export async function handleContainerTargetsCommand(args) {
159
+ const explicitProfile = readFlagValue(args, ['--profile', '-p']);
160
+ const positionals = collectPositionals(args);
161
+ const profileId = explicitProfile || positionals[0] || null;
162
+ const result = getRegisteredTargets(profileId);
163
+ console.log(JSON.stringify(result, null, 2));
164
+ }
165
+
166
+ export async function handleContainerFilterCommand(args) {
167
+ const { profileId, selectors } = resolveProfileAndSelectors(args);
168
+ if (!profileId) {
169
+ throw new Error('Usage: camo container filter [profileId] <selector...> [--profile <id>]');
170
+ }
26
171
  if (selectors.length === 0) {
27
- throw new Error('Usage: camo container filter <selector> [--profile <id>]');
172
+ throw new Error('Usage: camo container filter [profileId] <selector...> [--profile <id>]');
28
173
  }
29
174
 
30
- // Get DOM snapshot from browser service
31
- const result = await callAPI(`/session/${session.session_id}/dom-tree`, { method: 'POST' });
32
- const snapshot = result.dom_tree || result;
175
+ const session = await ensureSession(profileId);
176
+ const snapshot = await getDomSnapshotByProfile(session.profileId || profileId);
33
177
 
34
- // Filter elements
35
178
  const matched = [];
36
179
  for (const selector of selectors) {
37
180
  const elements = notifier.findElements(snapshot, { css: selector });
38
- matched.push(...elements.map(e => ({
39
- path: e.path,
40
- tag: e.tag,
41
- id: e.id,
42
- classes: e.classes,
43
- text: (e.textSnippet || e.text || '').slice(0, 50),
44
- })));
181
+ matched.push(
182
+ ...elements.map((element) => ({
183
+ selector,
184
+ path: element.path,
185
+ tag: element.tag,
186
+ id: element.id,
187
+ classes: element.classes,
188
+ text: (element.textSnippet || element.text || '').slice(0, 80),
189
+ })),
190
+ );
45
191
  }
46
192
 
47
- console.log(JSON.stringify({ ok: true, count: matched.length, elements: matched }, null, 2));
193
+ console.log(JSON.stringify({ ok: true, profileId: session.profileId || profileId, count: matched.length, elements: matched }, null, 2));
48
194
  }
49
195
 
50
196
  export async function handleContainerWatchCommand(args) {
51
- const profileId = resolveProfileId(args);
52
- const session = await getSessionByProfile(profileId);
53
- if (!session) {
54
- throw new Error(`No active session for profile: ${profileId || 'default'}`);
197
+ const watchRequest = resolveWatchProfileAndSelectors(args);
198
+ const profileId = watchRequest.profileId;
199
+ if (!profileId) {
200
+ throw new Error(
201
+ 'Usage: camo container watch [profileId] [--selector <css>|<selector...>] [--throttle ms] [--profile <id>]',
202
+ );
55
203
  }
56
204
 
57
- const positionalArgs = getPositionals(args, ['--profile', '-p', '--selector', '-s', '--throttle', '-t']);
58
-
59
- const selectorIdx = args.indexOf('--selector') !== -1 ? args.indexOf('--selector') : args.indexOf('-s');
60
- const throttleIdx = args.indexOf('--throttle') !== -1 ? args.indexOf('--throttle') : args.indexOf('-t');
61
-
62
- const selector = selectorIdx >= 0 ? args[selectorIdx + 1] : positionalArgs[0];
63
- const throttle = throttleIdx >= 0 ? parseInt(args[throttleIdx + 1], 10) : 500;
205
+ let selectors = Array.from(
206
+ new Set(
207
+ (watchRequest.selectors || [])
208
+ .map((item) => String(item || '').trim())
209
+ .filter(Boolean),
210
+ ),
211
+ );
212
+ let source = watchRequest.source;
213
+
214
+ if (selectors.length === 0) {
215
+ const registered = getRegisteredTargets(profileId)?.profile;
216
+ selectors = Array.from(
217
+ new Set(
218
+ (registered?.selectors || [])
219
+ .map((item) => item?.css)
220
+ .filter((css) => typeof css === 'string' && css.trim())
221
+ .map((css) => css.trim()),
222
+ ),
223
+ );
224
+ source = 'subscription';
225
+ }
64
226
 
65
- if (!selector) {
66
- throw new Error('Usage: camo container watch --selector <css> [--throttle ms] [--profile <id>]');
227
+ if (selectors.length === 0) {
228
+ throw new Error(
229
+ `No selectors found for profile: ${profileId}. Use --selector <css> or run camo container register ${profileId} <setId...> first.`,
230
+ );
67
231
  }
68
232
 
69
- console.log(JSON.stringify({ ok: true, message: `Watching selector: ${selector}`, throttle }));
233
+ const session = await ensureSession(profileId);
234
+ const throttleRaw = readFlagValue(args, ['--throttle', '-t']);
235
+ const throttle = Math.max(100, Number(throttleRaw) || 500);
236
+
237
+ console.log(JSON.stringify({
238
+ ok: true,
239
+ message: `Watching ${selectors.length} selector(s) from ${source}`,
240
+ selectors,
241
+ profileId: session.profileId || profileId,
242
+ throttle,
243
+ }));
244
+ safeAppendProgressEvent({
245
+ source: 'container.watch',
246
+ mode: 'normal',
247
+ profileId: session.profileId || profileId,
248
+ event: 'container.watch.start',
249
+ payload: {
250
+ selectors,
251
+ throttle,
252
+ source,
253
+ },
254
+ });
70
255
 
71
- // Setup WebSocket connection for DOM updates
72
- // For now, poll the browser service
73
256
  const interval = setInterval(async () => {
74
257
  try {
75
- const result = await callAPI(`/session/${session.session_id}/dom-tree`, { method: 'POST' });
76
- const snapshot = result.dom_tree || result;
258
+ const snapshot = await getDomSnapshotByProfile(session.profileId || profileId);
77
259
  notifier.processSnapshot(snapshot);
78
260
  } catch (err) {
79
- console.error(JSON.stringify({ ok: false, error: err.message }));
261
+ console.error(JSON.stringify({ ok: false, error: err?.message || String(err) }));
80
262
  }
81
263
  }, throttle);
82
264
 
83
- // Watch the selector
84
- notifier.watch({ css: selector }, {
265
+ const watchers = selectors.map((selector) => notifier.watch({ css: selector }, {
85
266
  onAppear: (elements) => {
86
267
  console.log(JSON.stringify({ event: 'appear', selector, count: elements.length, elements }));
268
+ safeAppendProgressEvent({
269
+ source: 'container.watch',
270
+ mode: 'normal',
271
+ profileId: session.profileId || profileId,
272
+ event: 'container.appear',
273
+ payload: { selector, count: elements.length },
274
+ });
87
275
  },
88
276
  onDisappear: (elements) => {
89
277
  console.log(JSON.stringify({ event: 'disappear', selector, count: elements.length }));
278
+ safeAppendProgressEvent({
279
+ source: 'container.watch',
280
+ mode: 'normal',
281
+ profileId: session.profileId || profileId,
282
+ event: 'container.disappear',
283
+ payload: { selector, count: elements.length },
284
+ });
90
285
  },
91
286
  onChange: ({ appeared, disappeared }) => {
92
287
  console.log(JSON.stringify({ event: 'change', selector, appeared: appeared.length, disappeared: disappeared.length }));
288
+ safeAppendProgressEvent({
289
+ source: 'container.watch',
290
+ mode: 'normal',
291
+ profileId: session.profileId || profileId,
292
+ event: 'container.change',
293
+ payload: { selector, appeared: appeared.length, disappeared: disappeared.length },
294
+ });
93
295
  },
94
296
  throttle,
95
- });
297
+ }));
96
298
 
97
- // Keep process alive
98
- process.on('SIGINT', () => {
299
+ process.once('SIGINT', () => {
99
300
  clearInterval(interval);
301
+ for (const stopWatch of watchers) stopWatch();
100
302
  notifier.destroy();
101
303
  console.log(JSON.stringify({ ok: true, message: 'Watch stopped' }));
304
+ safeAppendProgressEvent({
305
+ source: 'container.watch',
306
+ mode: 'normal',
307
+ profileId: session.profileId || profileId,
308
+ event: 'container.watch.stop',
309
+ payload: { selectors },
310
+ });
102
311
  process.exit(0);
103
312
  });
104
313
  }
105
314
 
106
315
  export async function handleContainerListCommand(args) {
107
- const profileId = resolveProfileId(args);
108
- const session = await getSessionByProfile(profileId);
109
- if (!session) {
110
- throw new Error(`No active session for profile: ${profileId || 'default'}`);
316
+ const profileId = resolveListProfile(args);
317
+ if (!profileId) {
318
+ throw new Error('Usage: camo container list [profileId] [--profile <id>]');
111
319
  }
320
+ const session = await ensureSession(profileId);
112
321
 
113
- const result = await callAPI(`/session/${session.session_id}/dom-tree`, { method: 'POST' });
114
- const snapshot = result.dom_tree || result;
115
-
116
- // Get viewport info
117
- const viewportResult = await callAPI(`/session/${session.session_id}/viewport`);
118
- const viewport = viewportResult.viewport || { width: 1280, height: 720 };
322
+ const snapshot = await getDomSnapshotByProfile(session.profileId || profileId);
323
+ const viewport = await getViewportByProfile(session.profileId || profileId);
119
324
 
120
- // Collect all visible elements
121
- const collectElements = (node, path = 'root') => {
325
+ const collectElements = (node, domPath = 'root') => {
122
326
  const elements = [];
123
327
  if (!node) return elements;
124
328
 
@@ -126,10 +330,9 @@ export async function handleContainerListCommand(args) {
126
330
  if (rect && viewport) {
127
331
  const inViewport = elementFilter.isInViewport(rect, viewport);
128
332
  const visibilityRatio = elementFilter.getVisibilityRatio(rect, viewport);
129
-
130
333
  if (inViewport && visibilityRatio > 0.1) {
131
334
  elements.push({
132
- path,
335
+ path: domPath,
133
336
  tag: node.tag,
134
337
  id: node.id,
135
338
  classes: node.classes?.slice(0, 3),
@@ -139,9 +342,9 @@ export async function handleContainerListCommand(args) {
139
342
  }
140
343
  }
141
344
 
142
- if (node.children) {
143
- for (let i = 0; i < node.children.length; i++) {
144
- elements.push(...collectElements(node.children[i], `${path}/${i}`));
345
+ if (Array.isArray(node.children)) {
346
+ for (let i = 0; i < node.children.length; i += 1) {
347
+ elements.push(...collectElements(node.children[i], `${domPath}/${i}`));
145
348
  }
146
349
  }
147
350
 
@@ -149,30 +352,50 @@ export async function handleContainerListCommand(args) {
149
352
  };
150
353
 
151
354
  const elements = collectElements(snapshot);
152
- console.log(JSON.stringify({ ok: true, viewport, count: elements.length, elements: elements.slice(0, 50) }, null, 2));
355
+ console.log(JSON.stringify({
356
+ ok: true,
357
+ profileId: session.profileId || profileId,
358
+ viewport,
359
+ count: elements.length,
360
+ elements: elements.slice(0, 50),
361
+ }, null, 2));
153
362
  }
154
363
 
155
364
  export async function handleContainerCommand(args) {
156
365
  const sub = args[1];
157
366
 
158
367
  switch (sub) {
368
+ case 'init':
369
+ return handleContainerInitCommand(args);
370
+ case 'sets':
371
+ return handleContainerSetsCommand(args);
372
+ case 'register':
373
+ return handleContainerRegisterCommand(args);
374
+ case 'targets':
375
+ return handleContainerTargetsCommand(args);
159
376
  case 'filter':
160
- return handleContainerFilterCommand(args.slice(1));
377
+ return handleContainerFilterCommand(args);
161
378
  case 'watch':
162
- return handleContainerWatchCommand(args.slice(1));
379
+ return handleContainerWatchCommand(args);
163
380
  case 'list':
164
- return handleContainerListCommand(args.slice(1));
381
+ return handleContainerListCommand(args);
165
382
  default:
166
- console.log(`Usage: camo container <filter|watch|list> [options]
383
+ console.log(`Usage: camo container <init|sets|register|targets|filter|watch|list> [options]
167
384
 
168
385
  Commands:
169
- filter <selector> - Filter DOM elements by CSS selector
170
- watch --selector <css> - Watch for element changes (outputs JSON events)
171
- list - List all visible elements in viewport
386
+ init [--source <container-library-dir>] [--force] Initialize subscription directory and migrate container sets
387
+ sets [--site <siteKey>] List migrated subscription sets
388
+ register [profileId] <setId...> [--append] Register subscription targets for profile
389
+ targets [profileId] Show registered targets
390
+ filter [profileId] <selector...> Filter DOM elements by CSS selector
391
+ watch [profileId] [--selector <css>] [--throttle <ms>] Watch for element changes (defaults to registered selectors)
392
+ list [profileId] List visible elements in viewport
172
393
 
173
394
  Options:
174
- --profile, -p <id> - Profile to use
175
- --throttle, -t <ms> - Throttle interval for watch (default: 500)
395
+ --profile, -p <id> Profile to use
396
+ --selector, -s <css> Selector for watch
397
+ --throttle, -t <ms> Poll interval for watch (default: 500)
398
+ --site <siteKey> Filter set list by site
176
399
  `);
177
400
  }
178
401
  }