@ariestools/aries-witness-cli 0.1.6

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 (80) hide show
  1. package/dist/bin/witness.mjs +19663 -0
  2. package/dist/node/actions/bank/capitalOne.d.ts +19 -0
  3. package/dist/node/actions/bank/capitalOne.d.ts.map +1 -0
  4. package/dist/node/actions/bank/capitalOneProducts.d.ts +15 -0
  5. package/dist/node/actions/bank/capitalOneProducts.d.ts.map +1 -0
  6. package/dist/node/actions/bank/config.d.ts +33 -0
  7. package/dist/node/actions/bank/config.d.ts.map +1 -0
  8. package/dist/node/actions/bank/index.d.ts +7 -0
  9. package/dist/node/actions/bank/index.d.ts.map +1 -0
  10. package/dist/node/actions/pentair/circuit.d.ts +16 -0
  11. package/dist/node/actions/pentair/circuit.d.ts.map +1 -0
  12. package/dist/node/actions/pentair/config.d.ts +32 -0
  13. package/dist/node/actions/pentair/config.d.ts.map +1 -0
  14. package/dist/node/actions/pentair/index.d.ts +6 -0
  15. package/dist/node/actions/pentair/index.d.ts.map +1 -0
  16. package/dist/node/actions/pentair/screenlogic.d.ts +4 -0
  17. package/dist/node/actions/pentair/screenlogic.d.ts.map +1 -0
  18. package/dist/node/actions/witness/app/index.d.ts +4 -0
  19. package/dist/node/actions/witness/app/index.d.ts.map +1 -0
  20. package/dist/node/actions/witness/app/scanApps.d.ts +3 -0
  21. package/dist/node/actions/witness/app/scanApps.d.ts.map +1 -0
  22. package/dist/node/actions/witness/app/types.d.ts +15 -0
  23. package/dist/node/actions/witness/app/types.d.ts.map +1 -0
  24. package/dist/node/actions/witness/discoverPlugins.d.ts +3 -0
  25. package/dist/node/actions/witness/discoverPlugins.d.ts.map +1 -0
  26. package/dist/node/actions/witness/executeWitness.d.ts +16 -0
  27. package/dist/node/actions/witness/executeWitness.d.ts.map +1 -0
  28. package/dist/node/actions/witness/formatOutput.d.ts +8 -0
  29. package/dist/node/actions/witness/formatOutput.d.ts.map +1 -0
  30. package/dist/node/actions/witness/index.d.ts +6 -0
  31. package/dist/node/actions/witness/index.d.ts.map +1 -0
  32. package/dist/node/actions/witness/registerWitnessCommand.d.ts +4 -0
  33. package/dist/node/actions/witness/registerWitnessCommand.d.ts.map +1 -0
  34. package/dist/node/actions/witness/witnesses/bank.d.ts +3 -0
  35. package/dist/node/actions/witness/witnesses/bank.d.ts.map +1 -0
  36. package/dist/node/actions/witness/witnesses/index.d.ts +11 -0
  37. package/dist/node/actions/witness/witnesses/index.d.ts.map +1 -0
  38. package/dist/node/actions/witness/witnesses/netflixCatalog.d.ts +3 -0
  39. package/dist/node/actions/witness/witnesses/netflixCatalog.d.ts.map +1 -0
  40. package/dist/node/actions/witness/witnesses/netflixCatalogBrowser.d.ts +19 -0
  41. package/dist/node/actions/witness/witnesses/netflixCatalogBrowser.d.ts.map +1 -0
  42. package/dist/node/actions/witness/witnesses/netflixCatalogDetail.d.ts +26 -0
  43. package/dist/node/actions/witness/witnesses/netflixCatalogDetail.d.ts.map +1 -0
  44. package/dist/node/actions/witness/witnesses/netflixCatalogExtract.d.ts +24 -0
  45. package/dist/node/actions/witness/witnesses/netflixCatalogExtract.d.ts.map +1 -0
  46. package/dist/node/actions/witness/witnesses/pentair.d.ts +3 -0
  47. package/dist/node/actions/witness/witnesses/pentair.d.ts.map +1 -0
  48. package/dist/node/actions/witness/witnesses/pentairIntellichem.d.ts +3 -0
  49. package/dist/node/actions/witness/witnesses/pentairIntellichem.d.ts.map +1 -0
  50. package/dist/node/actions/witness/witnesses/pentairIntellichemGraph.d.ts +10 -0
  51. package/dist/node/actions/witness/witnesses/pentairIntellichemGraph.d.ts.map +1 -0
  52. package/dist/node/actions/witness/witnesses/pentairIntellichemTypes.d.ts +20 -0
  53. package/dist/node/actions/witness/witnesses/pentairIntellichemTypes.d.ts.map +1 -0
  54. package/dist/node/actions/witness/witnesses/pentairSchedules.d.ts +82 -0
  55. package/dist/node/actions/witness/witnesses/pentairSchedules.d.ts.map +1 -0
  56. package/dist/node/actions/witness/witnesses/systemInfo.d.ts +3 -0
  57. package/dist/node/actions/witness/witnesses/systemInfo.d.ts.map +1 -0
  58. package/dist/node/actions/witness/witnesses/timestamp.d.ts +3 -0
  59. package/dist/node/actions/witness/witnesses/timestamp.d.ts.map +1 -0
  60. package/dist/node/actions/witness/witnesses/url.d.ts +3 -0
  61. package/dist/node/actions/witness/witnesses/url.d.ts.map +1 -0
  62. package/dist/node/bin/witness.d.ts +3 -0
  63. package/dist/node/bin/witness.d.ts.map +1 -0
  64. package/dist/node/commands/bankCommand.d.ts +3 -0
  65. package/dist/node/commands/bankCommand.d.ts.map +1 -0
  66. package/dist/node/commands/pentairCommand.d.ts +3 -0
  67. package/dist/node/commands/pentairCommand.d.ts.map +1 -0
  68. package/dist/node/commands/witnessAppCommand.d.ts +3 -0
  69. package/dist/node/commands/witnessAppCommand.d.ts.map +1 -0
  70. package/dist/node/commands/witnessAppListCommand.d.ts +3 -0
  71. package/dist/node/commands/witnessAppListCommand.d.ts.map +1 -0
  72. package/dist/node/commands/witnessCommand.d.ts +4 -0
  73. package/dist/node/commands/witnessCommand.d.ts.map +1 -0
  74. package/dist/node/index.d.ts +10 -0
  75. package/dist/node/index.d.ts.map +1 -0
  76. package/dist/node/index.mjs +3829 -0
  77. package/dist/node/index.mjs.map +7 -0
  78. package/dist/node/witness.d.ts +2 -0
  79. package/dist/node/witness.d.ts.map +1 -0
  80. package/package.json +72 -0
@@ -0,0 +1,3829 @@
1
+ // src/actions/bank/capitalOne.ts
2
+ import { createHash } from "node:crypto";
3
+ import { hashBinary } from "@ariestools/aries-witness-core";
4
+
5
+ // src/actions/bank/config.ts
6
+ import {
7
+ chmodSync,
8
+ existsSync,
9
+ mkdirSync,
10
+ readFileSync,
11
+ renameSync,
12
+ writeFileSync
13
+ } from "node:fs";
14
+ import { homedir } from "node:os";
15
+ import path from "node:path";
16
+ import chalk from "chalk";
17
+ var CAPITAL_ONE_CONFIG_KEYS = [
18
+ "env",
19
+ "base-url",
20
+ "token-url",
21
+ "client-id",
22
+ "client-secret",
23
+ "scope",
24
+ "accounts-path",
25
+ "balances-path",
26
+ "transactions-path"
27
+ ];
28
+ var SECRET_KEYS = /* @__PURE__ */ new Set(["client-secret"]);
29
+ var KEY_ALIASES = {
30
+ "accountsPath": "accounts-path",
31
+ "accounts-path": "accounts-path",
32
+ "balancesPath": "balances-path",
33
+ "balances-path": "balances-path",
34
+ "baseUrl": "base-url",
35
+ "base-url": "base-url",
36
+ "clientId": "client-id",
37
+ "client-id": "client-id",
38
+ "clientSecret": "client-secret",
39
+ "client-secret": "client-secret",
40
+ "env": "env",
41
+ "environment": "env",
42
+ "scope": "scope",
43
+ "tokenUrl": "token-url",
44
+ "token-url": "token-url",
45
+ "transactionsPath": "transactions-path",
46
+ "transactions-path": "transactions-path"
47
+ };
48
+ var ENV_KEY_MAP = {
49
+ "accounts-path": "ARIES_CAPITALONE_ACCOUNTS_PATH",
50
+ "balances-path": "ARIES_CAPITALONE_BALANCES_PATH",
51
+ "base-url": "ARIES_CAPITALONE_BASE_URL",
52
+ "client-id": "ARIES_CAPITALONE_CLIENT_ID",
53
+ "client-secret": "ARIES_CAPITALONE_CLIENT_SECRET",
54
+ "env": "ARIES_CAPITALONE_ENV",
55
+ "scope": "ARIES_CAPITALONE_SCOPE",
56
+ "token-url": "ARIES_CAPITALONE_TOKEN_URL",
57
+ "transactions-path": "ARIES_CAPITALONE_TRANSACTIONS_PATH"
58
+ };
59
+ function ariesHome() {
60
+ return process.env.ARIES_HOME ?? path.join(homedir(), ".aries");
61
+ }
62
+ function bankConfigDir(root = ariesHome()) {
63
+ return path.join(root, "bank");
64
+ }
65
+ function capitalOneConfigPath(root = ariesHome()) {
66
+ return path.join(bankConfigDir(root), "capitalone.json");
67
+ }
68
+ function defaultCapitalOneConfig() {
69
+ return { env: "sandbox" };
70
+ }
71
+ function isRecord(value) {
72
+ return typeof value === "object" && value !== null && !Array.isArray(value);
73
+ }
74
+ function readString(value, key) {
75
+ if (typeof value === "string") return value;
76
+ throw new Error(`bank.capitalone.${key} must be a string`);
77
+ }
78
+ function parseEnvironment(value, key) {
79
+ if (value === "sandbox" || value === "production") return value;
80
+ throw new Error(`bank.capitalone.${key} must be one of: sandbox, production`);
81
+ }
82
+ function readConfigValue(key, value) {
83
+ const text = readString(value, key);
84
+ return key === "env" ? parseEnvironment(text, key) : text;
85
+ }
86
+ function parseConfigValue(key, raw) {
87
+ return key === "env" ? parseEnvironment(raw, key) : raw;
88
+ }
89
+ function normalizeConfig(config) {
90
+ const next = {};
91
+ for (const key of CAPITAL_ONE_CONFIG_KEYS) {
92
+ const value = config[key];
93
+ if (value !== void 0) next[key] = readConfigValue(key, value);
94
+ }
95
+ return next;
96
+ }
97
+ function orderedConfig(config) {
98
+ const out = {};
99
+ for (const key of CAPITAL_ONE_CONFIG_KEYS) {
100
+ const value = config[key];
101
+ if (value !== void 0) out[key] = value;
102
+ }
103
+ return out;
104
+ }
105
+ function envConfig(env = process.env) {
106
+ const config = {};
107
+ for (const key of CAPITAL_ONE_CONFIG_KEYS) {
108
+ const value = env[ENV_KEY_MAP[key]];
109
+ if (value !== void 0 && value.length > 0) {
110
+ config[key] = parseConfigValue(key, value);
111
+ }
112
+ }
113
+ return config;
114
+ }
115
+ function displayValue(key, value, showSecrets) {
116
+ if (value === void 0) return "";
117
+ if (!showSecrets && SECRET_KEYS.has(key)) return "********";
118
+ return value;
119
+ }
120
+ function outputValue(key, value, showSecrets) {
121
+ if (value === void 0) return null;
122
+ if (!showSecrets && SECRET_KEYS.has(key)) return "********";
123
+ return value;
124
+ }
125
+ function normalizeCapitalOneConfigKey(value) {
126
+ const withoutPrefix = value.startsWith("bank.capitalone.") ? value.slice("bank.capitalone.".length) : value;
127
+ const key = withoutPrefix.startsWith("capitalone.") ? withoutPrefix.slice("capitalone.".length) : withoutPrefix;
128
+ const normalized = KEY_ALIASES[key];
129
+ if (normalized) return normalized;
130
+ throw new Error(`Unknown bank.capitalone config key "${value}". Known: ${CAPITAL_ONE_CONFIG_KEYS.join(", ")}`);
131
+ }
132
+ function readCapitalOneConfig(root = ariesHome()) {
133
+ const filePath = capitalOneConfigPath(root);
134
+ if (!existsSync(filePath)) return {};
135
+ const raw = readFileSync(filePath, "utf8");
136
+ if (raw.trim().length === 0) return {};
137
+ const parsed = JSON.parse(raw);
138
+ if (!isRecord(parsed)) throw new Error(`${filePath} must contain a JSON object`);
139
+ const config = {};
140
+ for (const key of CAPITAL_ONE_CONFIG_KEYS) {
141
+ if (parsed[key] !== void 0) config[key] = readConfigValue(key, parsed[key]);
142
+ }
143
+ return config;
144
+ }
145
+ function writeCapitalOneConfig(config, root = ariesHome()) {
146
+ const normalized = normalizeConfig(config);
147
+ const dir = bankConfigDir(root);
148
+ mkdirSync(dir, { recursive: true, mode: 448 });
149
+ const filePath = capitalOneConfigPath(root);
150
+ const tmp = `${filePath}.tmp`;
151
+ writeFileSync(tmp, `${JSON.stringify(orderedConfig(normalized), null, 2)}
152
+ `, { encoding: "utf8", mode: 384 });
153
+ renameSync(tmp, filePath);
154
+ try {
155
+ chmodSync(filePath, 384);
156
+ } catch {
157
+ }
158
+ }
159
+ function resolveCapitalOneConfig(overrides = {}, root = ariesHome(), env = process.env) {
160
+ return normalizeConfig({
161
+ ...defaultCapitalOneConfig(),
162
+ ...readCapitalOneConfig(root),
163
+ ...envConfig(env),
164
+ ...normalizeConfig(overrides)
165
+ });
166
+ }
167
+ function capitalOneConfigSet(options) {
168
+ const current = readCapitalOneConfig();
169
+ const next = {
170
+ ...current,
171
+ [options.key]: parseConfigValue(options.key, options.value)
172
+ };
173
+ writeCapitalOneConfig(next);
174
+ const display = displayValue(options.key, next[options.key], false);
175
+ console.log(`${chalk.green("set")} bank.capitalone.${options.key}=${display}`);
176
+ }
177
+ function capitalOneConfigGet(options = {}) {
178
+ const config = resolveCapitalOneConfig();
179
+ const keys = options.key ? [options.key] : CAPITAL_ONE_CONFIG_KEYS;
180
+ const showSecrets = options.showSecrets === true;
181
+ if (options.json === true) {
182
+ const out = {};
183
+ for (const key of keys) {
184
+ out[`bank.capitalone.${key}`] = outputValue(key, config[key], showSecrets);
185
+ }
186
+ console.log(JSON.stringify(out, null, 2));
187
+ return;
188
+ }
189
+ for (const key of keys) {
190
+ console.log(`${chalk.cyan(`bank.capitalone.${key}`)}=${displayValue(key, config[key], showSecrets)}`);
191
+ }
192
+ }
193
+
194
+ // src/actions/bank/capitalOne.ts
195
+ var BANK_ACCOUNT_SNAPSHOT_SCHEMA = "network.xyo.bank.account-snapshot";
196
+ var DEFAULT_HEADERS = { Accept: "application/json" };
197
+ function isRecord2(value) {
198
+ return typeof value === "object" && value !== null && !Array.isArray(value);
199
+ }
200
+ function stringValue(value) {
201
+ if (typeof value === "string" && value.length > 0) return value;
202
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
203
+ return void 0;
204
+ }
205
+ function getPath(value, path7) {
206
+ let current = value;
207
+ for (const part of path7) {
208
+ if (!isRecord2(current)) return void 0;
209
+ current = current[part];
210
+ }
211
+ return current;
212
+ }
213
+ function firstValue(source, paths) {
214
+ for (const path7 of paths) {
215
+ const value = getPath(source, path7);
216
+ if (value !== void 0 && value !== null) return value;
217
+ }
218
+ return void 0;
219
+ }
220
+ function firstString(source, paths) {
221
+ return stringValue(firstValue(source, paths));
222
+ }
223
+ function itemsAt(raw, paths) {
224
+ for (const path7 of paths) {
225
+ const value = getPath(raw, path7);
226
+ if (Array.isArray(value)) return value;
227
+ if (isRecord2(value)) return [value];
228
+ }
229
+ return [];
230
+ }
231
+ function hashIdentifier(provider, kind, value) {
232
+ if (!value) return void 0;
233
+ return createHash("sha256").update(`${provider}:${kind}:${value}`).digest("hex");
234
+ }
235
+ function maybeRedactedId(provider, kind, value, redaction, clearKey) {
236
+ if (!value) return {};
237
+ if (redaction === "none") return { [clearKey]: value };
238
+ return { [`${clearKey}Hash`]: hashIdentifier(provider, kind, value) ?? "" };
239
+ }
240
+ function extractLast4(value) {
241
+ if (!value) return void 0;
242
+ const digits = value.replaceAll(/\D/gu, "");
243
+ if (digits.length >= 4) return digits.slice(-4);
244
+ return value.length >= 4 ? value.slice(-4) : value;
245
+ }
246
+ function normalizeAmount(source) {
247
+ const amount = firstString(source, [
248
+ ["Amount", "Amount"],
249
+ ["amount", "amount"],
250
+ ["amount"],
251
+ ["currentBalance"],
252
+ ["availableBalance"],
253
+ ["balance"]
254
+ ]);
255
+ const currency = firstString(source, [
256
+ ["Amount", "Currency"],
257
+ ["amount", "currency"],
258
+ ["currency"],
259
+ ["Currency"]
260
+ ]);
261
+ if (!amount && !currency) return void 0;
262
+ return { amount, currency };
263
+ }
264
+ function normalizeAccount(source, provider, redaction) {
265
+ const accountId = firstString(source, [
266
+ ["AccountId"],
267
+ ["accountId"],
268
+ ["id"],
269
+ ["account_id"]
270
+ ]);
271
+ const identification = firstString(source, [
272
+ ["Account", "Identification"],
273
+ ["account", "identification"],
274
+ ["identification"],
275
+ ["mask"],
276
+ ["last4"]
277
+ ]);
278
+ return {
279
+ ...maybeRedactedId(provider, "account", accountId, redaction, "accountId"),
280
+ accountSubType: firstString(source, [["AccountSubType"], ["accountSubType"], ["subtype"], ["subType"]]),
281
+ accountType: firstString(source, [["AccountType"], ["accountType"], ["type"]]),
282
+ currency: firstString(source, [["Currency"], ["currency"]]),
283
+ description: firstString(source, [["Description"], ["description"]]),
284
+ last4: extractLast4(identification),
285
+ name: firstString(source, [["Account", "Name"], ["account", "name"], ["name"], ["displayName"]])
286
+ };
287
+ }
288
+ function normalizeBalance(source, provider, redaction) {
289
+ const accountId = firstString(source, [
290
+ ["AccountId"],
291
+ ["accountId"],
292
+ ["account_id"]
293
+ ]);
294
+ return {
295
+ ...maybeRedactedId(provider, "account", accountId, redaction, "accountId"),
296
+ amount: normalizeAmount(source),
297
+ creditDebitIndicator: firstString(source, [["CreditDebitIndicator"], ["creditDebitIndicator"]]),
298
+ dateTime: firstString(source, [["DateTime"], ["dateTime"], ["datetime"], ["asOf"]]),
299
+ type: firstString(source, [["Type"], ["type"]])
300
+ };
301
+ }
302
+ function normalizeTransaction(source, provider, redaction) {
303
+ const accountId = firstString(source, [
304
+ ["AccountId"],
305
+ ["accountId"],
306
+ ["account_id"]
307
+ ]);
308
+ const transactionId = firstString(source, [
309
+ ["TransactionId"],
310
+ ["TransactionReference"],
311
+ ["transactionId"],
312
+ ["transactionReference"],
313
+ ["id"]
314
+ ]);
315
+ return {
316
+ ...maybeRedactedId(provider, "account", accountId, redaction, "accountId"),
317
+ ...maybeRedactedId(provider, "transaction", transactionId, redaction, "transactionId"),
318
+ amount: normalizeAmount(source),
319
+ bookingDateTime: firstString(source, [["BookingDateTime"], ["bookingDateTime"], ["postedAt"], ["postedDate"]]),
320
+ creditDebitIndicator: firstString(source, [["CreditDebitIndicator"], ["creditDebitIndicator"], ["type"]]),
321
+ description: firstString(source, [["TransactionInformation"], ["transactionInformation"], ["description"], ["memo"]]),
322
+ merchantName: firstString(source, [["MerchantDetails", "MerchantName"], ["merchantDetails", "merchantName"], ["merchantName"]]),
323
+ status: firstString(source, [["Status"], ["status"]]),
324
+ valueDateTime: firstString(source, [["ValueDateTime"], ["valueDateTime"], ["authorizedAt"], ["transactionDate"]])
325
+ };
326
+ }
327
+ function cleanObject(value) {
328
+ for (const key of Object.keys(value)) {
329
+ const child = value[key];
330
+ if (child === void 0) {
331
+ delete value[key];
332
+ } else if (isRecord2(child)) {
333
+ cleanObject(child);
334
+ if (Object.keys(child).length === 0) delete value[key];
335
+ }
336
+ }
337
+ return value;
338
+ }
339
+ function jsonBinary(name, raw) {
340
+ const data = Buffer.from(`${JSON.stringify(raw, null, 2)}
341
+ `, "utf8");
342
+ return {
343
+ data,
344
+ ref: {
345
+ filename: `capitalone-${name}.json`,
346
+ hash: hashBinary(data),
347
+ mimeType: "application/json",
348
+ size: data.length
349
+ }
350
+ };
351
+ }
352
+ function requireConfig(config, key, reason) {
353
+ const value = config[key];
354
+ if (typeof value === "string" && value.length > 0) return value;
355
+ throw new Error(`Capital One ${reason} is required: set bank.capitalone.${key} or ARIES_CAPITALONE_${String(key).replaceAll("-", "_").toUpperCase()}.`);
356
+ }
357
+ function endpointUrl(baseUrl, pathTemplate, replacements, query) {
358
+ let resolvedPath = pathTemplate;
359
+ for (const [key, value] of Object.entries(replacements)) {
360
+ if (value !== void 0) resolvedPath = resolvedPath.replaceAll(`{${key}}`, encodeURIComponent(value));
361
+ }
362
+ const url = new URL(resolvedPath.startsWith("http") ? resolvedPath : resolvedPath.replace(/^\//u, ""), baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`);
363
+ for (const [key, value] of Object.entries(query)) {
364
+ if (value !== void 0 && value.length > 0) url.searchParams.set(key, value);
365
+ }
366
+ return url;
367
+ }
368
+ async function fetchJson(url, init, label) {
369
+ const response = await fetch(url, init);
370
+ const bodyText = await response.text();
371
+ if (!response.ok) {
372
+ const snippet = bodyText.trim().slice(0, 400);
373
+ throw new Error(`Capital One ${label} request failed (${response.status} ${response.statusText}): ${snippet}`);
374
+ }
375
+ try {
376
+ return { bodyText, json: bodyText.length > 0 ? JSON.parse(bodyText) : null };
377
+ } catch (error) {
378
+ throw new Error(`Capital One ${label} response was not valid JSON`, { cause: error });
379
+ }
380
+ }
381
+ async function mintAccessToken(config) {
382
+ const tokenUrl = requireConfig(config, "token-url", "token URL");
383
+ const clientId = requireConfig(config, "client-id", "client id");
384
+ const clientSecret = requireConfig(config, "client-secret", "client secret");
385
+ const body = new URLSearchParams();
386
+ body.set("client_id", clientId);
387
+ body.set("client_secret", clientSecret);
388
+ body.set("grant_type", "client_credentials");
389
+ if (config.scope) body.set("scope", config.scope);
390
+ const result = await fetchJson(tokenUrl, {
391
+ body: body.toString(),
392
+ headers: {
393
+ ...DEFAULT_HEADERS,
394
+ "Content-Type": "application/x-www-form-urlencoded"
395
+ },
396
+ method: "POST"
397
+ }, "token");
398
+ const token = result.json?.access_token;
399
+ if (!token) throw new Error("Capital One token response did not include access_token");
400
+ return token;
401
+ }
402
+ async function resolveAccessToken(options, config) {
403
+ const token = options["access-token"] ?? process.env.ARIES_CAPITALONE_ACCESS_TOKEN;
404
+ if (token && token.length > 0) return token;
405
+ return mintAccessToken(config);
406
+ }
407
+ function accountQuery(accountId, pathTemplate) {
408
+ return pathTemplate.includes("{accountId}") ? {} : { accountId };
409
+ }
410
+ function environment(config) {
411
+ return config.env ?? "sandbox";
412
+ }
413
+ async function observeCapitalOne(args) {
414
+ const options = args;
415
+ const config = resolveCapitalOneConfig({
416
+ "accounts-path": options["accounts-path"],
417
+ "balances-path": options["balances-path"],
418
+ "base-url": options["base-url"],
419
+ "client-id": options["client-id"],
420
+ "client-secret": options["client-secret"],
421
+ "env": options.env,
422
+ "scope": options.scope,
423
+ "token-url": options["token-url"],
424
+ "transactions-path": options["transactions-path"]
425
+ });
426
+ const redaction = options.redaction ?? "default";
427
+ const baseUrl = requireConfig(config, "base-url", "base URL");
428
+ const accountsPath = requireConfig(config, "accounts-path", "accounts endpoint path");
429
+ const balancesPath = requireConfig(config, "balances-path", "balances endpoint path");
430
+ const transactionsPath = requireConfig(config, "transactions-path", "transactions endpoint path");
431
+ const accessToken = await resolveAccessToken(options, config);
432
+ const authHeaders = {
433
+ ...DEFAULT_HEADERS,
434
+ Authorization: `Bearer ${accessToken}`
435
+ };
436
+ const accountId = options["account-id"];
437
+ const accounts = await fetchJson(endpointUrl(baseUrl, accountsPath, {}, {}), {
438
+ headers: authHeaders,
439
+ method: "GET"
440
+ }, "accounts");
441
+ const balances = await fetchJson(endpointUrl(baseUrl, balancesPath, { accountId }, accountQuery(accountId, balancesPath)), {
442
+ headers: authHeaders,
443
+ method: "GET"
444
+ }, "balances");
445
+ const transactions = await fetchJson(endpointUrl(baseUrl, transactionsPath, { accountId }, {
446
+ ...accountQuery(accountId, transactionsPath),
447
+ from: options.from,
448
+ to: options.to
449
+ }), {
450
+ headers: authHeaders,
451
+ method: "GET"
452
+ }, "transactions");
453
+ const accountItems = itemsAt(accounts.json, [
454
+ ["Data", "Account"],
455
+ ["Data", "Accounts"],
456
+ ["data", "accounts"],
457
+ ["accounts"]
458
+ ]);
459
+ const balanceItems = itemsAt(balances.json, [
460
+ ["Data", "Balance"],
461
+ ["Data", "Balances"],
462
+ ["data", "balances"],
463
+ ["balances"]
464
+ ]);
465
+ const transactionItems = itemsAt(transactions.json, [
466
+ ["Data", "Transaction"],
467
+ ["Data", "Transactions"],
468
+ ["data", "transactions"],
469
+ ["transactions"]
470
+ ]);
471
+ const rawArtifacts = options["include-raw"] === true ? {
472
+ accounts: jsonBinary("accounts", accounts.json),
473
+ balances: jsonBinary("balances", balances.json),
474
+ transactions: jsonBinary("transactions", transactions.json)
475
+ } : void 0;
476
+ const payload = cleanObject({
477
+ accounts: accountItems.map((item) => cleanObject(normalizeAccount(item, "capitalone", redaction))),
478
+ balances: balanceItems.map((item) => cleanObject(normalizeBalance(item, "capitalone", redaction))),
479
+ environment: environment(config),
480
+ observedAt: (/* @__PURE__ */ new Date()).toISOString(),
481
+ provider: "capitalone",
482
+ raw: rawArtifacts ? {
483
+ accounts: rawArtifacts.accounts.ref,
484
+ balances: rawArtifacts.balances.ref,
485
+ transactions: rawArtifacts.transactions.ref
486
+ } : void 0,
487
+ request: cleanObject({
488
+ accountId: redaction === "none" ? accountId : void 0,
489
+ accountIdHash: redaction === "default" ? hashIdentifier("capitalone", "account", accountId) : void 0,
490
+ from: options.from,
491
+ to: options.to
492
+ }),
493
+ schema: BANK_ACCOUNT_SNAPSHOT_SCHEMA,
494
+ transactions: transactionItems.map((item) => cleanObject(normalizeTransaction(item, "capitalone", redaction)))
495
+ });
496
+ return {
497
+ binaries: rawArtifacts ? Object.values(rawArtifacts) : void 0,
498
+ payloads: [payload]
499
+ };
500
+ }
501
+ var capitalOneProvider = {
502
+ name: "capitalone",
503
+ observe: observeCapitalOne
504
+ };
505
+
506
+ // src/actions/bank/capitalOneProducts.ts
507
+ import { readFileSync as readFileSync2 } from "node:fs";
508
+ import { hashBinary as hashBinary2 } from "@ariestools/aries-witness-core";
509
+ var BANK_PRODUCT_CATALOG_SCHEMA = "network.xyo.bank.product-catalog";
510
+ var CAPITAL_ONE_PRODUCTS_DEVEX_PRODUCT_ID = "1359-5";
511
+ var CAPITAL_ONE_PRODUCTS_NAME = "Retrieve Consumer Bank Products";
512
+ var DEFAULT_API_VERSION = "5";
513
+ var DEFAULT_PRODUCTS_OPERATION = "search";
514
+ var DEFAULT_PRODUCTS_PATH_PRODUCT_ID = "~";
515
+ var SANDBOX_BASE_URL = "https://api-sandbox.capitalone.com";
516
+ var SANDBOX_TOKEN_URL = `${SANDBOX_BASE_URL}/oauth2/token`;
517
+ var PRODUCT_ENDPOINTS = {
518
+ "retrieve-products": {
519
+ method: "POST",
520
+ path: "/deposits/products/retrieve-products",
521
+ requiresProductId: false
522
+ },
523
+ "retail-products": {
524
+ method: "GET",
525
+ path: "/deposits/products/{productId}/retail-products",
526
+ requiresProductId: true
527
+ },
528
+ "search": {
529
+ method: "POST",
530
+ path: "/deposits/products/{productId}/search",
531
+ requiresProductId: true
532
+ }
533
+ };
534
+ function isRecord3(value) {
535
+ return typeof value === "object" && value !== null && !Array.isArray(value);
536
+ }
537
+ function stringValue2(value) {
538
+ if (typeof value === "string" && value.length > 0) return value;
539
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
540
+ return void 0;
541
+ }
542
+ function getPath2(value, path7) {
543
+ let current = value;
544
+ for (const part of path7) {
545
+ if (!isRecord3(current)) return void 0;
546
+ current = current[part];
547
+ }
548
+ return current;
549
+ }
550
+ function firstValue2(source, paths) {
551
+ for (const path7 of paths) {
552
+ const value = getPath2(source, path7);
553
+ if (value !== void 0 && value !== null) return value;
554
+ }
555
+ return void 0;
556
+ }
557
+ function firstString2(source, paths) {
558
+ return stringValue2(firstValue2(source, paths));
559
+ }
560
+ function itemsAt2(raw, operation) {
561
+ const paths = operation === "retail-products" ? [["retailProducts"], ["RetailProducts"], ["data", "retailProducts"], ["Data", "RetailProducts"]] : [["products"], ["Products"], ["data", "products"], ["Data", "Products"]];
562
+ for (const path7 of paths) {
563
+ const value = getPath2(raw, path7);
564
+ if (Array.isArray(value)) return value;
565
+ if (isRecord3(value)) return [value];
566
+ }
567
+ return [];
568
+ }
569
+ function cleanObject2(value) {
570
+ for (const key of Object.keys(value)) {
571
+ const child = value[key];
572
+ if (child === void 0) {
573
+ delete value[key];
574
+ } else if (isRecord3(child)) {
575
+ cleanObject2(child);
576
+ if (Object.keys(child).length === 0) delete value[key];
577
+ } else if (Array.isArray(child)) {
578
+ for (const item of child) {
579
+ if (isRecord3(item)) cleanObject2(item);
580
+ }
581
+ if (child.length === 0) delete value[key];
582
+ }
583
+ }
584
+ return value;
585
+ }
586
+ function jsonBinary2(name, raw) {
587
+ const data = Buffer.from(`${JSON.stringify(raw, null, 2)}
588
+ `, "utf8");
589
+ return {
590
+ data,
591
+ ref: {
592
+ filename: `capitalone-${name}.json`,
593
+ hash: hashBinary2(data),
594
+ mimeType: "application/json",
595
+ size: data.length
596
+ }
597
+ };
598
+ }
599
+ function parseJsonInput(raw, label) {
600
+ try {
601
+ return JSON.parse(raw);
602
+ } catch (error) {
603
+ throw new Error(`Capital One products ${label} must be valid JSON`, { cause: error });
604
+ }
605
+ }
606
+ function gatewayHint(label, status, bodyText) {
607
+ if (status !== 403 || !bodyText.toLowerCase().includes("no endpoint matched for request")) return "";
608
+ if (label === "token") {
609
+ return " For direct CLI token minting, use https://api-sandbox.capitalone.com/oauth2/token; /sandbox/oauth2/token is a DevExchange-host relative route.";
610
+ }
611
+ return [
612
+ " Capital One token minting succeeded, but the sandbox API gateway did not match the product endpoint.",
613
+ "Verify the app is connected to DevExchange product 1359-5 in the sandbox and confirm the live sandbox base path with Capital One."
614
+ ].join(" ");
615
+ }
616
+ function requestBody(options, method, operation) {
617
+ if (method === "GET") return void 0;
618
+ if (options["body-file"]) return parseJsonInput(readFileSync2(options["body-file"], "utf8"), "body file");
619
+ if (options.body) return parseJsonInput(options.body, "body");
620
+ if (operation === "search") return { isCollapseRate: true };
621
+ return {};
622
+ }
623
+ function environment2(config) {
624
+ return config.env ?? "sandbox";
625
+ }
626
+ function requireConfig2(config, key, reason) {
627
+ const value = config[key];
628
+ if (typeof value === "string" && value.length > 0) return value;
629
+ throw new Error(`Capital One products ${reason} is required: set bank.capitalone.${key} or ARIES_CAPITALONE_${String(key).replaceAll("-", "_").toUpperCase()}.`);
630
+ }
631
+ function configuredOrSandboxDefault(config, key, sandboxDefault, reason) {
632
+ const configured = config[key];
633
+ if (configured && configured.length > 0) return configured;
634
+ if (environment2(config) === "sandbox") return sandboxDefault;
635
+ return requireConfig2(config, key, reason);
636
+ }
637
+ async function fetchJson2(url, init, label) {
638
+ const response = await fetch(url, init);
639
+ const bodyText = await response.text();
640
+ if (!response.ok) {
641
+ const snippet = bodyText.trim().slice(0, 400);
642
+ throw new Error(`Capital One products ${label} request failed (${response.status} ${response.statusText}): ${snippet}${gatewayHint(label, response.status, bodyText)}`);
643
+ }
644
+ try {
645
+ return { bodyText, json: bodyText.length > 0 ? JSON.parse(bodyText) : null };
646
+ } catch (error) {
647
+ throw new Error(`Capital One products ${label} response was not valid JSON`, { cause: error });
648
+ }
649
+ }
650
+ async function mintAccessToken2(config) {
651
+ const tokenUrl = configuredOrSandboxDefault(config, "token-url", SANDBOX_TOKEN_URL, "token URL");
652
+ const clientId = requireConfig2(config, "client-id", "client id");
653
+ const clientSecret = requireConfig2(config, "client-secret", "client secret");
654
+ const body = new URLSearchParams();
655
+ body.set("client_id", clientId);
656
+ body.set("client_secret", clientSecret);
657
+ body.set("grant_type", "client_credentials");
658
+ if (config.scope) body.set("scope", config.scope);
659
+ const result = await fetchJson2(tokenUrl, {
660
+ body: body.toString(),
661
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
662
+ method: "POST"
663
+ }, "token");
664
+ const token = result.json?.access_token;
665
+ if (!token) throw new Error("Capital One products token response did not include access_token");
666
+ return token;
667
+ }
668
+ async function resolveAccessToken2(options, config) {
669
+ const token = options["access-token"] ?? process.env.ARIES_CAPITALONE_ACCESS_TOKEN;
670
+ if (token && token.length > 0) return token;
671
+ return mintAccessToken2(config);
672
+ }
673
+ function endpointUrl2(baseUrl, pathTemplate, productId) {
674
+ const resolvedPath = productId === void 0 ? pathTemplate : pathTemplate.replaceAll("{productId}", encodeURIComponent(productId));
675
+ return new URL(resolvedPath.replace(/^\//u, ""), baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`);
676
+ }
677
+ function normalizeProduct(source) {
678
+ const rates = firstValue2(source, [["interest", "rates"], ["rates"]]);
679
+ const features = firstValue2(source, [["features"], ["retailProductfeatures"], ["retailProductFeatures"]]);
680
+ return cleanObject2({
681
+ baseCurrency: firstString2(source, [["base", "baseCurrency"], ["baseCurrency"]]),
682
+ features: Array.isArray(features) ? features : void 0,
683
+ lineOfBusiness: firstString2(source, [["base", "lineOfBusiness"], ["productDefinition", "lineOfBusiness"], ["lineOfBusiness"]]),
684
+ productDescription: firstString2(source, [["productDefinition", "productDescription"], ["productDescription"]]),
685
+ productFamily: firstString2(source, [
686
+ ["base", "productFamily"],
687
+ ["productDefinition", "productGroup"],
688
+ ["productFamily"],
689
+ ["productFamilyName"]
690
+ ]),
691
+ productGroup: firstString2(source, [["productDefinition", "productGroup"], ["productGroup"]]),
692
+ productId: firstString2(source, [
693
+ ["base", "productId"],
694
+ ["base", "productLineId"],
695
+ ["productDefinition", "productCode"],
696
+ ["retailProductDefinition", "retailProductCode"],
697
+ ["retailProductDefinition", "productCode"],
698
+ ["productId"],
699
+ ["productLineId"]
700
+ ]),
701
+ productLineId: firstString2(source, [["base", "productLineId"], ["productLineId"]]),
702
+ productName: firstString2(source, [["productDefinition", "productName"], ["retailProductfeatures", "productName"], ["productName"]]),
703
+ productSourceId: firstString2(source, [["base", "productSourceId"], ["productSourceId"]]),
704
+ productStatus: firstString2(source, [["productDefinition", "productStatus"], ["productStatus"]]),
705
+ productType: firstString2(source, [["base", "productType"], ["productDefinition", "productType"], ["productType"]]),
706
+ rates: Array.isArray(rates) ? rates : void 0
707
+ });
708
+ }
709
+ async function observeCapitalOneProducts(args) {
710
+ const options = args;
711
+ const config = resolveCapitalOneConfig({
712
+ "base-url": options["base-url"],
713
+ "client-id": options["client-id"],
714
+ "client-secret": options["client-secret"],
715
+ "env": options.env,
716
+ "scope": options.scope,
717
+ "token-url": options["token-url"]
718
+ });
719
+ const operation = options.operation ?? DEFAULT_PRODUCTS_OPERATION;
720
+ const endpoint = PRODUCT_ENDPOINTS[operation];
721
+ const productId = options["product-id"] ?? (endpoint.requiresProductId ? DEFAULT_PRODUCTS_PATH_PRODUCT_ID : void 0);
722
+ if (endpoint.requiresProductId && (!productId || productId.length === 0)) {
723
+ throw new Error(`Capital One products operation "${operation}" requires --product-id.`);
724
+ }
725
+ const baseUrl = configuredOrSandboxDefault(config, "base-url", SANDBOX_BASE_URL, "base URL");
726
+ const apiVersion = options["api-version"] ?? DEFAULT_API_VERSION;
727
+ const accessToken = await resolveAccessToken2(options, config);
728
+ const body = requestBody(options, endpoint.method, operation);
729
+ const headers = {
730
+ Accept: `application/json;v=${apiVersion}`,
731
+ Authorization: `Bearer ${accessToken}`
732
+ };
733
+ if (endpoint.method === "POST") headers["Content-Type"] = `application/json;v=${apiVersion}`;
734
+ const response = await fetchJson2(endpointUrl2(baseUrl, endpoint.path, productId), {
735
+ body: body === void 0 ? void 0 : JSON.stringify(body),
736
+ headers,
737
+ method: endpoint.method
738
+ }, operation);
739
+ const rawArtifact = options["include-raw"] === true ? jsonBinary2("bank-products-response", response.json) : void 0;
740
+ const products = itemsAt2(response.json, operation).map(normalizeProduct);
741
+ const payload = cleanObject2({
742
+ apiVersion,
743
+ devExchangeProductId: CAPITAL_ONE_PRODUCTS_DEVEX_PRODUCT_ID,
744
+ environment: environment2(config),
745
+ observedAt: (/* @__PURE__ */ new Date()).toISOString(),
746
+ operation,
747
+ productCount: products.length,
748
+ productId,
749
+ productName: CAPITAL_ONE_PRODUCTS_NAME,
750
+ products,
751
+ provider: "capitalone",
752
+ raw: rawArtifact?.ref,
753
+ request: cleanObject2({
754
+ body,
755
+ method: endpoint.method,
756
+ path: endpoint.path,
757
+ productId
758
+ }),
759
+ schema: BANK_PRODUCT_CATALOG_SCHEMA
760
+ });
761
+ return {
762
+ binaries: rawArtifact ? [rawArtifact] : void 0,
763
+ payloads: [payload]
764
+ };
765
+ }
766
+ var capitalOneProductsProvider = {
767
+ name: "capitalone-products",
768
+ observe: observeCapitalOneProducts
769
+ };
770
+
771
+ // src/actions/pentair/circuit.ts
772
+ import chalk3 from "chalk";
773
+
774
+ // src/actions/pentair/screenlogic.ts
775
+ import { FindUnits, screenlogic } from "node-screenlogic";
776
+
777
+ // src/actions/pentair/config.ts
778
+ import {
779
+ chmodSync as chmodSync2,
780
+ existsSync as existsSync2,
781
+ mkdirSync as mkdirSync2,
782
+ readFileSync as readFileSync3,
783
+ renameSync as renameSync2,
784
+ writeFileSync as writeFileSync2
785
+ } from "node:fs";
786
+ import { homedir as homedir2 } from "node:os";
787
+ import path2 from "node:path";
788
+ import chalk2 from "chalk";
789
+ var PENTAIR_CONFIG_KEYS = [
790
+ "address",
791
+ "port",
792
+ "password",
793
+ "system-name",
794
+ "response-timeout",
795
+ "search-timeout",
796
+ "search-attempts",
797
+ "series-file"
798
+ ];
799
+ var NUMBER_KEYS = /* @__PURE__ */ new Set(["port", "response-timeout", "search-timeout", "search-attempts"]);
800
+ var SECRET_KEYS2 = /* @__PURE__ */ new Set(["password"]);
801
+ var KEY_ALIASES2 = {
802
+ "address": "address",
803
+ "password": "password",
804
+ "port": "port",
805
+ "responseTimeout": "response-timeout",
806
+ "response-timeout": "response-timeout",
807
+ "searchAttempts": "search-attempts",
808
+ "search-attempts": "search-attempts",
809
+ "searchTimeout": "search-timeout",
810
+ "search-timeout": "search-timeout",
811
+ "seriesFile": "series-file",
812
+ "series-file": "series-file",
813
+ "systemName": "system-name",
814
+ "system-name": "system-name"
815
+ };
816
+ function ariesHome2() {
817
+ return process.env.ARIES_HOME ?? path2.join(homedir2(), ".aries");
818
+ }
819
+ function pentairConfigDir(root = ariesHome2()) {
820
+ return path2.join(root, "pentair");
821
+ }
822
+ function pentairConfigPath(root = ariesHome2()) {
823
+ return path2.join(pentairConfigDir(root), "config.json");
824
+ }
825
+ function defaultPentairSeriesFile(root = ariesHome2()) {
826
+ return path2.join(pentairConfigDir(root), "intellichem.jsonl");
827
+ }
828
+ function defaultPentairConfig(root = ariesHome2()) {
829
+ return {
830
+ "port": 80,
831
+ "response-timeout": 15e3,
832
+ "search-attempts": 3,
833
+ "search-timeout": 5e3,
834
+ "series-file": defaultPentairSeriesFile(root),
835
+ "system-name": "Pentair ScreenLogic"
836
+ };
837
+ }
838
+ function isRecord4(value) {
839
+ return typeof value === "object" && value !== null && !Array.isArray(value);
840
+ }
841
+ function readNumber(value, key) {
842
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) return value;
843
+ throw new Error(`pentair.${key} must be a positive number`);
844
+ }
845
+ function readString2(value, key) {
846
+ if (typeof value === "string") return value;
847
+ throw new Error(`pentair.${key} must be a string`);
848
+ }
849
+ function parseNumber(raw, key) {
850
+ const parsed = Number(raw);
851
+ if (Number.isFinite(parsed) && parsed > 0) return parsed;
852
+ throw new Error(`pentair.${key} must be a positive number`);
853
+ }
854
+ function readConfigValue2(key, value) {
855
+ return NUMBER_KEYS.has(key) ? readNumber(value, key) : readString2(value, key);
856
+ }
857
+ function parseConfigValue2(key, raw) {
858
+ return NUMBER_KEYS.has(key) ? parseNumber(raw, key) : raw;
859
+ }
860
+ function normalizeConfig2(config) {
861
+ const next = {};
862
+ for (const key of PENTAIR_CONFIG_KEYS) {
863
+ const value = config[key];
864
+ if (value !== void 0) next[key] = readConfigValue2(key, value);
865
+ }
866
+ return next;
867
+ }
868
+ function orderedConfig2(config) {
869
+ const out = {};
870
+ for (const key of PENTAIR_CONFIG_KEYS) {
871
+ const value = config[key];
872
+ if (value !== void 0) out[key] = value;
873
+ }
874
+ return out;
875
+ }
876
+ function displayValue2(key, value, showSecrets) {
877
+ if (value === void 0) return "";
878
+ if (!showSecrets && SECRET_KEYS2.has(key)) return "********";
879
+ return String(value);
880
+ }
881
+ function outputValue2(key, value, showSecrets) {
882
+ if (value === void 0) return null;
883
+ if (!showSecrets && SECRET_KEYS2.has(key)) return "********";
884
+ return value;
885
+ }
886
+ function normalizePentairConfigKey(value) {
887
+ const key = value.startsWith("pentair.") ? value.slice("pentair.".length) : value;
888
+ const normalized = KEY_ALIASES2[key];
889
+ if (normalized) return normalized;
890
+ throw new Error(`Unknown pentair config key "${value}". Known: ${PENTAIR_CONFIG_KEYS.join(", ")}`);
891
+ }
892
+ function readPentairConfig(root = ariesHome2()) {
893
+ const filePath = pentairConfigPath(root);
894
+ if (!existsSync2(filePath)) return {};
895
+ const raw = readFileSync3(filePath, "utf8");
896
+ if (raw.trim().length === 0) return {};
897
+ const parsed = JSON.parse(raw);
898
+ if (!isRecord4(parsed)) throw new Error(`${filePath} must contain a JSON object`);
899
+ const config = {};
900
+ for (const key of PENTAIR_CONFIG_KEYS) {
901
+ if (parsed[key] !== void 0) config[key] = readConfigValue2(key, parsed[key]);
902
+ }
903
+ return config;
904
+ }
905
+ function writePentairConfig(config, root = ariesHome2()) {
906
+ const normalized = normalizeConfig2(config);
907
+ const dir = pentairConfigDir(root);
908
+ mkdirSync2(dir, { recursive: true, mode: 448 });
909
+ const filePath = pentairConfigPath(root);
910
+ const tmp = `${filePath}.tmp`;
911
+ writeFileSync2(tmp, `${JSON.stringify(orderedConfig2(normalized), null, 2)}
912
+ `, { encoding: "utf8", mode: 384 });
913
+ renameSync2(tmp, filePath);
914
+ try {
915
+ chmodSync2(filePath, 384);
916
+ } catch {
917
+ }
918
+ }
919
+ function resolvePentairConfig(overrides = {}, root = ariesHome2()) {
920
+ return normalizeConfig2({
921
+ ...defaultPentairConfig(root),
922
+ ...readPentairConfig(root),
923
+ ...normalizeConfig2(overrides)
924
+ });
925
+ }
926
+ function pentairConfigSet(options) {
927
+ const current = readPentairConfig();
928
+ const next = {
929
+ ...current,
930
+ [options.key]: parseConfigValue2(options.key, options.value)
931
+ };
932
+ writePentairConfig(next);
933
+ const display = displayValue2(options.key, next[options.key], false);
934
+ console.log(`${chalk2.green("set")} pentair.${options.key}=${display}`);
935
+ }
936
+ function pentairConfigGet(options = {}) {
937
+ const config = resolvePentairConfig();
938
+ const keys = options.key ? [options.key] : PENTAIR_CONFIG_KEYS;
939
+ const showSecrets = options.showSecrets === true;
940
+ if (options.json === true) {
941
+ const out = {};
942
+ for (const key of keys) {
943
+ out[`pentair.${key}`] = outputValue2(key, config[key], showSecrets);
944
+ }
945
+ console.log(JSON.stringify(out, null, 2));
946
+ return;
947
+ }
948
+ for (const key of keys) {
949
+ console.log(`${chalk2.cyan(`pentair.${key}`)}=${displayValue2(key, config[key], showSecrets)}`);
950
+ }
951
+ }
952
+
953
+ // src/actions/pentair/screenlogic.ts
954
+ async function findLocalUnit(searchTimeout, searchAttempts) {
955
+ for (let attempt = 0; attempt < searchAttempts; attempt += 1) {
956
+ const finder = new FindUnits();
957
+ const localUnit = (await finder.searchAsync(searchTimeout)).shift();
958
+ if (localUnit) return localUnit;
959
+ }
960
+ return;
961
+ }
962
+ async function withScreenLogic(overrides, callback) {
963
+ const options = resolvePentairConfig(overrides);
964
+ if (options.address) {
965
+ const password = options.password ?? process.env.SCREENLOGIC_PASSWORD;
966
+ screenlogic.init(options["system-name"] ?? "Pentair ScreenLogic", options.address, options.port ?? 80, password);
967
+ } else {
968
+ const localUnit = await findLocalUnit(options["search-timeout"] ?? 5e3, options["search-attempts"] ?? 3);
969
+ if (!localUnit) throw new Error("No local ScreenLogic unit found");
970
+ screenlogic.initUnit(localUnit);
971
+ }
972
+ screenlogic.netTimeout = options["response-timeout"] ?? 15e3;
973
+ if (!await screenlogic.connectAsync()) {
974
+ throw new Error("Failed to connect to ScreenLogic");
975
+ }
976
+ try {
977
+ return await callback(screenlogic);
978
+ } finally {
979
+ await screenlogic.closeAsync().catch(() => false);
980
+ }
981
+ }
982
+
983
+ // src/actions/pentair/circuit.ts
984
+ function readPositiveInteger(value, name) {
985
+ if (value === void 0) return void 0;
986
+ if (Number.isInteger(value) && value > 0) return value;
987
+ throw new Error(`${name} must be a positive integer`);
988
+ }
989
+ function resolveCircuitRuntimeMinutes(options) {
990
+ const hours = readPositiveInteger(options.hours, "--hours");
991
+ const minutes = readPositiveInteger(options.minutes, "--minutes");
992
+ if (hours !== void 0 && minutes !== void 0) {
993
+ throw new Error("Use either --hours or --minutes, not both");
994
+ }
995
+ if (hours === void 0 && minutes === void 0) {
996
+ throw new Error("Either --hours or --minutes is required");
997
+ }
998
+ if (minutes !== void 0) return minutes;
999
+ if (hours !== void 0) return hours * 60;
1000
+ throw new Error("Either --hours or --minutes is required");
1001
+ }
1002
+ async function setPentairCircuitRuntime(options) {
1003
+ const circuitId = readPositiveInteger(options["circuit-id"], "--circuit-id");
1004
+ if (circuitId === void 0) throw new Error("--circuit-id is required");
1005
+ const runTimeMinutes = resolveCircuitRuntimeMinutes(options);
1006
+ const turnOn = options.on === true;
1007
+ await withScreenLogic(options, async (unit) => {
1008
+ await unit.circuits.setCircuitRuntimebyIdAsync(circuitId, runTimeMinutes);
1009
+ if (turnOn) await unit.circuits.setCircuitStateAsync(circuitId, true);
1010
+ });
1011
+ const result = {
1012
+ circuitId,
1013
+ runTimeMinutes,
1014
+ ...turnOn && { state: "on" }
1015
+ };
1016
+ if (options.json === true) {
1017
+ console.log(JSON.stringify(result, null, 2));
1018
+ return;
1019
+ }
1020
+ console.log(`${chalk3.green("set")} pentair.circuit.${circuitId}.runtime=${runTimeMinutes}m`);
1021
+ if (turnOn) console.log(`${chalk3.green("set")} pentair.circuit.${circuitId}.state=on`);
1022
+ }
1023
+
1024
+ // src/actions/witness/discoverPlugins.ts
1025
+ import {
1026
+ existsSync as existsSync3,
1027
+ readdirSync,
1028
+ readFileSync as readFileSync4
1029
+ } from "node:fs";
1030
+ import { homedir as homedir3 } from "node:os";
1031
+ import path3 from "node:path";
1032
+ var PLUGIN_PREFIX_SCOPED = /^@(ariestools|xylabs)\/aries-witness-.+/;
1033
+ var PLUGIN_PREFIX_UNSCOPED = /^aries-witness-.+/;
1034
+ var CONFIG_PATH = path3.join(homedir3(), ".aries", "witness-plugins.json");
1035
+ function isWitnessPlugin(value) {
1036
+ if (typeof value !== "object" || value === null) return false;
1037
+ const obj = value;
1038
+ if (typeof obj.meta !== "object" || obj.meta === null) return false;
1039
+ const meta = obj.meta;
1040
+ return typeof meta.name === "string" && typeof meta.description === "string" && typeof meta.schema === "string" && typeof obj.observe === "function";
1041
+ }
1042
+ function readConfigPlugins() {
1043
+ if (!existsSync3(CONFIG_PATH)) return [];
1044
+ try {
1045
+ const raw = readFileSync4(CONFIG_PATH, "utf8");
1046
+ const parsed = JSON.parse(raw);
1047
+ const plugins = parsed.plugins;
1048
+ return Array.isArray(plugins) ? plugins : [];
1049
+ } catch {
1050
+ return [];
1051
+ }
1052
+ }
1053
+ function scanConventionPlugins() {
1054
+ const packageNames = [];
1055
+ const nodeModulesPath = path3.resolve("node_modules");
1056
+ if (!existsSync3(nodeModulesPath)) return packageNames;
1057
+ for (const scope of ["@ariestools", "@xylabs"]) {
1058
+ const scopePath = path3.join(nodeModulesPath, scope);
1059
+ if (!existsSync3(scopePath)) continue;
1060
+ for (const entry of readdirSync(scopePath)) {
1061
+ const fullName = `${scope}/${entry}`;
1062
+ if (PLUGIN_PREFIX_SCOPED.test(fullName)) {
1063
+ packageNames.push(fullName);
1064
+ }
1065
+ }
1066
+ }
1067
+ for (const entry of readdirSync(nodeModulesPath)) {
1068
+ if (PLUGIN_PREFIX_UNSCOPED.test(entry)) {
1069
+ packageNames.push(entry);
1070
+ }
1071
+ }
1072
+ return packageNames;
1073
+ }
1074
+ async function discoverPlugins() {
1075
+ const configNames = readConfigPlugins();
1076
+ const conventionNames = scanConventionPlugins();
1077
+ const allNames = [.../* @__PURE__ */ new Set([...configNames, ...conventionNames])];
1078
+ const plugins = [];
1079
+ for (const name of allNames) {
1080
+ try {
1081
+ const mod = await import(name);
1082
+ const plugin = mod.default ?? mod;
1083
+ if (isWitnessPlugin(plugin)) {
1084
+ plugins.push(plugin);
1085
+ }
1086
+ } catch {
1087
+ }
1088
+ }
1089
+ return plugins;
1090
+ }
1091
+
1092
+ // src/actions/witness/executeWitness.ts
1093
+ import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "node:fs";
1094
+ import { homedir as homedir4 } from "node:os";
1095
+ import path4 from "node:path";
1096
+ import { hashPayload } from "@ariestools/aries-witness-core";
1097
+
1098
+ // src/actions/witness/formatOutput.ts
1099
+ import { writeFileSync as writeFileSync3 } from "node:fs";
1100
+ import chalk4 from "chalk";
1101
+ function formatPayloadValue(value) {
1102
+ if (value === null || value === void 0) return JSON.stringify(value);
1103
+ switch (typeof value) {
1104
+ case "string":
1105
+ case "number":
1106
+ case "boolean":
1107
+ case "bigint": {
1108
+ return String(value);
1109
+ }
1110
+ default: {
1111
+ return JSON.stringify(value, null, 2);
1112
+ }
1113
+ }
1114
+ }
1115
+ function formatHuman(result) {
1116
+ console.log(chalk4.cyan(`
1117
+ Witness: ${result.witnessName}`));
1118
+ console.log(chalk4.gray(`Time: ${new Date(result.timestamp).toISOString()}`));
1119
+ if (result.boundWitness) {
1120
+ console.log(chalk4.yellow("\nBound Witness"));
1121
+ console.log(chalk4.gray(` payload_hashes: ${result.boundWitness.payload_hashes.length}`));
1122
+ console.log(chalk4.gray(` payload_schemas: ${result.boundWitness.payload_schemas.join(", ")}`));
1123
+ }
1124
+ for (const payload of result.payloads) {
1125
+ console.log(chalk4.green(`
1126
+ Schema: ${payload.schema}`));
1127
+ for (const [key, value] of Object.entries(payload)) {
1128
+ if (key === "schema") continue;
1129
+ console.log(` ${chalk4.white(key)}: ${formatPayloadValue(value)}`);
1130
+ }
1131
+ }
1132
+ if (result.binaries && result.binaries.length > 0) {
1133
+ console.log(chalk4.yellow(`
1134
+ Binaries: ${result.binaries.length}`));
1135
+ for (const bin of result.binaries) {
1136
+ console.log(` ${chalk4.gray(bin.hash.slice(0, 12))}... ${bin.mimeType} (${bin.size} bytes)`);
1137
+ if (bin.path) {
1138
+ console.log(` ${chalk4.gray(`saved: ${bin.path}`)}`);
1139
+ }
1140
+ }
1141
+ }
1142
+ console.log(chalk4.gray(`
1143
+ ${result.payloads.length} payload(s) observed`));
1144
+ }
1145
+ function toJson(result, pretty) {
1146
+ return JSON.stringify(result, null, pretty ? 2 : void 0);
1147
+ }
1148
+ function formatOutput(result, options) {
1149
+ if (options.output) {
1150
+ const json = toJson(result, options.pretty);
1151
+ writeFileSync3(options.output, json + "\n", "utf8");
1152
+ console.error(chalk4.green(`Written to ${options.output}`));
1153
+ return;
1154
+ }
1155
+ if (options.json) {
1156
+ console.log(toJson(result, options.pretty));
1157
+ return;
1158
+ }
1159
+ formatHuman(result);
1160
+ }
1161
+
1162
+ // src/actions/witness/executeWitness.ts
1163
+ function mimeToExtension(mimeType) {
1164
+ const map = {
1165
+ "application/json": "json",
1166
+ "application/pdf": "pdf",
1167
+ "image/gif": "gif",
1168
+ "image/jpeg": "jpg",
1169
+ "image/png": "png",
1170
+ "image/svg+xml": "svg",
1171
+ "image/webp": "webp",
1172
+ "text/html": "html",
1173
+ "text/plain": "txt"
1174
+ };
1175
+ return map[mimeType] ?? "bin";
1176
+ }
1177
+ function writeBinaries(binaries, binaryDir) {
1178
+ mkdirSync3(binaryDir, { recursive: true });
1179
+ const results = [];
1180
+ for (const { ref, data } of binaries) {
1181
+ const ext = mimeToExtension(ref.mimeType);
1182
+ const filePath = path4.join(binaryDir, `${ref.hash}.${ext}`);
1183
+ writeFileSync4(filePath, data);
1184
+ results.push({
1185
+ hash: ref.hash,
1186
+ mimeType: ref.mimeType,
1187
+ path: filePath,
1188
+ size: ref.size
1189
+ });
1190
+ }
1191
+ return results;
1192
+ }
1193
+ function buildBoundWitness(payloads) {
1194
+ return {
1195
+ addresses: [],
1196
+ payload_hashes: payloads.map((p) => hashPayload(p)),
1197
+ payload_schemas: payloads.map((p) => p.schema),
1198
+ previous_hashes: [null],
1199
+ schema: "network.xyo.boundwitness"
1200
+ };
1201
+ }
1202
+ async function executeWitness(plugin, argv) {
1203
+ const observation = await plugin.observe(argv);
1204
+ const defaultBinaryDir = path4.join(homedir4(), ".aries", "binaries");
1205
+ const binaryDir = argv["binary-dir"] ?? defaultBinaryDir;
1206
+ let savedBinaries;
1207
+ if (observation.binaries && observation.binaries.length > 0) {
1208
+ savedBinaries = writeBinaries(observation.binaries, binaryDir);
1209
+ }
1210
+ const result = {
1211
+ binaries: savedBinaries,
1212
+ payloads: observation.payloads,
1213
+ timestamp: Date.now(),
1214
+ witnessName: plugin.meta.name
1215
+ };
1216
+ if (argv["bound-witness"]) {
1217
+ result.boundWitness = buildBoundWitness(observation.payloads);
1218
+ }
1219
+ formatOutput(result, {
1220
+ json: argv.json ?? false,
1221
+ output: argv.output,
1222
+ pretty: argv.pretty ?? true
1223
+ });
1224
+ }
1225
+
1226
+ // src/actions/witness/registerWitnessCommand.ts
1227
+ function registerWitnessCommand(plugin) {
1228
+ return {
1229
+ builder: (yargs) => {
1230
+ yargs.option("json", {
1231
+ default: false,
1232
+ describe: "Output raw JSON",
1233
+ type: "boolean"
1234
+ }).option("output", {
1235
+ alias: "o",
1236
+ describe: "Write output to file",
1237
+ type: "string"
1238
+ }).option("pretty", {
1239
+ default: true,
1240
+ describe: "Pretty-print JSON output",
1241
+ type: "boolean"
1242
+ }).option("bound-witness", {
1243
+ default: false,
1244
+ describe: "Wrap payloads in a BoundWitness",
1245
+ type: "boolean"
1246
+ }).option("binary-dir", {
1247
+ describe: "Directory for binary artifacts (default: ~/.aries/binaries/)",
1248
+ type: "string"
1249
+ });
1250
+ if (plugin.builder) plugin.builder(yargs);
1251
+ return yargs;
1252
+ },
1253
+ command: plugin.meta.name,
1254
+ describe: plugin.meta.description,
1255
+ handler: async (argv) => {
1256
+ await executeWitness(plugin, argv);
1257
+ }
1258
+ };
1259
+ }
1260
+
1261
+ // src/commands/bankCommand.ts
1262
+ import { noopHandler } from "@ariestools/aries-cli-core";
1263
+ var capitalOneConfigGetCommand = {
1264
+ command: "get [key]",
1265
+ describe: "Read Capital One bank witness config",
1266
+ builder: (yargs) => yargs.positional("key", {
1267
+ describe: "Config key; omit to list all Capital One config",
1268
+ type: "string"
1269
+ }).option("json", {
1270
+ default: false,
1271
+ describe: "Output as JSON",
1272
+ type: "boolean"
1273
+ }).option("show-secrets", {
1274
+ default: false,
1275
+ describe: "Show secret values instead of redacting them",
1276
+ type: "boolean"
1277
+ }),
1278
+ handler: (argv) => {
1279
+ const keyArg = argv.key;
1280
+ capitalOneConfigGet({
1281
+ ...keyArg && { key: normalizeCapitalOneConfigKey(keyArg) },
1282
+ json: argv.json === true,
1283
+ showSecrets: argv["show-secrets"] === true
1284
+ });
1285
+ }
1286
+ };
1287
+ var capitalOneConfigSetCommand = {
1288
+ command: "set <key> <value>",
1289
+ describe: "Set Capital One bank witness config",
1290
+ builder: (yargs) => yargs.positional("key", {
1291
+ demandOption: true,
1292
+ describe: "Config key",
1293
+ type: "string"
1294
+ }).positional("value", {
1295
+ demandOption: true,
1296
+ describe: "New value",
1297
+ type: "string"
1298
+ }),
1299
+ handler: (argv) => {
1300
+ capitalOneConfigSet({
1301
+ key: normalizeCapitalOneConfigKey(argv.key),
1302
+ value: argv.value
1303
+ });
1304
+ }
1305
+ };
1306
+ var capitalOneConfigCommand = {
1307
+ command: "config",
1308
+ describe: "Manage Capital One bank witness config",
1309
+ builder: (yargs) => yargs.command(capitalOneConfigGetCommand).command(capitalOneConfigSetCommand).demandCommand(1).strictCommands(),
1310
+ handler: noopHandler
1311
+ };
1312
+ var capitalOneCommand = {
1313
+ command: "capitalone",
1314
+ describe: "Manage Capital One bank witness settings",
1315
+ builder: (yargs) => yargs.command(capitalOneConfigCommand).demandCommand(1).strictCommands(),
1316
+ handler: noopHandler
1317
+ };
1318
+ var bankCommand = {
1319
+ command: "bank",
1320
+ describe: "Manage bank witness settings",
1321
+ builder: (yargs) => yargs.command(capitalOneCommand).demandCommand(1).strictCommands(),
1322
+ handler: noopHandler
1323
+ };
1324
+
1325
+ // src/commands/pentairCommand.ts
1326
+ import { noopHandler as noopHandler2 } from "@ariestools/aries-cli-core";
1327
+ function addConnectionOptions(yargs) {
1328
+ return yargs.option("address", {
1329
+ describe: "ScreenLogic adapter IP for direct connection",
1330
+ type: "string"
1331
+ }).option("port", {
1332
+ describe: "ScreenLogic adapter TCP port for direct connection",
1333
+ type: "number"
1334
+ }).option("password", {
1335
+ describe: "ScreenLogic password for direct connection",
1336
+ type: "string"
1337
+ }).option("system-name", {
1338
+ describe: "ScreenLogic system name for direct connection",
1339
+ type: "string"
1340
+ }).option("search-timeout", {
1341
+ describe: "Milliseconds to wait for each UDP discovery attempt",
1342
+ type: "number"
1343
+ }).option("search-attempts", {
1344
+ describe: "Number of UDP discovery attempts before failing",
1345
+ type: "number"
1346
+ }).option("response-timeout", {
1347
+ describe: "Milliseconds to wait for each ScreenLogic command response",
1348
+ type: "number"
1349
+ });
1350
+ }
1351
+ var pentairConfigGetCommand = {
1352
+ command: "get [key]",
1353
+ describe: "Read Pentair ScreenLogic config",
1354
+ builder: (yargs) => yargs.positional("key", {
1355
+ type: "string",
1356
+ describe: "Config key; omit to list all Pentair config"
1357
+ }).option("json", {
1358
+ type: "boolean",
1359
+ default: false,
1360
+ describe: "Output as JSON"
1361
+ }).option("show-secrets", {
1362
+ type: "boolean",
1363
+ default: false,
1364
+ describe: "Show secret values instead of redacting them"
1365
+ }),
1366
+ handler: (argv) => {
1367
+ const keyArg = argv.key;
1368
+ pentairConfigGet({
1369
+ ...keyArg && { key: normalizePentairConfigKey(keyArg) },
1370
+ json: argv.json === true,
1371
+ showSecrets: argv["show-secrets"] === true
1372
+ });
1373
+ }
1374
+ };
1375
+ var pentairConfigSetCommand = {
1376
+ command: "set <key> <value>",
1377
+ describe: "Set Pentair ScreenLogic config",
1378
+ builder: (yargs) => yargs.positional("key", {
1379
+ type: "string",
1380
+ demandOption: true,
1381
+ describe: "Config key"
1382
+ }).positional("value", {
1383
+ type: "string",
1384
+ demandOption: true,
1385
+ describe: "New value"
1386
+ }),
1387
+ handler: (argv) => {
1388
+ pentairConfigSet({
1389
+ key: normalizePentairConfigKey(argv.key),
1390
+ value: argv.value
1391
+ });
1392
+ }
1393
+ };
1394
+ var pentairConfigCommand = {
1395
+ command: "config",
1396
+ describe: "Manage Pentair ScreenLogic config",
1397
+ builder: (yargs) => yargs.command(pentairConfigGetCommand).command(pentairConfigSetCommand).demandCommand(1).strictCommands(),
1398
+ handler: noopHandler2
1399
+ };
1400
+ var pentairCircuitRuntimeSetCommand = {
1401
+ command: "set",
1402
+ describe: "Set a circuit egg-timer runtime",
1403
+ builder: (yargs) => addConnectionOptions(yargs).option("circuit-id", {
1404
+ type: "number",
1405
+ demandOption: true,
1406
+ describe: "Pentair circuit id to update"
1407
+ }).option("hours", {
1408
+ type: "number",
1409
+ describe: "Runtime in hours"
1410
+ }).option("minutes", {
1411
+ type: "number",
1412
+ describe: "Runtime in minutes"
1413
+ }).option("on", {
1414
+ type: "boolean",
1415
+ default: false,
1416
+ describe: "Turn the circuit on after setting the runtime"
1417
+ }).option("json", {
1418
+ type: "boolean",
1419
+ default: false,
1420
+ describe: "Output as JSON"
1421
+ }).check((argv) => {
1422
+ const hasHours = argv.hours !== void 0;
1423
+ const hasMinutes = argv.minutes !== void 0;
1424
+ if (hasHours === hasMinutes) throw new Error("Use exactly one of --hours or --minutes");
1425
+ return true;
1426
+ }),
1427
+ handler: async (argv) => {
1428
+ await setPentairCircuitRuntime({
1429
+ "address": argv.address,
1430
+ "circuit-id": argv["circuit-id"],
1431
+ "hours": argv.hours,
1432
+ "json": argv.json === true,
1433
+ "minutes": argv.minutes,
1434
+ "on": argv.on === true,
1435
+ "password": argv.password,
1436
+ "port": argv.port,
1437
+ "response-timeout": argv["response-timeout"],
1438
+ "search-attempts": argv["search-attempts"],
1439
+ "search-timeout": argv["search-timeout"],
1440
+ "system-name": argv["system-name"]
1441
+ });
1442
+ }
1443
+ };
1444
+ var pentairCircuitRuntimeCommand = {
1445
+ command: "runtime",
1446
+ describe: "Manage circuit egg-timer runtimes",
1447
+ builder: (yargs) => yargs.command(pentairCircuitRuntimeSetCommand).demandCommand(1).strictCommands(),
1448
+ handler: noopHandler2
1449
+ };
1450
+ var pentairCircuitCommand = {
1451
+ command: "circuit",
1452
+ describe: "Manage Pentair ScreenLogic circuits",
1453
+ builder: (yargs) => yargs.command(pentairCircuitRuntimeCommand).demandCommand(1).strictCommands(),
1454
+ handler: noopHandler2
1455
+ };
1456
+ var pentairCommand = {
1457
+ command: "pentair",
1458
+ describe: "Manage Pentair ScreenLogic systems",
1459
+ builder: (yargs) => yargs.command(pentairCircuitCommand).command(pentairConfigCommand).demandCommand(1).strictCommands(),
1460
+ handler: noopHandler2
1461
+ };
1462
+
1463
+ // src/commands/witnessAppListCommand.ts
1464
+ import { homedir as homedir6 } from "node:os";
1465
+ import PATH2 from "node:path";
1466
+
1467
+ // src/actions/witness/app/scanApps.ts
1468
+ import { execFile as execFileCb } from "node:child_process";
1469
+ import { existsSync as existsSync4, readdirSync as readdirSync2 } from "node:fs";
1470
+ import { homedir as homedir5 } from "node:os";
1471
+ import PATH from "node:path";
1472
+ import { promisify } from "node:util";
1473
+ var execFile = promisify(execFileCb);
1474
+ async function readPlist(appPath) {
1475
+ const plistPath = PATH.join(appPath, "Contents", "Info.plist");
1476
+ try {
1477
+ const { stdout } = await execFile("plutil", ["-convert", "json", "-o", "-", plistPath]);
1478
+ return JSON.parse(stdout);
1479
+ } catch {
1480
+ return null;
1481
+ }
1482
+ }
1483
+ async function readCodesign(appPath) {
1484
+ try {
1485
+ const { stderr } = await execFile("codesign", ["-dv", "--verbose=2", appPath]);
1486
+ const authorityMatch = /^Authority=(.+)$/m.exec(stderr);
1487
+ const teamMatch = /^TeamIdentifier=(.+)$/m.exec(stderr);
1488
+ let developer = null;
1489
+ let obtainedFrom = "unknown";
1490
+ if (authorityMatch) {
1491
+ const authority = authorityMatch[1];
1492
+ const devIdMatch = /^Developer ID Application: (.+?)(?:\s*\([A-Z\d]+\))?$/.exec(authority);
1493
+ if (devIdMatch) {
1494
+ developer = devIdMatch[1];
1495
+ obtainedFrom = "developer-id";
1496
+ } else if (authority === "Software Signing") {
1497
+ developer = "Apple";
1498
+ obtainedFrom = "apple-system";
1499
+ } else if (authority === "Apple Mac OS Application Signing") {
1500
+ obtainedFrom = "mac-app-store";
1501
+ } else {
1502
+ developer = authority;
1503
+ }
1504
+ }
1505
+ const teamId = teamMatch?.[1];
1506
+ const teamIdentifier = teamId && teamId !== "not set" ? teamId : null;
1507
+ return {
1508
+ developer,
1509
+ obtainedFrom,
1510
+ teamIdentifier
1511
+ };
1512
+ } catch {
1513
+ return {
1514
+ developer: null,
1515
+ obtainedFrom: "unknown",
1516
+ teamIdentifier: null
1517
+ };
1518
+ }
1519
+ }
1520
+ async function lookupAppStoreDeveloper(bundleId) {
1521
+ try {
1522
+ const url = `https://itunes.apple.com/lookup?bundleId=${encodeURIComponent(bundleId)}&entity=macSoftware`;
1523
+ const response = await fetch(url);
1524
+ const data = await response.json();
1525
+ if (data.resultCount > 0 && data.results[0].artistName) {
1526
+ return data.results[0].artistName;
1527
+ }
1528
+ return null;
1529
+ } catch {
1530
+ return null;
1531
+ }
1532
+ }
1533
+ function appNameFromDir(dirName) {
1534
+ return dirName.replace(/\.app$/i, "");
1535
+ }
1536
+ async function scanDirectory(dir) {
1537
+ if (!existsSync4(dir.path)) return [];
1538
+ const entries = readdirSync2(dir.path, { withFileTypes: true });
1539
+ const appEntries = entries.filter((e) => e.name.endsWith(".app") && e.isDirectory());
1540
+ const results = await Promise.allSettled(
1541
+ appEntries.map(async (entry) => {
1542
+ const appPath = PATH.join(dir.path, entry.name);
1543
+ const [plist, codesign] = await Promise.all([
1544
+ readPlist(appPath),
1545
+ readCodesign(appPath)
1546
+ ]);
1547
+ const bundleIdentifier = plist?.CFBundleIdentifier ?? null;
1548
+ let { developer } = codesign;
1549
+ if (!developer && codesign.obtainedFrom === "mac-app-store" && bundleIdentifier) {
1550
+ developer = await lookupAppStoreDeveloper(bundleIdentifier);
1551
+ }
1552
+ const name = plist?.CFBundleDisplayName ?? plist?.CFBundleName ?? appNameFromDir(entry.name);
1553
+ return {
1554
+ buildVersion: plist?.CFBundleVersion ?? null,
1555
+ bundleIdentifier,
1556
+ copyright: plist?.NSHumanReadableCopyright ?? null,
1557
+ developer,
1558
+ name,
1559
+ obtainedFrom: codesign.obtainedFrom,
1560
+ path: appPath,
1561
+ scope: dir.scope,
1562
+ teamIdentifier: codesign.teamIdentifier,
1563
+ version: plist?.CFBundleShortVersionString ?? null
1564
+ };
1565
+ })
1566
+ );
1567
+ return results.filter((r) => r.status === "fulfilled").map((r) => r.value);
1568
+ }
1569
+ async function scanApps() {
1570
+ if (process.platform !== "darwin") return [];
1571
+ const dirs = [
1572
+ { path: "/Applications", scope: "global" },
1573
+ { path: "/System/Applications", scope: "system" },
1574
+ { path: PATH.join(homedir5(), "Applications"), scope: "user" }
1575
+ ];
1576
+ const results = await Promise.all(dirs.map((d) => scanDirectory(d)));
1577
+ return results.flat().toSorted((a, b) => a.name.localeCompare(b.name));
1578
+ }
1579
+
1580
+ // src/actions/witness/app/types.ts
1581
+ var APP_LIST_SCHEMA = "network.xyo.macos.app.list";
1582
+ var APP_INFO_SCHEMA = "network.xyo.macos.app.info";
1583
+
1584
+ // src/commands/witnessAppListCommand.ts
1585
+ var witnessAppListCommand = {
1586
+ command: "list",
1587
+ describe: "List installed macOS applications",
1588
+ builder: (yargs) => yargs.option("json", {
1589
+ default: false,
1590
+ describe: "Output raw JSON",
1591
+ type: "boolean"
1592
+ }).option("output", {
1593
+ alias: "o",
1594
+ describe: "Write output to file",
1595
+ type: "string"
1596
+ }).option("pretty", {
1597
+ default: true,
1598
+ describe: "Pretty-print JSON output",
1599
+ type: "boolean"
1600
+ }).option("bound-witness", {
1601
+ default: false,
1602
+ describe: "Wrap payloads in a BoundWitness",
1603
+ type: "boolean"
1604
+ }).option("binary-dir", {
1605
+ describe: "Directory for binary artifacts (default: ~/.aries/binaries/)",
1606
+ type: "string"
1607
+ }).option("scope", {
1608
+ choices: ["all", "global", "system", "user"],
1609
+ default: "all",
1610
+ describe: "Filter by install scope",
1611
+ type: "string"
1612
+ }),
1613
+ handler: async (argv) => {
1614
+ const scope = argv.scope;
1615
+ const allApps = await scanApps();
1616
+ const apps = scope === "all" ? allApps : allApps.filter((a) => a.scope === scope);
1617
+ const searchPaths = [
1618
+ "/Applications",
1619
+ "/System/Applications",
1620
+ PATH2.join(homedir6(), "Applications")
1621
+ ];
1622
+ const plugin = {
1623
+ meta: {
1624
+ description: "List installed macOS applications",
1625
+ name: "app-list",
1626
+ schema: APP_LIST_SCHEMA
1627
+ },
1628
+ observe: async () => ({
1629
+ payloads: [
1630
+ {
1631
+ appCount: apps.length,
1632
+ schema: APP_LIST_SCHEMA,
1633
+ scannedAt: Date.now(),
1634
+ searchPaths
1635
+ },
1636
+ ...apps.map((app) => ({
1637
+ buildVersion: app.buildVersion,
1638
+ bundleIdentifier: app.bundleIdentifier,
1639
+ copyright: app.copyright,
1640
+ developer: app.developer,
1641
+ name: app.name,
1642
+ obtainedFrom: app.obtainedFrom,
1643
+ path: app.path,
1644
+ schema: APP_INFO_SCHEMA,
1645
+ scope: app.scope,
1646
+ teamIdentifier: app.teamIdentifier,
1647
+ version: app.version
1648
+ }))
1649
+ ]
1650
+ })
1651
+ };
1652
+ await executeWitness(plugin, argv);
1653
+ }
1654
+ };
1655
+
1656
+ // src/commands/witnessAppCommand.ts
1657
+ var witnessAppCommand = {
1658
+ command: "app",
1659
+ describe: "Inspect installed applications",
1660
+ builder: (yargs) => yargs.command(witnessAppListCommand).demandCommand(1).strictCommands(),
1661
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
1662
+ handler: () => {
1663
+ }
1664
+ };
1665
+
1666
+ // src/commands/witnessCommand.ts
1667
+ import { noopHandler as noopHandler3 } from "@ariestools/aries-cli-core";
1668
+
1669
+ // src/actions/witness/witnesses/bank.ts
1670
+ var BANK_WITNESS_SCHEMA = "network.xyo.bank.account-snapshot";
1671
+ var bankWitness = {
1672
+ builder: (yargs) => {
1673
+ return yargs.option("provider", {
1674
+ choices: ["capitalone"],
1675
+ default: "capitalone",
1676
+ describe: "Bank provider to witness",
1677
+ type: "string"
1678
+ }).option("resource", {
1679
+ choices: ["accounts", "products"],
1680
+ default: "accounts",
1681
+ describe: "Capital One resource to witness",
1682
+ type: "string"
1683
+ }).option("from", {
1684
+ describe: "Transaction window start passed to the provider endpoint",
1685
+ type: "string"
1686
+ }).option("to", {
1687
+ describe: "Transaction window end passed to the provider endpoint",
1688
+ type: "string"
1689
+ }).option("account-id", {
1690
+ describe: "Provider account id to filter balances and transactions",
1691
+ type: "string"
1692
+ }).option("include-raw", {
1693
+ default: false,
1694
+ describe: "Store raw provider JSON responses as witness binary artifacts",
1695
+ type: "boolean"
1696
+ }).option("redaction", {
1697
+ choices: ["default", "none"],
1698
+ default: "default",
1699
+ describe: "Redaction mode for provider account and transaction ids",
1700
+ type: "string"
1701
+ }).option("env", {
1702
+ choices: ["sandbox", "production"],
1703
+ describe: "Capital One environment label",
1704
+ type: "string"
1705
+ }).option("base-url", {
1706
+ describe: "Capital One API base URL",
1707
+ type: "string"
1708
+ }).option("token-url", {
1709
+ describe: "Capital One OAuth client-credentials token URL",
1710
+ type: "string"
1711
+ }).option("access-token", {
1712
+ describe: "Pre-issued Capital One bearer token; not persisted",
1713
+ type: "string"
1714
+ }).option("client-id", {
1715
+ describe: "Capital One OAuth client id",
1716
+ type: "string"
1717
+ }).option("client-secret", {
1718
+ describe: "Capital One OAuth client secret",
1719
+ type: "string"
1720
+ }).option("scope", {
1721
+ describe: "Optional Capital One OAuth client-credentials scope",
1722
+ type: "string"
1723
+ }).option("api-version", {
1724
+ default: "5",
1725
+ describe: "Capital One products API media-type version",
1726
+ type: "string"
1727
+ }).option("operation", {
1728
+ choices: ["retrieve-products", "retail-products", "search"],
1729
+ default: "search",
1730
+ describe: "Capital One products operation",
1731
+ type: "string"
1732
+ }).option("product-id", {
1733
+ describe: "Capital One deposit product id for product-specific operations (default: ~)",
1734
+ type: "string"
1735
+ }).option("body", {
1736
+ describe: "JSON request body for Capital One products POST operations",
1737
+ type: "string"
1738
+ }).option("body-file", {
1739
+ describe: "Path to JSON request body for Capital One products POST operations",
1740
+ type: "string"
1741
+ }).option("accounts-path", {
1742
+ describe: "Capital One accounts endpoint path, relative to --base-url",
1743
+ type: "string"
1744
+ }).option("balances-path", {
1745
+ describe: "Capital One balances endpoint path; may include {accountId}",
1746
+ type: "string"
1747
+ }).option("transactions-path", {
1748
+ describe: "Capital One transactions endpoint path; may include {accountId}",
1749
+ type: "string"
1750
+ });
1751
+ },
1752
+ meta: {
1753
+ description: "Capture bank accounts, balances, transactions, or product catalog data",
1754
+ name: "bank",
1755
+ schema: BANK_WITNESS_SCHEMA
1756
+ },
1757
+ observe: async (args) => {
1758
+ const provider = args.provider ?? "capitalone";
1759
+ if (provider !== "capitalone") throw new Error(`Unsupported bank provider: ${provider}`);
1760
+ const resource = args.resource ?? "accounts";
1761
+ if (resource === "products") return capitalOneProductsProvider.observe(args);
1762
+ if (resource !== "accounts") throw new Error(`Unsupported Capital One bank resource: ${resource}`);
1763
+ return capitalOneProvider.observe(args);
1764
+ }
1765
+ };
1766
+
1767
+ // src/actions/witness/witnesses/netflixCatalog.ts
1768
+ import { readFile } from "node:fs/promises";
1769
+ import PATH4 from "node:path";
1770
+ import {
1771
+ ingestStreamingCatalogSnapshot
1772
+ } from "@ariestools/aries-streaming-catalog-core";
1773
+ import { hashBinary as hashBinary4 } from "@ariestools/aries-witness-core";
1774
+
1775
+ // src/actions/witness/witnesses/netflixCatalogBrowser.ts
1776
+ import { spawn } from "node:child_process";
1777
+ import FS from "node:fs";
1778
+ import NET from "node:net";
1779
+ import OS from "node:os";
1780
+ import PATH3 from "node:path";
1781
+ async function captureNetflixPagesWithChrome(urls, options) {
1782
+ const chromePath = resolveChromePath(options.chromePath);
1783
+ const port = await freePort();
1784
+ ensureDir(options.profileDir);
1785
+ const chrome = launchChrome(chromePath, port, options);
1786
+ const readStderr = collectProcessStderr(chrome);
1787
+ try {
1788
+ await waitForDevTools(port, options.devtoolsTimeoutMs, chrome, readStderr);
1789
+ const snapshots = [];
1790
+ for (const url of urls) {
1791
+ snapshots.push(...await captureUrl(port, url, options));
1792
+ }
1793
+ return snapshots;
1794
+ } finally {
1795
+ stopChrome(chrome);
1796
+ }
1797
+ }
1798
+ function defaultChromeProfileDir() {
1799
+ return PATH3.join(process.env.ARIES_HOME ?? PATH3.join(OS.homedir(), ".aries"), "browser-profiles", "netflix-catalog");
1800
+ }
1801
+ async function captureUrl(port, url, options) {
1802
+ const target = await openTarget(port, url);
1803
+ const client = await CdpClient.connect(target.webSocketDebuggerUrl);
1804
+ try {
1805
+ await client.send("Page.enable");
1806
+ await client.send("Runtime.enable");
1807
+ await sleep(options.waitMs);
1808
+ return await captureScrolledSnapshots(client, url, options);
1809
+ } finally {
1810
+ client.close();
1811
+ await closeTarget(port, target.id);
1812
+ }
1813
+ }
1814
+ async function captureScrolledSnapshots(client, url, options) {
1815
+ const snapshots = [];
1816
+ for (let index = 0; index <= options.scrolls; index += 1) {
1817
+ snapshots.push({
1818
+ content: await evaluateString(client, 'document.documentElement ? document.documentElement.outerHTML : ""'),
1819
+ sourceName: `browser:${url}#${index}`,
1820
+ sourceUrl: url
1821
+ });
1822
+ if (index < options.scrolls) {
1823
+ await client.send("Runtime.evaluate", {
1824
+ awaitPromise: true,
1825
+ expression: "window.scrollBy(0, Math.max(1, Math.floor(window.innerHeight * 0.85)))",
1826
+ returnByValue: true
1827
+ });
1828
+ await sleep(options.scrollWaitMs);
1829
+ }
1830
+ }
1831
+ return snapshots;
1832
+ }
1833
+ async function evaluateString(client, expression) {
1834
+ const result = await client.send("Runtime.evaluate", {
1835
+ awaitPromise: true,
1836
+ expression,
1837
+ returnByValue: true
1838
+ });
1839
+ if (result.exceptionDetails !== void 0) throw new Error(`Chrome evaluation failed for expression: ${expression}`);
1840
+ return typeof result.result?.value === "string" ? result.result.value : "";
1841
+ }
1842
+ function launchChrome(chromePath, port, options) {
1843
+ const args = [
1844
+ `--remote-debugging-port=${port}`,
1845
+ "--remote-debugging-address=127.0.0.1",
1846
+ `--user-data-dir=${options.profileDir}`,
1847
+ "--no-first-run",
1848
+ "--no-default-browser-check",
1849
+ `--window-size=${options.viewportWidth},${options.viewportHeight}`,
1850
+ ...options.headless ? ["--headless=new", "--disable-gpu"] : [],
1851
+ "about:blank"
1852
+ ];
1853
+ return spawn(chromePath, args, { stdio: ["ignore", "ignore", "pipe"] });
1854
+ }
1855
+ function collectProcessStderr(chrome) {
1856
+ let stderr = "";
1857
+ chrome.stderr?.setEncoding("utf8");
1858
+ chrome.stderr?.on("data", (chunk) => {
1859
+ stderr = `${stderr}${String(chunk)}`.slice(-4e3);
1860
+ });
1861
+ return () => stderr.trim();
1862
+ }
1863
+ function stopChrome(chrome) {
1864
+ if (chrome.exitCode !== null || chrome.signalCode !== null) return;
1865
+ chrome.kill("SIGTERM");
1866
+ }
1867
+ async function openTarget(port, url) {
1868
+ const endpoint = `http://127.0.0.1:${port}/json/new?${encodeURIComponent(url)}`;
1869
+ const response = await fetch(endpoint, { method: "PUT" });
1870
+ if (!response.ok) throw new Error(`Chrome failed to open ${url}: HTTP ${response.status}`);
1871
+ return response.json();
1872
+ }
1873
+ async function closeTarget(port, targetId) {
1874
+ try {
1875
+ await fetch(`http://127.0.0.1:${port}/json/close/${encodeURIComponent(targetId)}`);
1876
+ } catch {
1877
+ }
1878
+ }
1879
+ async function waitForDevTools(port, timeoutMs, chrome, readStderr) {
1880
+ const deadline = Date.now() + timeoutMs;
1881
+ while (Date.now() < deadline) {
1882
+ if (chrome.exitCode !== null || chrome.signalCode !== null) {
1883
+ throw chromeDevToolsError(`Chrome exited before exposing a DevTools endpoint (${exitStatus(chrome)}).`, readStderr);
1884
+ }
1885
+ try {
1886
+ const response = await fetch(`http://127.0.0.1:${port}/json/version`);
1887
+ if (response.ok) return;
1888
+ } catch {
1889
+ }
1890
+ await sleep(100);
1891
+ }
1892
+ throw chromeDevToolsError(`Chrome did not expose a DevTools endpoint within ${timeoutMs} ms.`, readStderr);
1893
+ }
1894
+ function chromeDevToolsError(message, readStderr) {
1895
+ const stderr = readStderr();
1896
+ return new Error(stderr.length === 0 ? message : `${message} Chrome stderr: ${stderr}`);
1897
+ }
1898
+ function exitStatus(chrome) {
1899
+ if (chrome.exitCode !== null) return `exit code ${chrome.exitCode}`;
1900
+ return `signal ${chrome.signalCode ?? "unknown"}`;
1901
+ }
1902
+ function resolveChromePath(chromePath) {
1903
+ if (chromePath !== void 0 && chromePath.length > 0) {
1904
+ if (FS.existsSync(chromePath)) return chromePath;
1905
+ throw new Error(`Chrome executable not found at --chrome-path: ${chromePath}`);
1906
+ }
1907
+ if (process.env.CHROME_PATH !== void 0 && process.env.CHROME_PATH.length > 0) {
1908
+ if (FS.existsSync(process.env.CHROME_PATH)) return process.env.CHROME_PATH;
1909
+ throw new Error(`Chrome executable not found at CHROME_PATH: ${process.env.CHROME_PATH}`);
1910
+ }
1911
+ const candidates = platformChromeCandidates();
1912
+ const found = candidates.find((candidate) => FS.existsSync(candidate));
1913
+ if (found !== void 0) return found;
1914
+ throw new Error("Chrome executable not found. Install Chrome/Chromium or pass --chrome-path with the executable path.");
1915
+ }
1916
+ function platformChromeCandidates() {
1917
+ if (process.platform === "darwin") {
1918
+ return [
1919
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
1920
+ PATH3.join(OS.homedir(), "Applications/Google Chrome.app/Contents/MacOS/Google Chrome"),
1921
+ "/Applications/Chromium.app/Contents/MacOS/Chromium"
1922
+ ];
1923
+ }
1924
+ if (process.platform === "win32") {
1925
+ return [
1926
+ process.env.PROGRAMFILES === void 0 ? void 0 : PATH3.join(process.env.PROGRAMFILES, "Google/Chrome/Application/chrome.exe"),
1927
+ process.env["PROGRAMFILES(X86)"] === void 0 ? void 0 : PATH3.join(process.env["PROGRAMFILES(X86)"], "Google/Chrome/Application/chrome.exe"),
1928
+ process.env.LOCALAPPDATA === void 0 ? void 0 : PATH3.join(process.env.LOCALAPPDATA, "Google/Chrome/Application/chrome.exe")
1929
+ ].filter((candidate) => candidate !== void 0);
1930
+ }
1931
+ return [
1932
+ "/usr/bin/google-chrome",
1933
+ "/usr/bin/google-chrome-stable",
1934
+ "/usr/bin/chromium",
1935
+ "/usr/bin/chromium-browser"
1936
+ ];
1937
+ }
1938
+ async function freePort() {
1939
+ const server = NET.createServer();
1940
+ await new Promise((resolve, reject) => {
1941
+ server.once("error", reject);
1942
+ server.listen(0, "127.0.0.1", resolve);
1943
+ });
1944
+ const address = server.address();
1945
+ await new Promise((resolve) => server.close(() => resolve()));
1946
+ if (typeof address === "object" && address !== null) return address.port;
1947
+ throw new Error("Failed to allocate a local Chrome debugging port.");
1948
+ }
1949
+ function ensureDir(dir) {
1950
+ FS.mkdirSync(dir, { recursive: true, mode: 448 });
1951
+ }
1952
+ function sleep(ms) {
1953
+ return new Promise((resolve) => setTimeout(resolve, ms));
1954
+ }
1955
+ var CdpClient = class _CdpClient {
1956
+ #nextId = 1;
1957
+ #pending = /* @__PURE__ */ new Map();
1958
+ #socket;
1959
+ constructor(socket) {
1960
+ this.#socket = socket;
1961
+ socket.addEventListener("message", (event) => this.handleMessage(event.data));
1962
+ socket.addEventListener("error", () => this.rejectAll(new Error("Chrome DevTools WebSocket failed.")));
1963
+ socket.addEventListener("close", () => this.rejectAll(new Error("Chrome DevTools WebSocket closed.")));
1964
+ }
1965
+ static async connect(url) {
1966
+ if (typeof WebSocket !== "function") {
1967
+ throw new TypeError("Browser capture requires a Node.js runtime with global WebSocket support.");
1968
+ }
1969
+ const socket = await new Promise((resolve, reject) => {
1970
+ const ws = new WebSocket(url);
1971
+ ws.addEventListener("open", () => resolve(ws), { once: true });
1972
+ ws.addEventListener("error", () => reject(new Error("Failed to connect to Chrome DevTools WebSocket.")), { once: true });
1973
+ });
1974
+ return new _CdpClient(socket);
1975
+ }
1976
+ close() {
1977
+ this.#socket.close();
1978
+ }
1979
+ send(method, params) {
1980
+ const id = this.#nextId;
1981
+ this.#nextId += 1;
1982
+ const promise = new Promise((resolve, reject) => {
1983
+ this.#pending.set(id, {
1984
+ reject,
1985
+ resolve: (value) => resolve(value)
1986
+ });
1987
+ });
1988
+ const message = params === void 0 ? {
1989
+ id,
1990
+ method
1991
+ } : {
1992
+ id,
1993
+ method,
1994
+ params
1995
+ };
1996
+ this.#socket.send(JSON.stringify(message));
1997
+ return promise;
1998
+ }
1999
+ handleMessage(data) {
2000
+ const message = JSON.parse(messageText(data));
2001
+ if (message.id === void 0) return;
2002
+ const pending = this.#pending.get(message.id);
2003
+ if (pending === void 0) return;
2004
+ this.#pending.delete(message.id);
2005
+ if (message.error === void 0) {
2006
+ pending.resolve(message.result);
2007
+ } else {
2008
+ pending.reject(new Error(`Chrome DevTools error ${message.error.code}: ${message.error.message}`));
2009
+ }
2010
+ }
2011
+ rejectAll(error) {
2012
+ for (const pending of this.#pending.values()) {
2013
+ pending.reject(error);
2014
+ }
2015
+ this.#pending.clear();
2016
+ }
2017
+ };
2018
+ function messageText(data) {
2019
+ if (typeof data === "string") return data;
2020
+ if (data instanceof ArrayBuffer) return Buffer.from(data).toString("utf8");
2021
+ return String(data);
2022
+ }
2023
+
2024
+ // src/actions/witness/witnesses/netflixCatalogDetail.ts
2025
+ import { hashBinary as hashBinary3 } from "@ariestools/aries-witness-core";
2026
+
2027
+ // src/actions/witness/witnesses/netflixCatalogExtract.ts
2028
+ var TITLE_URL_PATTERN = /(?:href|src)=["'][^"']*(?:\/(?:watch|title)\/|netflix:\/\/title\/)(\d+)[^"']*["']/giu;
2029
+ var QUALITY_BADGES = [
2030
+ { badge: "Dolby Vision", pattern: /\bDolby\s+Vision\b/iu },
2031
+ { badge: "Atmos", pattern: /\b(?:Dolby\s+)?Atmos\b/iu },
2032
+ { badge: "HDR", pattern: /\bHDR\b/iu },
2033
+ { badge: "4K", pattern: /\b(?:4K|Ultra\s+HD|UHD)\b/iu },
2034
+ { badge: "HD", pattern: /\bHD\b/iu }
2035
+ ];
2036
+ var WEAK_TITLE_PATTERNS = [
2037
+ /^-/u,
2038
+ /\baudio description\b/iu,
2039
+ /\b(?:episodes|more info|netflix home|sign in|subtitles|trailers? & more)\b/iu,
2040
+ /\b(?:add to my list|remove from my list|start with)\b/iu
2041
+ ];
2042
+ function extractNetflixCatalogObservations(html, context) {
2043
+ const observations = /* @__PURE__ */ new Map();
2044
+ for (const match of html.matchAll(TITLE_URL_PATTERN)) {
2045
+ const sourceTitleId = match[1];
2046
+ if (sourceTitleId === void 0) continue;
2047
+ const windowText = nearbyHtml(html, match.index ?? 0);
2048
+ const title = extractBestTitle(windowText) ?? fallbackTitle(sourceTitleId);
2049
+ const existing = observations.get(sourceTitleId);
2050
+ const badges = qualityBadges(windowText);
2051
+ if (existing && !isWeakNetflixTitle(existing.title, sourceTitleId)) {
2052
+ existing.quality = mergeQualityBadges(existing.quality?.badges ?? [], badges);
2053
+ continue;
2054
+ }
2055
+ observations.set(sourceTitleId, {
2056
+ available: true,
2057
+ evidence: {
2058
+ rawResponseHash: context.rawResponseHash,
2059
+ region: context.region,
2060
+ source: context.sourceName
2061
+ },
2062
+ observedAt: context.observedAt,
2063
+ provider: "netflix",
2064
+ quality: mergeQualityBadges(existing?.quality?.badges ?? [], badges),
2065
+ region: context.region,
2066
+ sourceKind: "browser-witness",
2067
+ sourceTitleId,
2068
+ title,
2069
+ type: "unknown",
2070
+ url: netflixTitleUrl(sourceTitleId)
2071
+ });
2072
+ }
2073
+ return [...observations.values()].toSorted((a, b) => a.sourceTitleId.localeCompare(b.sourceTitleId));
2074
+ }
2075
+ function extractNetflixTitleDetails(html, context) {
2076
+ const jsonLdRecords = extractJsonLdRecords(html);
2077
+ const detail = {
2078
+ evidence: {
2079
+ rawResponseHash: context.rawResponseHash,
2080
+ region: context.region,
2081
+ source: context.sourceName
2082
+ },
2083
+ quality: mergeQualityBadges([], qualityBadges(html)),
2084
+ sourceTitleId: context.sourceTitleId,
2085
+ url: netflixTitleUrl(context.sourceTitleId)
2086
+ };
2087
+ assignDetailString(detail, "title", extractDetailTitle(html, jsonLdRecords));
2088
+ assignDetailString(detail, "type", extractTitleType(html, jsonLdRecords));
2089
+ assignDetailString(detail, "maturityRating", firstString3([
2090
+ ...jsonLdRecords.map((record) => record.contentRating),
2091
+ jsonStringValue(html, "contentRating"),
2092
+ jsonStringValue(html, "maturityRating"),
2093
+ jsonStringValue(html, "rating")
2094
+ ]));
2095
+ assignDetailString(detail, "synopsis", firstString3([
2096
+ ...jsonLdRecords.map((record) => record.description),
2097
+ metaContent(html, "description"),
2098
+ metaContent(html, "og:description"),
2099
+ jsonStringValue(html, "synopsis"),
2100
+ jsonStringValue(html, "description"),
2101
+ jsonStringValue(html, "summary")
2102
+ ]));
2103
+ const year = extractYear(html, jsonLdRecords);
2104
+ if (year !== void 0) detail.year = year;
2105
+ const runtime = extractRuntimeMinutes(html, jsonLdRecords);
2106
+ if (runtime !== void 0) detail.runtime = runtime;
2107
+ assignDetailStrings(detail, "genres", [
2108
+ ...jsonLdRecords.flatMap((record) => stringsFromUnknown(record.genre)),
2109
+ ...jsonStringArrayValues(html, "genre"),
2110
+ ...jsonStringArrayValues(html, "genres")
2111
+ ]);
2112
+ assignDetailStrings(detail, "cast", [
2113
+ ...jsonLdRecords.flatMap((record) => namesFromUnknown(record.actor)),
2114
+ ...jsonLdRecords.flatMap((record) => namesFromUnknown(record.actors)),
2115
+ ...jsonStringArrayValues(html, "cast")
2116
+ ]);
2117
+ assignDetailStrings(detail, "creators", [
2118
+ ...jsonLdRecords.flatMap((record) => namesFromUnknown(record.creator)),
2119
+ ...jsonLdRecords.flatMap((record) => namesFromUnknown(record.creators)),
2120
+ ...jsonLdRecords.flatMap((record) => namesFromUnknown(record.director)),
2121
+ ...jsonLdRecords.flatMap((record) => namesFromUnknown(record.directors)),
2122
+ ...jsonStringArrayValues(html, "creators"),
2123
+ ...jsonStringArrayValues(html, "directors")
2124
+ ]);
2125
+ return detail;
2126
+ }
2127
+ function isWeakNetflixTitle(title, sourceTitleId) {
2128
+ const cleaned = cleanTitleText(title);
2129
+ return cleaned === void 0 || cleaned === fallbackTitle(sourceTitleId) || WEAK_TITLE_PATTERNS.some((pattern) => pattern.test(cleaned));
2130
+ }
2131
+ function netflixTitleIdFromUrl(url) {
2132
+ return /(?:\/(?:watch|title)\/|netflix:\/\/title\/)(\d+)/iu.exec(url)?.[1];
2133
+ }
2134
+ function netflixTitleUrl(sourceTitleId) {
2135
+ return `https://www.netflix.com/title/${sourceTitleId}`;
2136
+ }
2137
+ function nearbyHtml(html, index) {
2138
+ return html.slice(Math.max(0, index - 1200), Math.min(html.length, index + 1200));
2139
+ }
2140
+ function extractBestTitle(html) {
2141
+ const attributes = [
2142
+ attributeValue(html, "aria-label"),
2143
+ attributeValue(html, "alt"),
2144
+ attributeValue(html, "title"),
2145
+ attributeValue(html, "data-title"),
2146
+ jsonStringValue(html, "title"),
2147
+ jsonStringValue(html, "name")
2148
+ ];
2149
+ return attributes.map(cleanTitleText).find((value) => value !== void 0);
2150
+ }
2151
+ function attributeValue(html, name) {
2152
+ const match = new RegExp(String.raw`${name}\s*=\s*["']([^"']+)["']`, "iu").exec(html);
2153
+ return match?.[1];
2154
+ }
2155
+ function jsonStringValue(html, name) {
2156
+ const match = new RegExp(String.raw`["']${name}["']\s*:\s*["']((?:\\.|[^"'])+)["']`, "iu").exec(html);
2157
+ return unescapeJsonString(match?.[1]);
2158
+ }
2159
+ function cleanTitleText(value) {
2160
+ if (value === void 0) return void 0;
2161
+ const decoded = decodeHtml(value).replaceAll(/\s+/gu, " ").replace(/\s*\|\s*Netflix(?:\s+Official\s+Site)?\s*$/iu, "").replace(/\s*-\s*Netflix(?:\s+Official\s+Site)?\s*$/iu, "").replace(/^\s*(?:go\s+to|watch|play)\s+/iu, "").trim();
2162
+ if (decoded.length === 0 || decoded.length > 120 || /^\d+$/u.test(decoded)) return void 0;
2163
+ if (WEAK_TITLE_PATTERNS.some((pattern) => pattern.test(decoded))) return void 0;
2164
+ return decoded;
2165
+ }
2166
+ function cleanDetailText(value) {
2167
+ if (value === void 0) return void 0;
2168
+ const decoded = decodeHtml(value).replaceAll(/\s+/gu, " ").trim();
2169
+ return decoded.length === 0 ? void 0 : decoded;
2170
+ }
2171
+ function decodeHtml(value) {
2172
+ return value.replaceAll(/&#(\d+);/gu, (_match, code) => String.fromCodePoint(Number.parseInt(code, 10))).replaceAll(/&#x([a-f0-9]+);/giu, (_match, code) => String.fromCodePoint(Number.parseInt(code, 16))).replaceAll("&amp;", "&").replaceAll("&quot;", '"').replaceAll("&#39;", "'").replaceAll("&apos;", "'").replaceAll("&lt;", "<").replaceAll("&gt;", ">");
2173
+ }
2174
+ function extractDetailTitle(html, jsonLdRecords) {
2175
+ return [
2176
+ ...jsonLdRecords.map((record) => record.name),
2177
+ metaContent(html, "og:title"),
2178
+ metaContent(html, "twitter:title"),
2179
+ metaContent(html, "title"),
2180
+ tagText(html, "title"),
2181
+ jsonStringValue(html, "title"),
2182
+ jsonStringValue(html, "name")
2183
+ ].map((value) => cleanTitleText(stringFromUnknown(value))).find((value) => value !== void 0);
2184
+ }
2185
+ function extractTitleType(html, jsonLdRecords) {
2186
+ for (const rawType of [
2187
+ ...jsonLdRecords.map((record) => record["@type"]),
2188
+ jsonStringValue(html, "type"),
2189
+ jsonStringValue(html, "videoType")
2190
+ ]) {
2191
+ const type = stringFromUnknown(rawType)?.toLowerCase();
2192
+ if (type === void 0) continue;
2193
+ if (/\b(?:movie|film)\b/u.test(type)) return "movie";
2194
+ if (/\b(?:series|tvseries|show|season)\b/u.test(type)) return "series";
2195
+ }
2196
+ return void 0;
2197
+ }
2198
+ function extractYear(html, jsonLdRecords) {
2199
+ for (const value of [
2200
+ ...jsonLdRecords.flatMap((record) => [record.dateCreated, record.datePublished, record.releaseDate, record.releaseYear]),
2201
+ jsonNumberValue(html, "releaseYear"),
2202
+ jsonNumberValue(html, "year"),
2203
+ jsonStringValue(html, "datePublished"),
2204
+ jsonStringValue(html, "releaseDate")
2205
+ ]) {
2206
+ const year = yearFromUnknown(value);
2207
+ if (year !== void 0) return year;
2208
+ }
2209
+ return void 0;
2210
+ }
2211
+ function extractRuntimeMinutes(html, jsonLdRecords) {
2212
+ for (const value of [
2213
+ ...jsonLdRecords.flatMap((record) => [record.duration, record.runtime]),
2214
+ jsonStringValue(html, "duration"),
2215
+ jsonStringValue(html, "runtime"),
2216
+ jsonNumberValue(html, "runtime")
2217
+ ]) {
2218
+ const runtime = runtimeMinutesFromUnknown(value);
2219
+ if (runtime !== void 0) return runtime;
2220
+ }
2221
+ return runtimeMinutesFromText(html);
2222
+ }
2223
+ function extractJsonLdRecords(html) {
2224
+ const records = [];
2225
+ for (const match of html.matchAll(/<script\b[^>]*type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/giu)) {
2226
+ const json = cleanDetailText(match[1]);
2227
+ if (json === void 0) continue;
2228
+ try {
2229
+ records.push(...flattenJsonLd(JSON.parse(json)));
2230
+ } catch {
2231
+ }
2232
+ }
2233
+ return records;
2234
+ }
2235
+ function flattenJsonLd(value) {
2236
+ if (Array.isArray(value)) return value.flatMap(flattenJsonLd);
2237
+ if (!isRecord5(value)) return [];
2238
+ const graph = value["@graph"];
2239
+ return [
2240
+ value,
2241
+ ...Array.isArray(graph) ? graph.flatMap(flattenJsonLd) : []
2242
+ ];
2243
+ }
2244
+ function metaContent(html, target) {
2245
+ for (const tag of html.matchAll(/<meta\b[^>]*>/giu)) {
2246
+ const attributes = tagAttributes(tag[0]);
2247
+ if (attributes.property !== target && attributes.name !== target) continue;
2248
+ const content = cleanDetailText(attributes.content);
2249
+ if (content !== void 0) return content;
2250
+ }
2251
+ return void 0;
2252
+ }
2253
+ function tagText(html, tagName) {
2254
+ const match = new RegExp(String.raw`<${tagName}\b[^>]*>([\s\S]*?)<\/${tagName}>`, "iu").exec(html);
2255
+ return cleanDetailText(match?.[1]?.replaceAll(/<[^>]+>/gu, " "));
2256
+ }
2257
+ function tagAttributes(tag) {
2258
+ const attributes = {};
2259
+ for (const match of tag.matchAll(/([a-z0-9:_-]+)\s*=\s*(["'])(.*?)\2/giu)) {
2260
+ const key = match[1];
2261
+ const value = match[3];
2262
+ if (key !== void 0 && value !== void 0) attributes[key.toLowerCase()] = decodeHtml(value);
2263
+ }
2264
+ return attributes;
2265
+ }
2266
+ function jsonStringArrayValues(html, name) {
2267
+ const values = [];
2268
+ const pattern = new RegExp(String.raw`["']${name}["']\s*:\s*\[([\s\S]{0,3000}?)\]`, "giu");
2269
+ for (const match of html.matchAll(pattern)) {
2270
+ const content = match[1] ?? "";
2271
+ if (/[{}]/u.test(content)) continue;
2272
+ for (const stringMatch of content.matchAll(/["']((?:\\.|[^"'])+)["']/gu)) {
2273
+ const value = cleanDetailText(unescapeJsonString(stringMatch[1]));
2274
+ if (value !== void 0) values.push(value);
2275
+ }
2276
+ }
2277
+ return values;
2278
+ }
2279
+ function jsonNumberValue(html, name) {
2280
+ const match = new RegExp(String.raw`["']${name}["']\s*:\s*(\d+)`, "iu").exec(html);
2281
+ return match?.[1] === void 0 ? void 0 : Number.parseInt(match[1], 10);
2282
+ }
2283
+ function firstString3(values) {
2284
+ return values.map((value) => cleanDetailText(stringFromUnknown(value))).find((value) => value !== void 0);
2285
+ }
2286
+ function namesFromUnknown(value) {
2287
+ if (Array.isArray(value)) return value.flatMap(namesFromUnknown);
2288
+ if (isRecord5(value)) return stringsFromUnknown(value.name);
2289
+ return stringsFromUnknown(value);
2290
+ }
2291
+ function stringsFromUnknown(value) {
2292
+ if (Array.isArray(value)) return value.flatMap(stringsFromUnknown);
2293
+ const text = cleanDetailText(stringFromUnknown(value));
2294
+ return text === void 0 ? [] : [text];
2295
+ }
2296
+ function stringFromUnknown(value) {
2297
+ return typeof value === "string" ? value : void 0;
2298
+ }
2299
+ function yearFromUnknown(value) {
2300
+ if (typeof value === "number" && Number.isInteger(value) && value >= 1888 && value <= 3e3) return value;
2301
+ const text = stringFromUnknown(value);
2302
+ const match = text === void 0 ? void 0 : /\b(19|20|21)\d{2}\b/u.exec(text);
2303
+ return match?.[0] === void 0 ? void 0 : Number.parseInt(match[0], 10);
2304
+ }
2305
+ function runtimeMinutesFromUnknown(value) {
2306
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) return Math.round(value);
2307
+ const text = stringFromUnknown(value);
2308
+ if (text === void 0) return void 0;
2309
+ const isoMatch = /^P(?:T)?(?:(\d+)H)?(?:(\d+)M)?$/iu.exec(text);
2310
+ if (isoMatch !== null) {
2311
+ const hours = isoMatch[1] === void 0 ? 0 : Number.parseInt(isoMatch[1], 10);
2312
+ const minutes = isoMatch[2] === void 0 ? 0 : Number.parseInt(isoMatch[2], 10);
2313
+ const total = hours * 60 + minutes;
2314
+ return total > 0 ? total : void 0;
2315
+ }
2316
+ return runtimeMinutesFromText(text);
2317
+ }
2318
+ function runtimeMinutesFromText(text) {
2319
+ const match = /\b(?:(\d+)\s*h(?:ours?)?)?\s*(?:(\d+)\s*m(?:in(?:utes?)?)?)\b/iu.exec(decodeHtml(text));
2320
+ if (match === null || match[1] === void 0 && match[2] === void 0) return void 0;
2321
+ const hours = match[1] === void 0 ? 0 : Number.parseInt(match[1], 10);
2322
+ const minutes = match[2] === void 0 ? 0 : Number.parseInt(match[2], 10);
2323
+ const total = hours * 60 + minutes;
2324
+ return total > 0 ? total : void 0;
2325
+ }
2326
+ function assignDetailString(target, key, value) {
2327
+ const parsed = cleanDetailText(value);
2328
+ if (parsed !== void 0) target[key] = parsed;
2329
+ }
2330
+ function assignDetailStrings(target, key, value) {
2331
+ const strings = [...new Set(value.map(cleanDetailText).filter((item) => item !== void 0))].toSorted((a, b) => a.localeCompare(b));
2332
+ if (strings.length > 0) target[key] = strings;
2333
+ }
2334
+ function isRecord5(value) {
2335
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2336
+ }
2337
+ function unescapeJsonString(value) {
2338
+ return value?.replaceAll(String.raw`\"`, '"').replaceAll(String.raw`\'`, "'").replaceAll(String.raw`\/`, "/");
2339
+ }
2340
+ function fallbackTitle(sourceTitleId) {
2341
+ return `Netflix title ${sourceTitleId}`;
2342
+ }
2343
+ function qualityBadges(html) {
2344
+ return QUALITY_BADGES.filter(({ pattern }) => pattern.test(html)).map(({ badge }) => badge);
2345
+ }
2346
+ function mergeQualityBadges(existing, next) {
2347
+ return {
2348
+ badges: [.../* @__PURE__ */ new Set([...existing, ...next])].toSorted((a, b) => a.localeCompare(b)),
2349
+ encodes: []
2350
+ };
2351
+ }
2352
+
2353
+ // src/actions/witness/witnesses/netflixCatalogDetail.ts
2354
+ async function enrichNetflixCatalogObservations(observations, options) {
2355
+ if (observations.length === 0) {
2356
+ return {
2357
+ detailSourceCount: 0,
2358
+ observations: []
2359
+ };
2360
+ }
2361
+ const detailUrls = [...new Set(observations.slice(0, options.detailLimit).map((observation) => observation.url ?? netflixTitleUrl(observation.sourceTitleId)))];
2362
+ const detailSources = await resolveDetailSources(detailUrls, options);
2363
+ const detailsByTitleId = /* @__PURE__ */ new Map();
2364
+ for (const source of detailSources) {
2365
+ const sourceTitleId = source.titleId ?? netflixTitleIdFromUrl(source.sourceUrl ?? "");
2366
+ if (sourceTitleId === void 0) continue;
2367
+ const rawResponseHash = hashBinary3(Buffer.from(source.content));
2368
+ const detail = extractNetflixTitleDetails(source.content, {
2369
+ observedAt: options.observedAt,
2370
+ rawResponseHash,
2371
+ region: options.region,
2372
+ sourceName: source.sourceName,
2373
+ sourceTitleId,
2374
+ sourceUrl: source.sourceUrl
2375
+ });
2376
+ detailsByTitleId.set(sourceTitleId, mergeDetailPatch(detailsByTitleId.get(sourceTitleId), detail));
2377
+ }
2378
+ return {
2379
+ detailSourceCount: detailSources.length,
2380
+ observations: observations.map((observation) => mergeObservationDetail(observation, detailsByTitleId.get(observation.sourceTitleId)))
2381
+ };
2382
+ }
2383
+ async function resolveDetailSources(urls, options) {
2384
+ if (options.captureMethod === "browser") {
2385
+ return (await captureNetflixPagesWithChrome(urls, {
2386
+ chromePath: options.chromePath,
2387
+ devtoolsTimeoutMs: options.devtoolsTimeoutMs,
2388
+ headless: options.headless,
2389
+ profileDir: options.profileDir,
2390
+ scrolls: options.detailScrolls,
2391
+ scrollWaitMs: options.detailScrollWaitMs,
2392
+ viewportHeight: options.viewportHeight,
2393
+ viewportWidth: options.viewportWidth,
2394
+ waitMs: options.detailWaitMs
2395
+ })).map(browserSnapshotToCaptureSource);
2396
+ }
2397
+ return Promise.all(urls.map(async (url) => ({
2398
+ content: await options.fetchText(url),
2399
+ sourceName: `detail-url:${url}`,
2400
+ sourceUrl: url,
2401
+ titleId: netflixTitleIdFromUrl(url)
2402
+ })));
2403
+ }
2404
+ function browserSnapshotToCaptureSource(snapshot) {
2405
+ return {
2406
+ content: snapshot.content,
2407
+ sourceName: `detail-${snapshot.sourceName}`,
2408
+ sourceUrl: snapshot.sourceUrl,
2409
+ titleId: netflixTitleIdFromUrl(snapshot.sourceUrl)
2410
+ };
2411
+ }
2412
+ function mergeObservationDetail(observation, detail) {
2413
+ if (detail === void 0 || !hasDetailData(detail)) return observation;
2414
+ return {
2415
+ ...observation,
2416
+ cast: mergeStringList(observation.cast, detail.cast),
2417
+ creators: mergeStringList(observation.creators, detail.creators),
2418
+ evidence: detail.evidence ?? observation.evidence,
2419
+ genres: mergeStringList(observation.genres, detail.genres),
2420
+ maturityRating: detail.maturityRating ?? observation.maturityRating,
2421
+ quality: mergeQuality(observation.quality, detail.quality),
2422
+ runtime: detail.runtime ?? observation.runtime,
2423
+ synopsis: detail.synopsis ?? observation.synopsis,
2424
+ title: chooseMergedTitle(observation, detail),
2425
+ type: detail.type ?? observation.type,
2426
+ url: detail.url ?? observation.url,
2427
+ year: detail.year ?? observation.year
2428
+ };
2429
+ }
2430
+ function mergeDetailPatch(existing, next) {
2431
+ if (existing === void 0) return next;
2432
+ return {
2433
+ ...existing,
2434
+ cast: mergeStringList(existing.cast, next.cast),
2435
+ creators: mergeStringList(existing.creators, next.creators),
2436
+ evidence: next.evidence ?? existing.evidence,
2437
+ genres: mergeStringList(existing.genres, next.genres),
2438
+ maturityRating: next.maturityRating ?? existing.maturityRating,
2439
+ quality: mergeQuality(existing.quality, next.quality),
2440
+ runtime: next.runtime ?? existing.runtime,
2441
+ sourceTitleId: existing.sourceTitleId ?? next.sourceTitleId,
2442
+ synopsis: next.synopsis ?? existing.synopsis,
2443
+ title: next.title ?? existing.title,
2444
+ type: next.type ?? existing.type,
2445
+ url: next.url ?? existing.url,
2446
+ year: next.year ?? existing.year
2447
+ };
2448
+ }
2449
+ function hasDetailData(detail) {
2450
+ const values = [
2451
+ detail.title,
2452
+ detail.year,
2453
+ detail.runtime,
2454
+ detail.maturityRating,
2455
+ detail.synopsis
2456
+ ];
2457
+ const arrays = [
2458
+ detail.genres,
2459
+ detail.cast,
2460
+ detail.creators,
2461
+ detail.quality?.badges,
2462
+ detail.quality?.encodes
2463
+ ];
2464
+ return values.some((value) => value !== void 0) || arrays.some((value) => (value?.length ?? 0) > 0);
2465
+ }
2466
+ function chooseMergedTitle(observation, detail) {
2467
+ if (detail.title === void 0) return observation.title;
2468
+ if (isWeakNetflixTitle(observation.title, observation.sourceTitleId)) return detail.title;
2469
+ return observation.title;
2470
+ }
2471
+ function mergeQuality(existing, next) {
2472
+ return {
2473
+ badges: mergeStringList(existing?.badges, next?.badges) ?? [],
2474
+ encodes: [...existing?.encodes ?? [], ...next?.encodes ?? []]
2475
+ };
2476
+ }
2477
+ function mergeStringList(existing, next) {
2478
+ const merged = [...new Set([...existing ?? [], ...next ?? []].map((item) => item.trim()).filter((item) => item.length > 0))].toSorted((a, b) => a.localeCompare(b));
2479
+ return merged.length === 0 ? void 0 : merged;
2480
+ }
2481
+
2482
+ // src/actions/witness/witnesses/netflixCatalog.ts
2483
+ var NETFLIX_CATALOG_SCHEMA = "network.xyo.streaming.catalog.snapshot";
2484
+ var JSONL_MIME_TYPE = "application/x-ndjson";
2485
+ var netflixCatalogWitness = {
2486
+ builder: (yargs) => {
2487
+ return yargs.option("mode", {
2488
+ choices: ["genre", "html-file", "search", "title", "url", "url-file"],
2489
+ describe: "Netflix capture mode; inferred from the supplied target option when omitted",
2490
+ type: "string"
2491
+ }).option("region", {
2492
+ default: "US",
2493
+ describe: "Netflix catalog region label to attach to observations",
2494
+ type: "string"
2495
+ }).option("query", {
2496
+ describe: "Search query for mode=search",
2497
+ type: "string"
2498
+ }).option("genre-id", {
2499
+ describe: "Netflix genre id for mode=genre",
2500
+ type: "string"
2501
+ }).option("title-id", {
2502
+ describe: "Netflix title id for mode=title",
2503
+ type: "string"
2504
+ }).option("url", {
2505
+ array: true,
2506
+ describe: "Netflix URL to fetch for mode=url",
2507
+ type: "string"
2508
+ }).option("url-file", {
2509
+ describe: "File containing one Netflix URL per line for mode=url-file",
2510
+ type: "string"
2511
+ }).option("html-file", {
2512
+ array: true,
2513
+ describe: "Saved Netflix HTML or DOM file to parse for mode=html-file",
2514
+ type: "string"
2515
+ }).option("limit", {
2516
+ default: 100,
2517
+ describe: "Maximum observations to keep from extracted title links",
2518
+ type: "number"
2519
+ }).option("scope-key", {
2520
+ describe: "Override the snapshot scope key stored in the catalog index",
2521
+ type: "string"
2522
+ }).option("observed-at", {
2523
+ describe: "Override observation timestamp",
2524
+ type: "string"
2525
+ }).option("save", {
2526
+ default: false,
2527
+ describe: "Ingest the snapshot into ARIES_HOME/streaming-catalog",
2528
+ type: "boolean"
2529
+ }).option("allow-empty", {
2530
+ default: false,
2531
+ describe: "Allow --save to ingest a successful snapshot with zero extracted titles",
2532
+ type: "boolean"
2533
+ }).option("include-details", {
2534
+ default: false,
2535
+ describe: "Navigate captured title URLs and merge detail-page metadata into observations",
2536
+ type: "boolean"
2537
+ }).option("detail-limit", {
2538
+ describe: "Maximum captured titles to enrich when --include-details is set",
2539
+ type: "number"
2540
+ }).option("detail-wait-ms", {
2541
+ default: 5e3,
2542
+ describe: "Milliseconds to wait after opening each Netflix title detail page",
2543
+ type: "number"
2544
+ }).option("detail-scrolls", {
2545
+ default: 1,
2546
+ describe: "Number of vertical scroll steps to run on each detail page",
2547
+ type: "number"
2548
+ }).option("detail-scroll-wait-ms", {
2549
+ default: 750,
2550
+ describe: "Milliseconds to wait after each detail-page scroll step",
2551
+ type: "number"
2552
+ }).option("capture-method", {
2553
+ choices: ["browser", "fetch"],
2554
+ describe: "How to load Netflix URL/search/genre/title pages; browser renders the live site, fetch retrieves static HTML only",
2555
+ type: "string"
2556
+ }).option("chrome-path", {
2557
+ describe: "Chrome/Chromium executable path for browser capture",
2558
+ type: "string"
2559
+ }).option("browser-profile-dir", {
2560
+ describe: "Persistent Chrome profile directory for Netflix login/session state",
2561
+ type: "string"
2562
+ }).option("devtools-timeout-ms", {
2563
+ default: 3e4,
2564
+ describe: "Milliseconds to wait for Chrome to expose its DevTools endpoint before capture starts",
2565
+ type: "number"
2566
+ }).option("headless", {
2567
+ default: false,
2568
+ describe: "Run Chrome in headless mode for browser capture",
2569
+ type: "boolean"
2570
+ }).option("wait-ms", {
2571
+ default: 8e3,
2572
+ describe: "Milliseconds to wait after opening each Netflix page before extracting the first DOM snapshot",
2573
+ type: "number"
2574
+ }).option("scrolls", {
2575
+ default: 8,
2576
+ describe: "Number of vertical scroll steps to run before final extraction",
2577
+ type: "number"
2578
+ }).option("scroll-wait-ms", {
2579
+ default: 1e3,
2580
+ describe: "Milliseconds to wait after each scroll step",
2581
+ type: "number"
2582
+ }).option("viewport-width", {
2583
+ default: 1440,
2584
+ describe: "Browser viewport width for rendered Netflix capture",
2585
+ type: "number"
2586
+ }).option("viewport-height", {
2587
+ default: 1200,
2588
+ describe: "Browser viewport height for rendered Netflix capture",
2589
+ type: "number"
2590
+ });
2591
+ },
2592
+ meta: {
2593
+ description: "Snapshot visible Netflix catalog title links as scoped streaming catalog observations",
2594
+ name: "netflix-catalog",
2595
+ schema: NETFLIX_CATALOG_SCHEMA
2596
+ },
2597
+ observe: async (args) => {
2598
+ const observedAt = normalizeObservedAt(args["observed-at"]);
2599
+ const mode = inferMode(args);
2600
+ const region = normalizeRegion(args.region);
2601
+ const limit = normalizeLimit(args.limit);
2602
+ const sources = await resolveCaptureSources(mode, args);
2603
+ const scope = buildScope(mode, args, sources, limit);
2604
+ const scopeKey = stringArg(args["scope-key"]) ?? defaultScopeKey(scope);
2605
+ let observations = collectObservations(sources, {
2606
+ limit,
2607
+ observedAt,
2608
+ region
2609
+ });
2610
+ if (mode === "title") {
2611
+ const titleId = requiredString(args["title-id"], "--title-id is required for mode=title");
2612
+ observations = observations.filter((observation) => observation.sourceTitleId === titleId);
2613
+ if (observations.length === 0) observations.push(fallbackTitleObservation(titleId, observedAt, region));
2614
+ }
2615
+ const detailResult = await enrichObservationsWithDetails(observations, args, {
2616
+ limit,
2617
+ observedAt,
2618
+ region
2619
+ });
2620
+ observations = detailResult.observations;
2621
+ assertNonEmptySave(args, observations.length);
2622
+ const jsonl = Buffer.from(`${observations.map((observation) => JSON.stringify(observation)).join("\n")}${observations.length > 0 ? "\n" : ""}`);
2623
+ const observationsHash = hashBinary4(jsonl);
2624
+ const snapshot = args.save === true ? ingestStreamingCatalogSnapshot({
2625
+ complete: false,
2626
+ observations,
2627
+ observedAt,
2628
+ provider: "netflix",
2629
+ region,
2630
+ removalPolicy: "scope",
2631
+ scope,
2632
+ scopeKey,
2633
+ sourceKind: "browser-witness",
2634
+ sourceName: "netflix-catalog-witness",
2635
+ status: "success"
2636
+ }) : void 0;
2637
+ const binaryRef = {
2638
+ filename: `netflix-catalog-${region.toLowerCase()}-${scopeKey}-${observedAt.slice(0, 10)}.jsonl`,
2639
+ hash: observationsHash,
2640
+ mimeType: JSONL_MIME_TYPE,
2641
+ size: jsonl.length
2642
+ };
2643
+ const payload = {
2644
+ complete: false,
2645
+ observedAt,
2646
+ observations: binaryRef,
2647
+ observationsHash,
2648
+ observationsMimeType: JSONL_MIME_TYPE,
2649
+ provider: "netflix",
2650
+ region,
2651
+ removalPolicy: "scope",
2652
+ schema: NETFLIX_CATALOG_SCHEMA,
2653
+ scope,
2654
+ scopeKey,
2655
+ sourceCount: sources.length,
2656
+ sourceKind: "browser-witness",
2657
+ titleCount: observations.length,
2658
+ ...detailResult.detailSourceCount > 0 && { detailSourceCount: detailResult.detailSourceCount },
2659
+ ...snapshot !== void 0 && { snapshot }
2660
+ };
2661
+ return {
2662
+ binaries: [{ data: jsonl, ref: binaryRef }],
2663
+ payloads: [payload]
2664
+ };
2665
+ }
2666
+ };
2667
+ function assertNonEmptySave(args, titleCount) {
2668
+ if (args.save !== true || booleanArg(args["allow-empty"]) || titleCount > 0) return;
2669
+ throw new Error([
2670
+ "Refusing to save an empty Netflix catalog snapshot.",
2671
+ "The capture returned 0 titles; rerun after login/profile selection or pass --allow-empty",
2672
+ "to intentionally record an empty successful snapshot."
2673
+ ].join(" "));
2674
+ }
2675
+ function collectObservations(sources, options) {
2676
+ const byTitleId = /* @__PURE__ */ new Map();
2677
+ for (const source of sources) {
2678
+ const rawResponseHash = hashBinary4(Buffer.from(source.content));
2679
+ for (const observation of extractNetflixCatalogObservations(source.content, {
2680
+ observedAt: options.observedAt,
2681
+ rawResponseHash,
2682
+ region: options.region,
2683
+ sourceName: source.sourceName,
2684
+ sourceUrl: source.sourceUrl
2685
+ })) {
2686
+ byTitleId.set(observation.sourceTitleId, observation);
2687
+ if (byTitleId.size >= options.limit) break;
2688
+ }
2689
+ if (byTitleId.size >= options.limit) break;
2690
+ }
2691
+ return [...byTitleId.values()].toSorted((a, b) => a.sourceTitleId.localeCompare(b.sourceTitleId));
2692
+ }
2693
+ async function enrichObservationsWithDetails(observations, args, options) {
2694
+ if (!booleanArg(args["include-details"]) || observations.length === 0) {
2695
+ return {
2696
+ detailSourceCount: 0,
2697
+ observations: [...observations]
2698
+ };
2699
+ }
2700
+ return enrichNetflixCatalogObservations(observations, {
2701
+ captureMethod: captureMethod(args),
2702
+ chromePath: stringArg(args["chrome-path"]),
2703
+ devtoolsTimeoutMs: normalizePositiveInteger(args["devtools-timeout-ms"], "--devtools-timeout-ms", 3e4),
2704
+ detailLimit: normalizeDetailLimit(args["detail-limit"], options.limit),
2705
+ detailScrolls: normalizeNonNegativeInteger(args["detail-scrolls"], "--detail-scrolls", 1),
2706
+ detailScrollWaitMs: normalizePositiveInteger(args["detail-scroll-wait-ms"], "--detail-scroll-wait-ms", 750),
2707
+ detailWaitMs: normalizeNonNegativeInteger(args["detail-wait-ms"], "--detail-wait-ms", 5e3),
2708
+ fetchText,
2709
+ headless: args.headless === true,
2710
+ observedAt: options.observedAt,
2711
+ profileDir: stringArg(args["browser-profile-dir"]) ?? defaultChromeProfileDir(),
2712
+ region: options.region,
2713
+ viewportHeight: normalizePositiveInteger(args["viewport-height"], "--viewport-height", 1200),
2714
+ viewportWidth: normalizePositiveInteger(args["viewport-width"], "--viewport-width", 1440)
2715
+ });
2716
+ }
2717
+ async function resolveCaptureSources(mode, args) {
2718
+ if (mode === "html-file") {
2719
+ const files = arrayArg(args["html-file"]);
2720
+ if (files.length === 0) throw new Error("--html-file is required for mode=html-file");
2721
+ return Promise.all(files.map(async (file) => ({
2722
+ content: await readRequiredTextFile(
2723
+ file,
2724
+ `Netflix HTML file not found: ${file}. Pass an existing saved Netflix HTML/DOM file, or use --query, --genre-id, --title-id, or --url to fetch a page directly.`
2725
+ ),
2726
+ sourceName: `html-file:${PATH4.basename(file)}`
2727
+ })));
2728
+ }
2729
+ const urls = await urlsForMode(mode, args);
2730
+ if (captureMethod(args) === "browser") {
2731
+ return (await captureNetflixPagesWithChrome(urls, {
2732
+ chromePath: stringArg(args["chrome-path"]),
2733
+ devtoolsTimeoutMs: normalizePositiveInteger(args["devtools-timeout-ms"], "--devtools-timeout-ms", 3e4),
2734
+ headless: args.headless === true,
2735
+ profileDir: stringArg(args["browser-profile-dir"]) ?? defaultChromeProfileDir(),
2736
+ scrolls: normalizeNonNegativeInteger(args.scrolls, "--scrolls", 8),
2737
+ scrollWaitMs: normalizePositiveInteger(args["scroll-wait-ms"], "--scroll-wait-ms", 1e3),
2738
+ viewportHeight: normalizePositiveInteger(args["viewport-height"], "--viewport-height", 1200),
2739
+ viewportWidth: normalizePositiveInteger(args["viewport-width"], "--viewport-width", 1440),
2740
+ waitMs: normalizeNonNegativeInteger(args["wait-ms"], "--wait-ms", 8e3)
2741
+ })).map(browserSnapshotToCaptureSource2);
2742
+ }
2743
+ return Promise.all(urls.map(async (url) => ({
2744
+ content: await fetchText(url),
2745
+ sourceName: `url:${url}`,
2746
+ sourceUrl: url,
2747
+ titleId: netflixTitleIdFromUrl(url)
2748
+ })));
2749
+ }
2750
+ function browserSnapshotToCaptureSource2(snapshot) {
2751
+ return {
2752
+ content: snapshot.content,
2753
+ sourceName: snapshot.sourceName,
2754
+ sourceUrl: snapshot.sourceUrl,
2755
+ titleId: netflixTitleIdFromUrl(snapshot.sourceUrl)
2756
+ };
2757
+ }
2758
+ function captureMethod(args) {
2759
+ const method = stringArg(args["capture-method"]);
2760
+ if (method === void 0) return "browser";
2761
+ if (method === "browser" || method === "fetch") return method;
2762
+ throw new Error(`Invalid --capture-method value: ${method}`);
2763
+ }
2764
+ async function urlsForMode(mode, args) {
2765
+ if (mode === "search") {
2766
+ const query = requiredString(args.query, "--query is required for mode=search");
2767
+ return [`https://www.netflix.com/search?q=${encodeURIComponent(query)}`];
2768
+ }
2769
+ if (mode === "genre") {
2770
+ const genreId = requiredString(args["genre-id"], "--genre-id is required for mode=genre");
2771
+ return [`https://www.netflix.com/browse/genre/${genreId}`];
2772
+ }
2773
+ if (mode === "title") {
2774
+ const titleId = requiredString(args["title-id"], "--title-id is required for mode=title");
2775
+ return [netflixTitleUrl(titleId)];
2776
+ }
2777
+ if (mode === "url") {
2778
+ const urls = arrayArg(args.url);
2779
+ if (urls.length === 0) throw new Error("--url is required for mode=url");
2780
+ return urls;
2781
+ }
2782
+ if (mode === "url-file") {
2783
+ const file = requiredString(args["url-file"], "--url-file is required for mode=url-file");
2784
+ return (await readRequiredTextFile(file, `Netflix URL file not found: ${file}. Pass an existing file with one Netflix URL per line.`)).split(/\r?\n/u).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
2785
+ }
2786
+ throw new Error(`Unsupported Netflix catalog mode: ${mode}`);
2787
+ }
2788
+ async function fetchText(url) {
2789
+ const response = await fetch(url, {
2790
+ headers: {
2791
+ "accept-language": "en-US,en;q=0.9",
2792
+ "user-agent": "Mozilla/5.0 AriesNetflixCatalogWitness/0.1"
2793
+ }
2794
+ });
2795
+ return response.text();
2796
+ }
2797
+ async function readRequiredTextFile(file, missingMessage) {
2798
+ try {
2799
+ return await readFile(file, "utf8");
2800
+ } catch (error) {
2801
+ if (isNodeFileNotFoundError(error)) throw new Error(missingMessage, { cause: error });
2802
+ throw error;
2803
+ }
2804
+ }
2805
+ function isNodeFileNotFoundError(error) {
2806
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
2807
+ }
2808
+ function inferMode(args) {
2809
+ const mode = stringArg(args.mode);
2810
+ if (mode !== void 0) return mode;
2811
+ if (arrayArg(args["html-file"]).length > 0) return "html-file";
2812
+ if (stringArg(args["url-file"]) !== void 0) return "url-file";
2813
+ if (arrayArg(args.url).length > 0) return "url";
2814
+ if (stringArg(args["title-id"]) !== void 0) return "title";
2815
+ if (stringArg(args["genre-id"]) !== void 0) return "genre";
2816
+ if (stringArg(args.query) !== void 0) return "search";
2817
+ throw new Error("Netflix catalog witness requires one target: --query, --genre-id, --title-id, --url, --url-file, or --html-file.");
2818
+ }
2819
+ function buildScope(mode, args, sources, limit) {
2820
+ const scope = { limit, mode };
2821
+ if (mode !== "html-file") assignScopeValue(scope, "captureMethod", captureMethod(args));
2822
+ if (booleanArg(args["include-details"])) {
2823
+ scope.includeDetails = true;
2824
+ scope.detailLimit = normalizeDetailLimit(args["detail-limit"], limit);
2825
+ }
2826
+ assignScopeValue(scope, "query", stringArg(args.query));
2827
+ assignScopeValue(scope, "genreId", stringArg(args["genre-id"]));
2828
+ assignScopeValue(scope, "titleId", stringArg(args["title-id"]));
2829
+ if (sources.length > 0) scope.sources = sources.map((source) => source.sourceUrl ?? source.sourceName);
2830
+ return scope;
2831
+ }
2832
+ function defaultScopeKey(scope) {
2833
+ if (scope.mode === "search" && typeof scope.query === "string") return slug(`search-${scope.query}`);
2834
+ if (scope.mode === "genre" && typeof scope.genreId === "string") return slug(`genre-${scope.genreId}`);
2835
+ if (scope.mode === "title" && typeof scope.titleId === "string") return slug(`title-${scope.titleId}`);
2836
+ return slug(`${scope.mode}-${hashBinary4(Buffer.from(JSON.stringify(scope))).slice(0, 12)}`);
2837
+ }
2838
+ function fallbackTitleObservation(titleId, observedAt, region) {
2839
+ return {
2840
+ available: true,
2841
+ evidence: {
2842
+ region,
2843
+ source: "netflix-catalog-witness"
2844
+ },
2845
+ observedAt,
2846
+ provider: "netflix",
2847
+ quality: { badges: [], encodes: [] },
2848
+ region,
2849
+ sourceKind: "browser-witness",
2850
+ sourceTitleId: titleId,
2851
+ title: `Netflix title ${titleId}`,
2852
+ type: "unknown",
2853
+ url: netflixTitleUrl(titleId)
2854
+ };
2855
+ }
2856
+ function normalizeObservedAt(value) {
2857
+ const parsed = new Date(stringArg(value) ?? Date.now());
2858
+ if (Number.isNaN(parsed.getTime())) throw new Error(`Invalid --observed-at value: ${String(value)}`);
2859
+ return parsed.toISOString();
2860
+ }
2861
+ function normalizeRegion(value) {
2862
+ return (stringArg(value) ?? "US").toUpperCase();
2863
+ }
2864
+ function normalizeLimit(value) {
2865
+ const limit = normalizePositiveInteger(value, "--limit", 100);
2866
+ return Math.min(limit, 1e3);
2867
+ }
2868
+ function normalizeDetailLimit(value, limit) {
2869
+ const detailLimit = normalizePositiveInteger(value, "--detail-limit", Math.min(limit, 25));
2870
+ return Math.min(detailLimit, limit, 1e3);
2871
+ }
2872
+ function normalizePositiveInteger(value, field, defaultValue) {
2873
+ const integer = numberArg(value, defaultValue);
2874
+ if (!Number.isInteger(integer) || integer <= 0) throw new Error(`${field} must be a positive integer.`);
2875
+ return integer;
2876
+ }
2877
+ function normalizeNonNegativeInteger(value, field, defaultValue) {
2878
+ const integer = numberArg(value, defaultValue);
2879
+ if (!Number.isInteger(integer) || integer < 0) throw new Error(`${field} must be a non-negative integer.`);
2880
+ return integer;
2881
+ }
2882
+ function numberArg(value, defaultValue) {
2883
+ if (typeof value === "number") return value;
2884
+ const parsed = stringArg(value);
2885
+ return parsed === void 0 ? defaultValue : Number(parsed);
2886
+ }
2887
+ function requiredString(value, message) {
2888
+ const parsed = stringArg(value);
2889
+ if (parsed === void 0) throw new Error(message);
2890
+ return parsed;
2891
+ }
2892
+ function stringArg(value) {
2893
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
2894
+ }
2895
+ function arrayArg(value) {
2896
+ if (Array.isArray(value)) return value.map(String).map((item) => item.trim()).filter((item) => item.length > 0);
2897
+ const parsed = stringArg(value);
2898
+ return parsed === void 0 ? [] : [parsed];
2899
+ }
2900
+ function booleanArg(value) {
2901
+ return value === true || value === "true";
2902
+ }
2903
+ function assignScopeValue(scope, key, value) {
2904
+ if (value !== void 0) scope[key] = value;
2905
+ }
2906
+ function slug(value) {
2907
+ return value.toLowerCase().replaceAll(/[^a-z0-9._-]+/gu, "-").replaceAll(/^-|-$/gu, "") || "netflix";
2908
+ }
2909
+
2910
+ // src/actions/witness/witnesses/pentair.ts
2911
+ import { SchedTypes } from "node-screenlogic";
2912
+ var PENTAIR_SCHEMA = "network.xyo.pentair.screenlogic";
2913
+ var pentairWitness = {
2914
+ meta: {
2915
+ description: "Capture Pentair ScreenLogic pool equipment state",
2916
+ name: "pentair",
2917
+ schema: PENTAIR_SCHEMA
2918
+ },
2919
+ observe: async (args) => {
2920
+ const payload = await withScreenLogic(args, async (unit) => ({
2921
+ chem: await unit.chem.getChemicalDataAsync(),
2922
+ chlor: await unit.chlor.getIntellichlorConfigAsync(),
2923
+ equipment: {
2924
+ circuitNames: await unit.equipment.getAllCircuitNamesAsync(),
2925
+ config: await unit.equipment.getEquipmentConfigurationAsync(),
2926
+ controllerConfig: await unit.equipment.getControllerConfigAsync(),
2927
+ customNames: await unit.equipment.getCustomNamesAsync(),
2928
+ state: await unit.equipment.getEquipmentStateAsync(),
2929
+ systemTime: await unit.equipment.getSystemTimeAsync(),
2930
+ weatherForecast: await unit.equipment.getWeatherForecastAsync()
2931
+ },
2932
+ schedule: {
2933
+ once: await unit.schedule.getScheduleDataAsync(SchedTypes.RUNONCE),
2934
+ recurring: await unit.schedule.getScheduleDataAsync(SchedTypes.RECURRING)
2935
+ },
2936
+ schema: PENTAIR_SCHEMA,
2937
+ version: await unit.getVersionAsync()
2938
+ }));
2939
+ return { payloads: [payload] };
2940
+ }
2941
+ };
2942
+
2943
+ // src/actions/witness/witnesses/pentairIntellichem.ts
2944
+ import {
2945
+ existsSync as existsSync5,
2946
+ mkdirSync as mkdirSync5,
2947
+ readFileSync as readFileSync5,
2948
+ writeFileSync as writeFileSync6
2949
+ } from "node:fs";
2950
+ import path6 from "node:path";
2951
+
2952
+ // src/actions/witness/witnesses/pentairIntellichemGraph.ts
2953
+ import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "node:fs";
2954
+ import path5 from "node:path";
2955
+ var DATA_GAP_MS = 2 * 60 * 60 * 1e3;
2956
+ function assertNumber(value) {
2957
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
2958
+ }
2959
+ function escapeHtml(value) {
2960
+ return String(value).replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
2961
+ }
2962
+ function mergeSamples(samples) {
2963
+ const byTimestamp = /* @__PURE__ */ new Map();
2964
+ for (const sample of samples) {
2965
+ const existing = byTimestamp.get(sample.timestamp);
2966
+ byTimestamp.set(sample.timestamp, existing ? { ...existing, ...sample } : sample);
2967
+ }
2968
+ return [...byTimestamp.values()].toSorted((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp));
2969
+ }
2970
+ function pointPath(points, scaleX, scaleY, gapMs = Number.POSITIVE_INFINITY) {
2971
+ let previous;
2972
+ return points.map((point, index) => {
2973
+ const startsSegment = index === 0 || previous !== void 0 && point.time - previous.time > gapMs;
2974
+ previous = point;
2975
+ return `${startsSegment ? "M" : "L"} ${scaleX(point.time).toFixed(1)} ${scaleY(point.value).toFixed(1)}`;
2976
+ }).join(" ");
2977
+ }
2978
+ function tickValues(min, max, count) {
2979
+ if (count <= 1 || min === max) return [min];
2980
+ return Array.from({ length: count }, (_, index) => min + (max - min) * index / (count - 1));
2981
+ }
2982
+ function metricPoints(samples, metric) {
2983
+ return samples.map((sample) => ({ time: Date.parse(sample.timestamp), value: assertNumber(sample[metric]) })).filter((point) => Number.isFinite(point.time) && point.value !== void 0);
2984
+ }
2985
+ function dataGaps(points) {
2986
+ const gaps = [];
2987
+ for (let index = 1; index < points.length; index += 1) {
2988
+ const previous = points[index - 1];
2989
+ const point = points[index];
2990
+ if (previous && point && point.time - previous.time > DATA_GAP_MS) {
2991
+ gaps.push({
2992
+ end: point.time,
2993
+ hours: (point.time - previous.time) / (60 * 60 * 1e3),
2994
+ start: previous.time
2995
+ });
2996
+ }
2997
+ }
2998
+ return gaps;
2999
+ }
3000
+ function renderLineChart(samples, metric, label, color, setPointMetric) {
3001
+ const width = 1040;
3002
+ const height = 340;
3003
+ const margin = {
3004
+ bottom: 48,
3005
+ left: 58,
3006
+ right: 22,
3007
+ top: 32
3008
+ };
3009
+ const points = metricPoints(samples, metric);
3010
+ if (points.length === 0) {
3011
+ return `<section class="chart"><h2>${escapeHtml(label)}</h2><div class="empty">No ${escapeHtml(label)} samples yet.</div></section>`;
3012
+ }
3013
+ const setPoints = setPointMetric ? metricPoints(samples, setPointMetric) : [];
3014
+ const latestSetPoint = setPoints.at(-1);
3015
+ const minTime = Math.min(...points.map((point) => point.time));
3016
+ const maxTime = Math.max(...points.map((point) => point.time));
3017
+ const values = [...points.map((point) => point.value), ...latestSetPoint ? [latestSetPoint.value] : []];
3018
+ const minValue = Math.min(...values);
3019
+ const maxValue = Math.max(...values);
3020
+ const padding = Math.max((maxValue - minValue) * 0.12, metric === "pH" ? 0.1 : 10);
3021
+ const yMin = Math.max(0, minValue - padding);
3022
+ const yMax = metric === "pH" ? Math.min(14, maxValue + padding) : maxValue + padding;
3023
+ const chartWidth = width - margin.left - margin.right;
3024
+ const chartHeight = height - margin.top - margin.bottom;
3025
+ const span = Math.max(maxTime - minTime, 1);
3026
+ const valueSpan = Math.max(yMax - yMin, 1);
3027
+ const scaleX = (time) => margin.left + (time - minTime) / span * chartWidth;
3028
+ const scaleY = (value) => margin.top + chartHeight - (value - yMin) / valueSpan * chartHeight;
3029
+ const xTicks = tickValues(minTime, maxTime, 5);
3030
+ const yTicks = tickValues(yMin, yMax, 5);
3031
+ const latest = points.at(-1);
3032
+ const latestLabel = latest ? `${latest.value.toFixed(metric === "pH" ? 2 : 0)} ${metric}` : "";
3033
+ const gaps = dataGaps(points);
3034
+ const setPointY = latestSetPoint ? scaleY(latestSetPoint.value).toFixed(1) : void 0;
3035
+ const setPointLine = setPointY ? `<line x1="${margin.left}" x2="${width - margin.right}" y1="${setPointY}" y2="${setPointY}" class="setpoint" />` : "";
3036
+ return `
3037
+ <section class="chart">
3038
+ <div class="chart-title">
3039
+ <h2>${escapeHtml(label)}</h2>
3040
+ <span>${escapeHtml(latestLabel)}${gaps.length > 0 ? ` \xB7 ${gaps.length} data gaps shaded` : ""}</span>
3041
+ </div>
3042
+ <svg viewBox="0 0 ${width} ${height}" role="img" aria-label="${escapeHtml(label)} over time">
3043
+ <rect x="${margin.left}" y="${margin.top}" width="${chartWidth}" height="${chartHeight}" class="plot" />
3044
+ ${gaps.map((gap) => {
3045
+ const x = scaleX(gap.start);
3046
+ const gapWidth = Math.max(scaleX(gap.end) - x, 1);
3047
+ return `<rect x="${x.toFixed(1)}" y="${margin.top}" width="${gapWidth.toFixed(1)}" height="${chartHeight}" class="gap-band">
3048
+ <title>No ${escapeHtml(label)} samples for ${escapeHtml(gap.hours.toFixed(1))} hours</title>
3049
+ </rect>`;
3050
+ }).join("")}
3051
+ ${yTicks.map((tick) => {
3052
+ const y = scaleY(tick);
3053
+ return `<line x1="${margin.left}" x2="${width - margin.right}" y1="${y.toFixed(1)}" y2="${y.toFixed(1)}" class="grid" />
3054
+ <text x="${margin.left - 10}" y="${(y + 4).toFixed(1)}" text-anchor="end" class="axis">${escapeHtml(tick.toFixed(metric === "pH" ? 2 : 0))}</text>`;
3055
+ }).join("")}
3056
+ ${xTicks.map((tick) => {
3057
+ const x = scaleX(tick);
3058
+ const labelText = new Date(tick).toLocaleString();
3059
+ return `<line x1="${x.toFixed(1)}" x2="${x.toFixed(1)}" y1="${margin.top}" y2="${height - margin.bottom}" class="grid-soft" />
3060
+ <text x="${x.toFixed(1)}" y="${height - 16}" text-anchor="middle" class="axis">${escapeHtml(labelText)}</text>`;
3061
+ }).join("")}
3062
+ <path d="${pointPath(points, scaleX, scaleY, DATA_GAP_MS)}" fill="none" stroke="${color}" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" />
3063
+ ${setPointLine}
3064
+ </svg>
3065
+ </section>
3066
+ `;
3067
+ }
3068
+ function renderGraph({
3069
+ generatedAt,
3070
+ samples,
3071
+ sourcePath,
3072
+ title
3073
+ }) {
3074
+ const merged = mergeSamples(samples);
3075
+ const latest = merged.findLast((sample) => sample.pH !== void 0 || sample.orp !== void 0);
3076
+ const firstTimestamp = merged.at(0)?.timestamp;
3077
+ const lastTimestamp = merged.at(-1)?.timestamp;
3078
+ const timeRange = merged.length > 0 ? `${new Date(firstTimestamp ?? generatedAt).toLocaleString()} to ${new Date(lastTimestamp ?? generatedAt).toLocaleString()}` : "No samples";
3079
+ return `<!doctype html>
3080
+ <html lang="en">
3081
+ <head>
3082
+ <meta charset="utf-8" />
3083
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
3084
+ <title>${escapeHtml(title)}</title>
3085
+ <style>
3086
+ :root {
3087
+ color-scheme: light;
3088
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
3089
+ background: #f8fafc;
3090
+ color: #172033;
3091
+ }
3092
+ body {
3093
+ margin: 0;
3094
+ }
3095
+ main {
3096
+ max-width: 1120px;
3097
+ margin: 0 auto;
3098
+ padding: 28px 20px 40px;
3099
+ }
3100
+ header {
3101
+ display: flex;
3102
+ flex-wrap: wrap;
3103
+ gap: 18px;
3104
+ justify-content: space-between;
3105
+ align-items: flex-end;
3106
+ border-bottom: 1px solid #d7dee8;
3107
+ padding-bottom: 18px;
3108
+ margin-bottom: 22px;
3109
+ }
3110
+ h1, h2, p {
3111
+ margin: 0;
3112
+ }
3113
+ h1 {
3114
+ font-size: 28px;
3115
+ font-weight: 720;
3116
+ letter-spacing: 0;
3117
+ }
3118
+ h2 {
3119
+ font-size: 17px;
3120
+ letter-spacing: 0;
3121
+ }
3122
+ .meta {
3123
+ color: #506174;
3124
+ font-size: 13px;
3125
+ line-height: 1.55;
3126
+ text-align: right;
3127
+ }
3128
+ .metrics {
3129
+ display: grid;
3130
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
3131
+ gap: 10px;
3132
+ margin: 18px 0 20px;
3133
+ }
3134
+ .metric {
3135
+ background: #ffffff;
3136
+ border: 1px solid #d7dee8;
3137
+ border-radius: 6px;
3138
+ padding: 12px 14px;
3139
+ }
3140
+ .metric span {
3141
+ display: block;
3142
+ color: #64748b;
3143
+ font-size: 12px;
3144
+ margin-bottom: 4px;
3145
+ }
3146
+ .metric strong {
3147
+ font-size: 22px;
3148
+ letter-spacing: 0;
3149
+ }
3150
+ .chart {
3151
+ background: #ffffff;
3152
+ border: 1px solid #d7dee8;
3153
+ border-radius: 6px;
3154
+ margin-top: 14px;
3155
+ padding: 16px;
3156
+ }
3157
+ .chart-title {
3158
+ display: flex;
3159
+ justify-content: space-between;
3160
+ align-items: baseline;
3161
+ gap: 12px;
3162
+ margin-bottom: 8px;
3163
+ }
3164
+ .chart-title span {
3165
+ color: #506174;
3166
+ font-size: 13px;
3167
+ }
3168
+ svg {
3169
+ width: 100%;
3170
+ height: auto;
3171
+ display: block;
3172
+ }
3173
+ .plot {
3174
+ fill: #fbfdff;
3175
+ stroke: #d7dee8;
3176
+ }
3177
+ .gap-band {
3178
+ fill: #eef2f6;
3179
+ opacity: 0.7;
3180
+ }
3181
+ .grid {
3182
+ stroke: #dbe4ee;
3183
+ stroke-width: 1;
3184
+ }
3185
+ .grid-soft {
3186
+ stroke: #eef3f8;
3187
+ stroke-width: 1;
3188
+ }
3189
+ .axis {
3190
+ fill: #64748b;
3191
+ font-size: 11px;
3192
+ }
3193
+ .setpoint {
3194
+ stroke: #64748b;
3195
+ stroke-width: 2;
3196
+ stroke-dasharray: 7 6;
3197
+ }
3198
+ .empty {
3199
+ color: #64748b;
3200
+ padding: 36px 0;
3201
+ text-align: center;
3202
+ }
3203
+ </style>
3204
+ </head>
3205
+ <body>
3206
+ <main>
3207
+ <header>
3208
+ <div>
3209
+ <h1>${escapeHtml(title)}</h1>
3210
+ <p>${escapeHtml(timeRange)}</p>
3211
+ </div>
3212
+ <div class="meta">
3213
+ <div>Generated: ${escapeHtml(new Date(generatedAt).toLocaleString())}</div>
3214
+ ${sourcePath ? `<div>Series: ${escapeHtml(sourcePath)}</div>` : ""}
3215
+ <div>Samples: ${merged.length}</div>
3216
+ </div>
3217
+ </header>
3218
+ <section class="metrics">
3219
+ <div class="metric"><span>Latest pH</span><strong>${escapeHtml(latest?.pH?.toFixed(2) ?? "n/a")}</strong></div>
3220
+ <div class="metric"><span>Latest ORP</span><strong>${escapeHtml(latest?.orp?.toFixed(0) ?? "n/a")}</strong></div>
3221
+ <div class="metric"><span>pH tank</span><strong>${escapeHtml(latest?.pHTankLevel ?? "n/a")}</strong></div>
3222
+ <div class="metric"><span>ORP tank</span><strong>${escapeHtml(latest?.orpTankLevel ?? "n/a")}</strong></div>
3223
+ </section>
3224
+ ${renderLineChart(merged, "pH", "pH", "#0f8b8d", "pHSetPoint")}
3225
+ ${renderLineChart(merged, "orp", "ORP", "#d95d39", "orpSetPoint")}
3226
+ </main>
3227
+ </body>
3228
+ </html>
3229
+ `;
3230
+ }
3231
+ function writeGraph(filePath, series) {
3232
+ mkdirSync4(path5.dirname(filePath), { recursive: true });
3233
+ writeFileSync5(filePath, renderGraph(series), "utf8");
3234
+ }
3235
+
3236
+ // src/actions/witness/witnesses/pentairIntellichem.ts
3237
+ var DAY_MS = 24 * 60 * 60 * 1e3;
3238
+ var DEFAULT_HISTORY_AGE = "24h";
3239
+ var INTELLICHEM_SCHEMA = "network.xyo.pentair.screenlogic.intellichem";
3240
+ function assertNumber2(value) {
3241
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
3242
+ }
3243
+ function parseDateOrAge(value, now, fallback) {
3244
+ if (!value) return fallback;
3245
+ const age = /^(\d+)(m|h|d)$/i.exec(value.trim());
3246
+ if (age) {
3247
+ const amount = Number(age[1]);
3248
+ const unit = age[2]?.toLowerCase();
3249
+ const multiplier = unit === "m" ? 60 * 1e3 : unit === "h" ? 60 * 60 * 1e3 : DAY_MS;
3250
+ return new Date(now.getTime() - amount * multiplier);
3251
+ }
3252
+ const date = new Date(value);
3253
+ if (Number.isNaN(date.getTime())) {
3254
+ throw new TypeError(`Invalid date or relative age: ${value}`);
3255
+ }
3256
+ return date;
3257
+ }
3258
+ function sampleKey(sample) {
3259
+ return sample.timestamp;
3260
+ }
3261
+ function mergeSamples2(samples) {
3262
+ const byTimestamp = /* @__PURE__ */ new Map();
3263
+ for (const sample of samples) {
3264
+ const existing = byTimestamp.get(sampleKey(sample));
3265
+ byTimestamp.set(sampleKey(sample), existing ? { ...existing, ...sample } : sample);
3266
+ }
3267
+ return [...byTimestamp.values()].toSorted((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp));
3268
+ }
3269
+ function currentChemSample(chem, observedAt) {
3270
+ return {
3271
+ alkalinity: assertNumber2(chem.alkalinity),
3272
+ calcium: assertNumber2(chem.calcium),
3273
+ corrosive: chem.corrosive,
3274
+ cyanuricAcid: assertNumber2(chem.cyanuricAcid),
3275
+ error: chem.error,
3276
+ orp: assertNumber2(chem.orp),
3277
+ orpSetPoint: assertNumber2(chem.orpSetPoint),
3278
+ orpTankLevel: assertNumber2(chem.orpTankLevel),
3279
+ pH: assertNumber2(chem.pH),
3280
+ pHSetPoint: assertNumber2(chem.pHSetPoint),
3281
+ pHTankLevel: assertNumber2(chem.pHTankLevel),
3282
+ saltPPM: assertNumber2(chem.saltPPM),
3283
+ saturation: assertNumber2(chem.saturation),
3284
+ scaling: chem.scaling,
3285
+ source: "current",
3286
+ temperature: assertNumber2(chem.temperature),
3287
+ timestamp: observedAt.toISOString()
3288
+ };
3289
+ }
3290
+ function historySamples(history) {
3291
+ const samples = /* @__PURE__ */ new Map();
3292
+ for (const point of history.phPoints ?? []) {
3293
+ const timestamp = point.time.toISOString();
3294
+ samples.set(timestamp, {
3295
+ ...samples.get(timestamp),
3296
+ pH: point.pH,
3297
+ source: "history",
3298
+ timestamp
3299
+ });
3300
+ }
3301
+ for (const point of history.orpPoints ?? []) {
3302
+ const timestamp = point.time.toISOString();
3303
+ samples.set(timestamp, {
3304
+ ...samples.get(timestamp),
3305
+ orp: point.orp,
3306
+ source: "history",
3307
+ timestamp
3308
+ });
3309
+ }
3310
+ return mergeSamples2([...samples.values()]);
3311
+ }
3312
+ function loadSamples(filePath) {
3313
+ if (!existsSync5(filePath)) return [];
3314
+ const raw = readFileSync5(filePath, "utf8");
3315
+ const samples = [];
3316
+ for (const line of raw.split(/\r?\n/)) {
3317
+ if (!line.trim()) continue;
3318
+ try {
3319
+ const parsed = JSON.parse(line);
3320
+ if (typeof parsed.timestamp === "string" && typeof parsed.source === "string") {
3321
+ samples.push(parsed);
3322
+ }
3323
+ } catch {
3324
+ }
3325
+ }
3326
+ return mergeSamples2(samples);
3327
+ }
3328
+ function saveSamples(filePath, samples) {
3329
+ const merged = mergeSamples2([...loadSamples(filePath), ...samples]);
3330
+ mkdirSync5(path6.dirname(filePath), { recursive: true });
3331
+ writeFileSync6(filePath, `${merged.map((sample) => JSON.stringify(sample)).join("\n")}
3332
+ `, "utf8");
3333
+ return merged;
3334
+ }
3335
+ var pentairIntellichemWitness = {
3336
+ builder: (yargs) => {
3337
+ return yargs.option("history", {
3338
+ default: false,
3339
+ describe: "Fetch IntelliChem controller history for the time window",
3340
+ type: "boolean"
3341
+ }).option("from", {
3342
+ default: DEFAULT_HISTORY_AGE,
3343
+ describe: "History start as ISO date or relative age like 30m, 6h, 7d",
3344
+ type: "string"
3345
+ }).option("to", {
3346
+ describe: "History end as ISO date or relative age (default: now)",
3347
+ type: "string"
3348
+ }).option("address", {
3349
+ describe: "Connect directly to a ScreenLogic adapter IP instead of UDP discovery",
3350
+ type: "string"
3351
+ }).option("port", {
3352
+ describe: "ScreenLogic adapter TCP port for direct connection (default: config or 80)",
3353
+ type: "number"
3354
+ }).option("password", {
3355
+ describe: "ScreenLogic password for direct connection, if configured",
3356
+ type: "string"
3357
+ }).option("system-name", {
3358
+ describe: "ScreenLogic system name for direct connection (default: config or Pentair ScreenLogic)",
3359
+ type: "string"
3360
+ }).option("search-timeout", {
3361
+ describe: "Milliseconds to wait for each UDP discovery attempt (default: config or 5000)",
3362
+ type: "number"
3363
+ }).option("search-attempts", {
3364
+ describe: "Number of UDP discovery attempts before failing (default: config or 3)",
3365
+ type: "number"
3366
+ }).option("response-timeout", {
3367
+ describe: "Milliseconds to wait for each ScreenLogic command response (default: config or 15000)",
3368
+ type: "number"
3369
+ }).option("save", {
3370
+ default: false,
3371
+ describe: "Save normalized IntelliChem samples to the series file",
3372
+ type: "boolean"
3373
+ }).option("series-file", {
3374
+ describe: `JSONL file used for saved IntelliChem samples and graphing (default: config or ${defaultPentairSeriesFile()})`,
3375
+ type: "string"
3376
+ }).option("graph", {
3377
+ describe: "Write a standalone HTML graph to this path",
3378
+ type: "string"
3379
+ }).option("graph-only", {
3380
+ default: false,
3381
+ describe: "Render a graph from the series file without connecting to ScreenLogic",
3382
+ type: "boolean"
3383
+ });
3384
+ },
3385
+ meta: {
3386
+ description: "Capture and graph Pentair IntelliChem chemistry data",
3387
+ name: "pentair-intellichem",
3388
+ schema: INTELLICHEM_SCHEMA
3389
+ },
3390
+ observe: async (args) => {
3391
+ const options = args;
3392
+ const pentairOptions = resolvePentairConfig(options);
3393
+ const now = /* @__PURE__ */ new Date();
3394
+ const wantsHistory = options.history === true || typeof options.graph === "string";
3395
+ const from = parseDateOrAge(options.from, now, new Date(now.getTime() - DAY_MS));
3396
+ const to = parseDateOrAge(options.to, now, now);
3397
+ const seriesFile = pentairOptions["series-file"] ?? defaultPentairSeriesFile();
3398
+ if (from.getTime() >= to.getTime()) {
3399
+ throw new Error("History start must be before history end");
3400
+ }
3401
+ if (options["graph-only"]) {
3402
+ if (!options.graph) throw new Error("--graph-only requires --graph");
3403
+ const graphSamples2 = loadSamples(seriesFile);
3404
+ writeGraph(options.graph, {
3405
+ generatedAt: now.toISOString(),
3406
+ samples: graphSamples2,
3407
+ sourcePath: seriesFile,
3408
+ title: "Pentair IntelliChem"
3409
+ });
3410
+ return {
3411
+ payloads: [{
3412
+ graph: {
3413
+ path: options.graph,
3414
+ samples: graphSamples2.length
3415
+ },
3416
+ samples: {
3417
+ seriesFile,
3418
+ stored: graphSamples2.length
3419
+ },
3420
+ schema: INTELLICHEM_SCHEMA
3421
+ }]
3422
+ };
3423
+ }
3424
+ const observed = await withScreenLogic(options, async (unit) => ({
3425
+ current: await unit.chem.getChemicalDataAsync(),
3426
+ history: wantsHistory ? await unit.chem.getChemHistoryDataAsync(from, to) : void 0
3427
+ }));
3428
+ const currentSample = currentChemSample(observed.current, now);
3429
+ const observedSamples = mergeSamples2([
3430
+ ...observed.history ? historySamples(observed.history) : [],
3431
+ currentSample
3432
+ ]);
3433
+ const savedSamples = options.save ? saveSamples(seriesFile, observedSamples) : void 0;
3434
+ const graphSamples = options.graph ? mergeSamples2([...loadSamples(seriesFile), ...observedSamples]) : void 0;
3435
+ if (options.graph && graphSamples) {
3436
+ writeGraph(options.graph, {
3437
+ generatedAt: now.toISOString(),
3438
+ samples: graphSamples,
3439
+ sourcePath: seriesFile,
3440
+ title: "Pentair IntelliChem"
3441
+ });
3442
+ }
3443
+ const payload = {
3444
+ current: observed.current,
3445
+ graph: options.graph ? {
3446
+ path: options.graph,
3447
+ samples: graphSamples?.length ?? 0
3448
+ } : void 0,
3449
+ history: observed.history,
3450
+ historyWindow: wantsHistory ? {
3451
+ from: from.toISOString(),
3452
+ to: to.toISOString()
3453
+ } : void 0,
3454
+ samples: {
3455
+ observed: observedSamples.length,
3456
+ saved: savedSamples?.length,
3457
+ seriesFile: options.save || options.graph ? seriesFile : void 0
3458
+ },
3459
+ schema: INTELLICHEM_SCHEMA
3460
+ };
3461
+ return { payloads: [payload] };
3462
+ }
3463
+ };
3464
+
3465
+ // src/actions/witness/witnesses/pentairSchedules.ts
3466
+ import { SchedTypes as SchedTypes2 } from "node-screenlogic";
3467
+ var PENTAIR_SCHEDULES_SCHEMA = "network.xyo.pentair.screenlogic.schedules";
3468
+ function circuitName(circuit) {
3469
+ return circuit?.name && circuit.name.length > 0 ? circuit.name : void 0;
3470
+ }
3471
+ function normalizePumpProfile(pump, pumpCircuit, circuitsById) {
3472
+ const name = circuitName(circuitsById.get(pumpCircuit.circuit));
3473
+ return {
3474
+ circuitId: pumpCircuit.circuit,
3475
+ ...name && { circuitName: name },
3476
+ ...!(pumpCircuit.flow === void 0) && { flow: pumpCircuit.flow },
3477
+ pumpId: pump.id,
3478
+ pumpName: pump.name,
3479
+ ...!(pumpCircuit.speed === void 0) && { speed: pumpCircuit.speed },
3480
+ units: pumpCircuit.units
3481
+ };
3482
+ }
3483
+ function normalizePump(pump, circuitsById, pumpStatuses) {
3484
+ const status = pumpStatuses.get(pump.id);
3485
+ return {
3486
+ circuits: pump.circuits.map((pumpCircuit) => normalizePumpProfile(pump, pumpCircuit, circuitsById)),
3487
+ id: pump.id,
3488
+ maxSpeed: pump.maxSpeed,
3489
+ minSpeed: pump.minSpeed,
3490
+ name: pump.name,
3491
+ ...typeof status === "string" && { statusError: status },
3492
+ ...typeof status === "object" && { status }
3493
+ };
3494
+ }
3495
+ function normalizeSchedules(rows, scheduleType, circuitsById, statesById, profilesByCircuitId) {
3496
+ return rows.map((schedule) => {
3497
+ const circuit = circuitsById.get(schedule.circuitId);
3498
+ const name = circuitName(circuit);
3499
+ const currentState = statesById.get(schedule.circuitId);
3500
+ const pumpProfile = profilesByCircuitId.get(schedule.circuitId);
3501
+ return {
3502
+ ...schedule,
3503
+ ...name && { circuitName: name },
3504
+ ...!(currentState === void 0) && { currentState },
3505
+ ...!(circuit?.eggTimer === void 0) && { eggTimer: circuit.eggTimer },
3506
+ ...!(pumpProfile === void 0) && { pumpProfile },
3507
+ scheduleType
3508
+ };
3509
+ });
3510
+ }
3511
+ function buildPentairSchedulesPayload({
3512
+ controllerCircuits,
3513
+ equipmentState,
3514
+ pumps,
3515
+ pumpStatuses = /* @__PURE__ */ new Map(),
3516
+ recurring,
3517
+ runOnce,
3518
+ systemTime
3519
+ }) {
3520
+ const circuitsById = new Map(controllerCircuits.map((circuit) => [circuit.circuitId, circuit]));
3521
+ const statesById = new Map(equipmentState.circuitArray.map((circuit) => [circuit.id, circuit.state]));
3522
+ const pumpSummaries = pumps.map((pump) => normalizePump(pump, circuitsById, pumpStatuses));
3523
+ const profilesByCircuitId = new Map(
3524
+ pumpSummaries.flatMap((pump) => pump.circuits.map((profile) => [profile.circuitId, profile]))
3525
+ );
3526
+ const recurringSchedules = normalizeSchedules(
3527
+ recurring.data,
3528
+ "recurring",
3529
+ circuitsById,
3530
+ statesById,
3531
+ profilesByCircuitId
3532
+ );
3533
+ const runOnceSchedules = normalizeSchedules(
3534
+ runOnce.data,
3535
+ "runOnce",
3536
+ circuitsById,
3537
+ statesById,
3538
+ profilesByCircuitId
3539
+ );
3540
+ const allSchedules = [...recurringSchedules, ...runOnceSchedules].toSorted((a, b) => a.scheduleType.localeCompare(b.scheduleType) || a.scheduleId - b.scheduleId || a.circuitId - b.circuitId);
3541
+ return {
3542
+ circuits: controllerCircuits.map((circuit) => ({
3543
+ circuitId: circuit.circuitId,
3544
+ eggTimer: circuit.eggTimer,
3545
+ function: circuit.function,
3546
+ interface: circuit.interface,
3547
+ name: circuit.name,
3548
+ state: statesById.get(circuit.circuitId)
3549
+ })),
3550
+ counts: {
3551
+ all: allSchedules.length,
3552
+ recurring: recurringSchedules.length,
3553
+ runOnce: runOnceSchedules.length
3554
+ },
3555
+ pumps: pumpSummaries,
3556
+ schedules: {
3557
+ all: allSchedules,
3558
+ recurring: recurringSchedules,
3559
+ runOnce: runOnceSchedules
3560
+ },
3561
+ systemTime
3562
+ };
3563
+ }
3564
+ function addPentairConnectionOptions(yargs) {
3565
+ return yargs.option("address", {
3566
+ describe: "Connect directly to a ScreenLogic adapter IP instead of UDP discovery",
3567
+ type: "string"
3568
+ }).option("port", {
3569
+ describe: "ScreenLogic adapter TCP port for direct connection (default: config or 80)",
3570
+ type: "number"
3571
+ }).option("password", {
3572
+ describe: "ScreenLogic password for direct connection, if configured",
3573
+ type: "string"
3574
+ }).option("system-name", {
3575
+ describe: "ScreenLogic system name for direct connection (default: config or Pentair ScreenLogic)",
3576
+ type: "string"
3577
+ }).option("search-timeout", {
3578
+ describe: "Milliseconds to wait for each UDP discovery attempt (default: config or 5000)",
3579
+ type: "number"
3580
+ }).option("search-attempts", {
3581
+ describe: "Number of UDP discovery attempts before failing (default: config or 3)",
3582
+ type: "number"
3583
+ }).option("response-timeout", {
3584
+ describe: "Milliseconds to wait for each ScreenLogic command response (default: config or 15000)",
3585
+ type: "number"
3586
+ });
3587
+ }
3588
+ var pentairSchedulesWitness = {
3589
+ builder: (yargs) => addPentairConnectionOptions(yargs),
3590
+ meta: {
3591
+ description: "Capture Pentair ScreenLogic schedules with circuit and pump context",
3592
+ name: "pentair-schedules",
3593
+ schema: PENTAIR_SCHEDULES_SCHEMA
3594
+ },
3595
+ observe: async (args) => {
3596
+ const payload = await withScreenLogic(args, async (unit) => {
3597
+ const equipmentConfig = await unit.equipment.getEquipmentConfigurationAsync();
3598
+ const controllerConfig = await unit.equipment.getControllerConfigAsync();
3599
+ const equipmentState = await unit.equipment.getEquipmentStateAsync();
3600
+ const systemTime = await unit.equipment.getSystemTimeAsync();
3601
+ const recurring = await unit.schedule.getScheduleDataAsync(SchedTypes2.RECURRING);
3602
+ const runOnce = await unit.schedule.getScheduleDataAsync(SchedTypes2.RUNONCE);
3603
+ const pumpStatuses = /* @__PURE__ */ new Map();
3604
+ for (const pump of equipmentConfig.pumps) {
3605
+ try {
3606
+ pumpStatuses.set(pump.id, await unit.pump.getPumpStatusAsync(pump.id));
3607
+ } catch (error) {
3608
+ pumpStatuses.set(pump.id, error instanceof Error ? error.message : String(error));
3609
+ }
3610
+ }
3611
+ return {
3612
+ ...buildPentairSchedulesPayload({
3613
+ controllerCircuits: controllerConfig.circuitArray,
3614
+ equipmentState,
3615
+ pumps: equipmentConfig.pumps,
3616
+ pumpStatuses,
3617
+ recurring,
3618
+ runOnce,
3619
+ systemTime
3620
+ }),
3621
+ schema: PENTAIR_SCHEDULES_SCHEMA
3622
+ };
3623
+ });
3624
+ return { payloads: [payload] };
3625
+ }
3626
+ };
3627
+
3628
+ // src/actions/witness/witnesses/systemInfo.ts
3629
+ import {
3630
+ arch,
3631
+ cpus,
3632
+ freemem,
3633
+ hostname,
3634
+ platform,
3635
+ release,
3636
+ totalmem,
3637
+ uptime
3638
+ } from "node:os";
3639
+ var systemInfoWitness = {
3640
+ meta: {
3641
+ description: "Capture system information (OS, CPU, memory, etc.)",
3642
+ name: "system-info",
3643
+ schema: "network.xyo.system.info"
3644
+ },
3645
+ observe: async () => ({
3646
+ payloads: [{
3647
+ arch: arch(),
3648
+ cpus: cpus().length,
3649
+ freeMemory: freemem(),
3650
+ hostname: hostname(),
3651
+ nodeVersion: process.version,
3652
+ os: release(),
3653
+ platform: platform(),
3654
+ schema: "network.xyo.system.info",
3655
+ totalMemory: totalmem(),
3656
+ uptime: uptime()
3657
+ }]
3658
+ })
3659
+ };
3660
+
3661
+ // src/actions/witness/witnesses/timestamp.ts
3662
+ var timestampWitness = {
3663
+ meta: {
3664
+ description: "Capture the current timestamp",
3665
+ name: "timestamp",
3666
+ schema: "network.xyo.timestamp"
3667
+ },
3668
+ observe: async () => ({
3669
+ payloads: [{
3670
+ schema: "network.xyo.timestamp",
3671
+ timestamp: Date.now()
3672
+ }]
3673
+ })
3674
+ };
3675
+
3676
+ // src/actions/witness/witnesses/url.ts
3677
+ import { hashBinary as hashBinary5 } from "@ariestools/aries-witness-core";
3678
+ async function fetchUrl(url) {
3679
+ const response = await fetch(url);
3680
+ const arrayBuffer = await response.arrayBuffer();
3681
+ const body = Buffer.from(arrayBuffer);
3682
+ const headers = Object.fromEntries(response.headers.entries());
3683
+ return {
3684
+ body,
3685
+ headers,
3686
+ statusCode: response.status
3687
+ };
3688
+ }
3689
+ function extractTitle(html) {
3690
+ const match = /<title[^>]*>([^<]*)<\/title>/i.exec(html);
3691
+ return match?.[1]?.trim();
3692
+ }
3693
+ function isBinaryContentType(contentType) {
3694
+ return contentType.startsWith("image/") || contentType.startsWith("audio/") || contentType.startsWith("video/") || contentType === "application/pdf" || contentType === "application/octet-stream";
3695
+ }
3696
+ var urlWitness = {
3697
+ builder: (yargs) => {
3698
+ return yargs.option("url", {
3699
+ demandOption: true,
3700
+ describe: "The URL to fetch",
3701
+ type: "string"
3702
+ });
3703
+ },
3704
+ meta: {
3705
+ description: "Capture metadata and content from a URL",
3706
+ name: "url",
3707
+ schema: "network.xyo.url"
3708
+ },
3709
+ observe: async (args) => {
3710
+ const url = args.url;
3711
+ const {
3712
+ body,
3713
+ statusCode,
3714
+ headers
3715
+ } = await fetchUrl(url);
3716
+ const contentType = headers["content-type"]?.split(";", 1)[0]?.trim() ?? "application/octet-stream";
3717
+ const contentHash = hashBinary5(body);
3718
+ const payload = {
3719
+ contentHash,
3720
+ contentLength: body.length,
3721
+ contentType,
3722
+ fetchedAt: Date.now(),
3723
+ schema: "network.xyo.url",
3724
+ statusCode,
3725
+ url
3726
+ };
3727
+ if (contentType === "text/html") {
3728
+ const html = body.toString("utf8");
3729
+ const title = extractTitle(html);
3730
+ if (title) payload.title = title;
3731
+ }
3732
+ const binaries = [];
3733
+ if (isBinaryContentType(contentType)) {
3734
+ const ref = {
3735
+ filename: url.split("/").pop()?.split("?", 1)[0],
3736
+ hash: contentHash,
3737
+ mimeType: contentType,
3738
+ size: body.length
3739
+ };
3740
+ binaries.push({ data: body, ref });
3741
+ }
3742
+ return { binaries, payloads: [payload] };
3743
+ }
3744
+ };
3745
+
3746
+ // src/actions/witness/witnesses/index.ts
3747
+ var builtInWitnesses = [
3748
+ timestampWitness,
3749
+ systemInfoWitness,
3750
+ urlWitness,
3751
+ bankWitness,
3752
+ netflixCatalogWitness,
3753
+ pentairWitness,
3754
+ pentairIntellichemWitness,
3755
+ pentairSchedulesWitness
3756
+ ];
3757
+
3758
+ // src/commands/witnessCommand.ts
3759
+ var buildWitnessCommands = async (yargs) => {
3760
+ for (const plugin of builtInWitnesses) {
3761
+ yargs.command(registerWitnessCommand(plugin));
3762
+ }
3763
+ const discovered = await discoverPlugins();
3764
+ for (const plugin of discovered) {
3765
+ yargs.command(registerWitnessCommand(plugin));
3766
+ }
3767
+ yargs.command(witnessAppCommand);
3768
+ return yargs.demandCommand(1).strictCommands();
3769
+ };
3770
+ var witnessCommand = {
3771
+ command: "witness",
3772
+ describe: "Observe data and produce XYO-compliant payloads",
3773
+ builder: buildWitnessCommands,
3774
+ handler: noopHandler3
3775
+ };
3776
+
3777
+ // src/witness.ts
3778
+ import { runYargsCli } from "@ariestools/aries-cli-core";
3779
+ var witness = async () => {
3780
+ await runYargsCli({
3781
+ configure: buildWitnessCommands,
3782
+ demandCommand: false,
3783
+ scriptName: "aries-witness",
3784
+ strictCommands: false,
3785
+ usage: "$0 <command> [options]"
3786
+ });
3787
+ };
3788
+ export {
3789
+ BANK_ACCOUNT_SNAPSHOT_SCHEMA,
3790
+ BANK_PRODUCT_CATALOG_SCHEMA,
3791
+ CAPITAL_ONE_CONFIG_KEYS,
3792
+ PENTAIR_CONFIG_KEYS,
3793
+ bankCommand,
3794
+ bankConfigDir,
3795
+ buildWitnessCommands,
3796
+ capitalOneConfigGet,
3797
+ capitalOneConfigPath,
3798
+ capitalOneConfigSet,
3799
+ capitalOneProductsProvider,
3800
+ capitalOneProvider,
3801
+ defaultCapitalOneConfig,
3802
+ defaultPentairConfig,
3803
+ defaultPentairSeriesFile,
3804
+ discoverPlugins,
3805
+ executeWitness,
3806
+ formatOutput,
3807
+ normalizeCapitalOneConfigKey,
3808
+ normalizePentairConfigKey,
3809
+ pentairCommand,
3810
+ pentairConfigDir,
3811
+ pentairConfigGet,
3812
+ pentairConfigPath,
3813
+ pentairConfigSet,
3814
+ readCapitalOneConfig,
3815
+ readPentairConfig,
3816
+ registerWitnessCommand,
3817
+ resolveCapitalOneConfig,
3818
+ resolveCircuitRuntimeMinutes,
3819
+ resolvePentairConfig,
3820
+ setPentairCircuitRuntime,
3821
+ withScreenLogic,
3822
+ witness,
3823
+ witnessAppCommand,
3824
+ witnessAppListCommand,
3825
+ witnessCommand,
3826
+ writeCapitalOneConfig,
3827
+ writePentairConfig
3828
+ };
3829
+ //# sourceMappingURL=index.mjs.map