iobroker.ai-usage 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +132 -0
  3. package/admin/ai-usage.svg +19 -0
  4. package/admin/i18n/de.json +29 -0
  5. package/admin/i18n/en.json +29 -0
  6. package/admin/i18n/es.json +29 -0
  7. package/admin/i18n/fr.json +29 -0
  8. package/admin/i18n/it.json +29 -0
  9. package/admin/i18n/nl.json +29 -0
  10. package/admin/i18n/pl.json +29 -0
  11. package/admin/i18n/pt.json +29 -0
  12. package/admin/i18n/ru.json +29 -0
  13. package/admin/i18n/uk.json +29 -0
  14. package/admin/i18n/zh-cn.json +29 -0
  15. package/admin/jsonConfig.json +252 -0
  16. package/build/lib/http.js +81 -0
  17. package/build/lib/http.js.map +7 -0
  18. package/build/lib/poll-engine.js +341 -0
  19. package/build/lib/poll-engine.js.map +7 -0
  20. package/build/lib/provider.js +40 -0
  21. package/build/lib/provider.js.map +7 -0
  22. package/build/lib/providers/anthropic-api.js +120 -0
  23. package/build/lib/providers/anthropic-api.js.map +7 -0
  24. package/build/lib/providers/claude-auth.js +109 -0
  25. package/build/lib/providers/claude-auth.js.map +7 -0
  26. package/build/lib/providers/claude-sub.js +165 -0
  27. package/build/lib/providers/claude-sub.js.map +7 -0
  28. package/build/lib/providers/deepseek.js +67 -0
  29. package/build/lib/providers/deepseek.js.map +7 -0
  30. package/build/lib/providers/openai.js +136 -0
  31. package/build/lib/providers/openai.js.map +7 -0
  32. package/build/lib/providers/openrouter.js +71 -0
  33. package/build/lib/providers/openrouter.js.map +7 -0
  34. package/build/lib/providers/report-utils.js +59 -0
  35. package/build/lib/providers/report-utils.js.map +7 -0
  36. package/build/lib/pure-helpers.js +96 -0
  37. package/build/lib/pure-helpers.js.map +7 -0
  38. package/build/lib/snapshot-tree.js +175 -0
  39. package/build/lib/snapshot-tree.js.map +7 -0
  40. package/build/lib/totals.js +79 -0
  41. package/build/lib/totals.js.map +7 -0
  42. package/build/main.js +336 -0
  43. package/build/main.js.map +7 -0
  44. package/io-package.json +222 -0
  45. package/package.json +91 -0
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var totals_exports = {};
20
+ __export(totals_exports, {
21
+ computeTotals: () => computeTotals
22
+ });
23
+ module.exports = __toCommonJS(totals_exports);
24
+ var import_snapshot_tree = require("./snapshot-tree");
25
+ const TOTAL_CURRENCY = "USD";
26
+ function computeTotals(statuses) {
27
+ var _a, _b, _c, _d;
28
+ let costsToday = 0;
29
+ let costsMonth = 0;
30
+ let costsProjectedMonth = 0;
31
+ let maxPercent = 0;
32
+ let warningsActive = 0;
33
+ let limitReached = false;
34
+ let reachable = 0;
35
+ for (const status of statuses) {
36
+ if (status.reachable) {
37
+ reachable++;
38
+ }
39
+ if (status.warning) {
40
+ warningsActive++;
41
+ }
42
+ const snapshot = status.snapshot;
43
+ if (!snapshot) {
44
+ continue;
45
+ }
46
+ const costs = snapshot.costs;
47
+ if (costs && costs.currency === TOTAL_CURRENCY) {
48
+ costsToday += (_a = costs.today) != null ? _a : 0;
49
+ costsMonth += (_b = costs.month) != null ? _b : 0;
50
+ costsProjectedMonth += (_d = (_c = costs.projectedMonth) != null ? _c : costs.month) != null ? _d : 0;
51
+ }
52
+ const percent = (0, import_snapshot_tree.maxLimitPercent)(snapshot);
53
+ if (percent !== void 0) {
54
+ maxPercent = Math.max(maxPercent, percent);
55
+ if (percent >= 100) {
56
+ limitReached = true;
57
+ }
58
+ }
59
+ }
60
+ return {
61
+ costsToday: round2(costsToday),
62
+ costsMonth: round2(costsMonth),
63
+ costsProjectedMonth: round2(costsProjectedMonth),
64
+ currency: TOTAL_CURRENCY,
65
+ maxLimitPercent: round2(maxPercent),
66
+ warningsActive,
67
+ limitReached,
68
+ accountsReachable: reachable,
69
+ accounts: statuses.length
70
+ };
71
+ }
72
+ function round2(value) {
73
+ return Math.round(value * 100) / 100;
74
+ }
75
+ // Annotate the CommonJS export names for ESM import in node:
76
+ 0 && (module.exports = {
77
+ computeTotals
78
+ });
79
+ //# sourceMappingURL=totals.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/lib/totals.ts"],
4
+ "sourcesContent": ["import type { UsageSnapshot } from \"./provider\";\nimport { maxLimitPercent } from \"./snapshot-tree\";\n\n/** One account's contribution to the totals. */\nexport interface AccountStatus {\n /** The last successful snapshot, if any. */\n snapshot?: UsageSnapshot;\n /** Whether the account is currently reachable. */\n reachable: boolean;\n /** Whether the account is above its warn threshold. */\n warning: boolean;\n}\n\n/** The adapter-wide totals. */\nexport interface Totals {\n /** Summed real money spent today (same-currency accounts only). */\n costsToday: number;\n /** Summed real money spent this month. */\n costsMonth: number;\n /** Summed projected month-end spend. */\n costsProjectedMonth: number;\n /** The currency the sums are in. */\n currency: string;\n /** The highest limit utilisation of any account (percent). */\n maxLimitPercent: number;\n /** Number of accounts above their warn threshold. */\n warningsActive: number;\n /** True when any limit window is full (>= 100 %). */\n limitReached: boolean;\n /** Reachable accounts. */\n accountsReachable: number;\n /** Configured (enabled) accounts. */\n accounts: number;\n}\n\n/** The currency the totals are summed in. Non-matching and piece-counters stay out. */\nconst TOTAL_CURRENCY = \"USD\";\n\n/**\n * Compute the adapter-wide totals from the in-memory account statuses. Money sums\n * include only real-money costs in {@link TOTAL_CURRENCY}; piece-counters and\n * foreign currencies are excluded by design.\n *\n * @param statuses each account's status\n * @returns the totals\n */\nexport function computeTotals(statuses: readonly AccountStatus[]): Totals {\n let costsToday = 0;\n let costsMonth = 0;\n let costsProjectedMonth = 0;\n let maxPercent = 0;\n let warningsActive = 0;\n let limitReached = false;\n let reachable = 0;\n for (const status of statuses) {\n if (status.reachable) {\n reachable++;\n }\n if (status.warning) {\n warningsActive++;\n }\n const snapshot = status.snapshot;\n if (!snapshot) {\n continue;\n }\n const costs = snapshot.costs;\n if (costs && costs.currency === TOTAL_CURRENCY) {\n costsToday += costs.today ?? 0;\n costsMonth += costs.month ?? 0;\n costsProjectedMonth += costs.projectedMonth ?? costs.month ?? 0;\n }\n const percent = maxLimitPercent(snapshot);\n if (percent !== undefined) {\n maxPercent = Math.max(maxPercent, percent);\n if (percent >= 100) {\n limitReached = true;\n }\n }\n }\n return {\n costsToday: round2(costsToday),\n costsMonth: round2(costsMonth),\n costsProjectedMonth: round2(costsProjectedMonth),\n currency: TOTAL_CURRENCY,\n maxLimitPercent: round2(maxPercent),\n warningsActive,\n limitReached,\n accountsReachable: reachable,\n accounts: statuses.length,\n };\n}\n\n/**\n * Round to two decimals (money/percent display).\n *\n * @param value the value\n * @returns the rounded value\n */\nfunction round2(value: number): number {\n return Math.round(value * 100) / 100;\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,2BAAgC;AAmChC,MAAM,iBAAiB;AAUhB,SAAS,cAAc,UAA4C;AA9C1E;AA+CE,MAAI,aAAa;AACjB,MAAI,aAAa;AACjB,MAAI,sBAAsB;AAC1B,MAAI,aAAa;AACjB,MAAI,iBAAiB;AACrB,MAAI,eAAe;AACnB,MAAI,YAAY;AAChB,aAAW,UAAU,UAAU;AAC7B,QAAI,OAAO,WAAW;AACpB;AAAA,IACF;AACA,QAAI,OAAO,SAAS;AAClB;AAAA,IACF;AACA,UAAM,WAAW,OAAO;AACxB,QAAI,CAAC,UAAU;AACb;AAAA,IACF;AACA,UAAM,QAAQ,SAAS;AACvB,QAAI,SAAS,MAAM,aAAa,gBAAgB;AAC9C,qBAAc,WAAM,UAAN,YAAe;AAC7B,qBAAc,WAAM,UAAN,YAAe;AAC7B,8BAAuB,iBAAM,mBAAN,YAAwB,MAAM,UAA9B,YAAuC;AAAA,IAChE;AACA,UAAM,cAAU,sCAAgB,QAAQ;AACxC,QAAI,YAAY,QAAW;AACzB,mBAAa,KAAK,IAAI,YAAY,OAAO;AACzC,UAAI,WAAW,KAAK;AAClB,uBAAe;AAAA,MACjB;AAAA,IACF;AAAA,EACF;AACA,SAAO;AAAA,IACL,YAAY,OAAO,UAAU;AAAA,IAC7B,YAAY,OAAO,UAAU;AAAA,IAC7B,qBAAqB,OAAO,mBAAmB;AAAA,IAC/C,UAAU;AAAA,IACV,iBAAiB,OAAO,UAAU;AAAA,IAClC;AAAA,IACA;AAAA,IACA,mBAAmB;AAAA,IACnB,UAAU,SAAS;AAAA,EACrB;AACF;AAQA,SAAS,OAAO,OAAuB;AACrC,SAAO,KAAK,MAAM,QAAQ,GAAG,IAAI;AACnC;",
6
+ "names": []
7
+ }
package/build/main.js ADDED
@@ -0,0 +1,336 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var main_exports = {};
30
+ __export(main_exports, {
31
+ AiUsageAdapter: () => AiUsageAdapter
32
+ });
33
+ module.exports = __toCommonJS(main_exports);
34
+ var utils = __toESM(require("@iobroker/adapter-core"));
35
+ var import_adapter_core = require("@iobroker/adapter-core");
36
+ var import_promises = require("node:fs/promises");
37
+ var import_node_path = require("node:path");
38
+ var import_http = require("./lib/http");
39
+ var import_poll_engine = require("./lib/poll-engine");
40
+ var import_pure_helpers = require("./lib/pure-helpers");
41
+ var import_claude_auth = require("./lib/providers/claude-auth");
42
+ var import_anthropic_api = require("./lib/providers/anthropic-api");
43
+ var import_claude_sub = require("./lib/providers/claude-sub");
44
+ var import_deepseek = require("./lib/providers/deepseek");
45
+ var import_openai = require("./lib/providers/openai");
46
+ var import_openrouter = require("./lib/providers/openrouter");
47
+ class AiUsageAdapter extends utils.Adapter {
48
+ engine = null;
49
+ /** Pending Claude sign-in attempts, keyed by account id (PKCE lives only in memory). */
50
+ pendingClaudeAuth = /* @__PURE__ */ new Map();
51
+ /**
52
+ * @param options the adapter options
53
+ */
54
+ constructor(options = {}) {
55
+ super({ ...options, name: "ai-usage" });
56
+ this.on("ready", this.onReady.bind(this));
57
+ this.on("message", this.onMessage.bind(this));
58
+ this.on("unload", this.onUnload.bind(this));
59
+ }
60
+ /**
61
+ * Handle admin messages — the guided Claude subscription sign-in.
62
+ *
63
+ * @param obj the message
64
+ */
65
+ async onMessage(obj) {
66
+ var _a;
67
+ try {
68
+ switch (obj.command) {
69
+ case "claudeAuthStart": {
70
+ const accountId = this.claudeAccountIdFrom(obj.message);
71
+ if (!accountId) {
72
+ this.respond(obj, "\u2192 Enter the exact name of a Claude subscription row from the table above");
73
+ return;
74
+ }
75
+ const pkce = (0, import_claude_auth.generatePkce)();
76
+ this.pendingClaudeAuth.set(accountId, pkce);
77
+ this.respond(obj, (0, import_claude_auth.buildAuthorizeUrl)(pkce));
78
+ return;
79
+ }
80
+ case "claudeAuthCode": {
81
+ const accountId = this.claudeAccountIdFrom(obj.message);
82
+ const code = typeof ((_a = obj.message) == null ? void 0 : _a.code) === "string" ? obj.message.code : "";
83
+ const pkce = accountId ? this.pendingClaudeAuth.get(accountId) : void 0;
84
+ if (!accountId || !pkce) {
85
+ this.respond(obj, { error: "Generate the sign-in link first (step 1)" });
86
+ return;
87
+ }
88
+ if (!code.trim()) {
89
+ this.respond(obj, { error: "Paste the code from the Anthropic page first" });
90
+ return;
91
+ }
92
+ try {
93
+ const tokens = await (0, import_claude_auth.exchangeCode)(code, pkce, import_http.postJson, Date.now());
94
+ await this.claudeTokenStore(accountId).save(tokens);
95
+ this.pendingClaudeAuth.delete(accountId);
96
+ this.respond(obj, { result: "Signed in \u2014 restart the instance (or save the settings) to start polling" });
97
+ } catch (e) {
98
+ this.respond(obj, { error: `Sign-in failed: ${e instanceof Error ? e.message : String(e)}` });
99
+ }
100
+ return;
101
+ }
102
+ default:
103
+ this.respond(obj, { error: `Unknown command: ${obj.command}` });
104
+ }
105
+ } catch (e) {
106
+ this.log.error(`onMessage failed: ${e instanceof Error ? e.message : String(e)}`);
107
+ this.respond(obj, { error: "internal error \u2014 see log" });
108
+ }
109
+ }
110
+ /**
111
+ * Send a message response, when the caller expects one.
112
+ *
113
+ * @param obj the request message
114
+ * @param response the response payload
115
+ */
116
+ respond(obj, response) {
117
+ if (obj.callback) {
118
+ this.sendTo(obj.from, obj.command, response, obj.callback);
119
+ }
120
+ }
121
+ /**
122
+ * Resolve the account id for a Claude sign-in message: the given name must match
123
+ * a claude-sub row of the accounts table.
124
+ *
125
+ * @param message the message payload ({ account })
126
+ * @returns the id-safe account id, or undefined
127
+ */
128
+ claudeAccountIdFrom(message) {
129
+ const name = typeof (message == null ? void 0 : message.account) === "string" ? message.account : "";
130
+ const id = (0, import_pure_helpers.sanitizeId)(name);
131
+ if (!id) {
132
+ return void 0;
133
+ }
134
+ const accounts = (0, import_pure_helpers.parseAccounts)(this.config.accounts);
135
+ return accounts.some((account) => account.id === id && account.provider === "claude-sub") ? id : void 0;
136
+ }
137
+ /**
138
+ * The persistent token storage for one Claude account: an encrypted JSON file in
139
+ * the instance data directory (a `native` write would restart the instance).
140
+ *
141
+ * @param accountId the id-safe account id
142
+ * @returns the store
143
+ */
144
+ claudeTokenStore(accountId) {
145
+ const dir = utils.getAbsoluteInstanceDataDir(this);
146
+ const file = (0, import_node_path.join)(dir, `claude-tokens-${accountId}.json`);
147
+ return {
148
+ load: async () => {
149
+ try {
150
+ const encrypted = await (0, import_promises.readFile)(file, "utf8");
151
+ const parsed = JSON.parse(this.decrypt(encrypted));
152
+ if (typeof parsed.accessToken !== "string" || typeof parsed.refreshToken !== "string") {
153
+ return null;
154
+ }
155
+ return {
156
+ accessToken: parsed.accessToken,
157
+ refreshToken: parsed.refreshToken,
158
+ expiresAt: Number(parsed.expiresAt) || 0
159
+ };
160
+ } catch {
161
+ return null;
162
+ }
163
+ },
164
+ save: async (tokens) => {
165
+ await (0, import_promises.mkdir)(dir, { recursive: true });
166
+ await (0, import_promises.writeFile)(file, this.encrypt(JSON.stringify(tokens)), "utf8");
167
+ }
168
+ };
169
+ }
170
+ /** Validate the configuration, clean up stale account trees and start the engine. */
171
+ async onReady() {
172
+ try {
173
+ const accounts = (0, import_pure_helpers.parseAccounts)(this.config.accounts);
174
+ const interval = (0, import_pure_helpers.clampPollInterval)(this.config.pollInterval);
175
+ await this.cleanupStaleAccounts();
176
+ if (accounts.length === 0) {
177
+ this.log.info("No AI accounts configured \u2014 add accounts in the instance settings");
178
+ await this.setState("info.connection", { val: false, ack: true });
179
+ return;
180
+ }
181
+ const providers = /* @__PURE__ */ new Map();
182
+ for (const account of accounts) {
183
+ const provider = await this.makeProvider(account);
184
+ if (provider) {
185
+ providers.set(account.id, provider);
186
+ }
187
+ }
188
+ this.engine = new import_poll_engine.PollEngine(accounts, providers, interval, {
189
+ upsertObject: async (def) => {
190
+ await this.extendObject(def.id, { type: def.type, common: def.common, native: {} });
191
+ },
192
+ setState: (id, value) => {
193
+ void this.setState(id, { val: value, ack: true }).catch(() => {
194
+ });
195
+ },
196
+ schedule: (cb, ms) => ({ kind: "interval", handle: this.setInterval(cb, ms) }),
197
+ scheduleOnce: (cb, ms) => ({ kind: "timeout", handle: this.setTimeout(cb, ms) }),
198
+ cancel: (handle) => {
199
+ const timer = handle;
200
+ if (timer.kind === "interval") {
201
+ this.clearInterval(timer.handle);
202
+ } else {
203
+ this.clearTimeout(timer.handle);
204
+ }
205
+ },
206
+ now: () => Date.now(),
207
+ log: {
208
+ debug: (m) => this.log.debug(m),
209
+ info: (m) => this.log.info(m),
210
+ warn: (m) => this.log.warn(m),
211
+ error: (m) => this.log.error(m)
212
+ },
213
+ notify: this.config.notifications ? (_account, message) => void this.registerNotification("ai-usage", "userActionRequired", message).catch(
214
+ (e) => this.log.debug(`Could not raise notification: ${e instanceof Error ? e.message : String(e)}`)
215
+ ) : void 0
216
+ });
217
+ await this.engine.start();
218
+ this.log.info(`Monitoring ${providers.size} of ${accounts.length} AI account(s), polling every ${interval} s`);
219
+ } catch (e) {
220
+ this.log.error(`Startup failed: ${e instanceof Error ? e.message : String(e)}`);
221
+ }
222
+ }
223
+ /**
224
+ * Build the provider for one account, resolving its credential from the central
225
+ * storage. Accounts whose provider is not implemented yet, or whose credential
226
+ * cannot be read, are skipped (the engine logs the skip).
227
+ *
228
+ * @param account the validated account config
229
+ * @returns the provider, or undefined to skip the account
230
+ */
231
+ async makeProvider(account) {
232
+ switch (account.provider) {
233
+ case "claude-sub":
234
+ return (0, import_claude_sub.claudeSubProvider)(this.claudeTokenStore(account.id), void 0, import_http.postJson);
235
+ case "openrouter": {
236
+ const key = await this.resolveKey(account);
237
+ return key ? (0, import_openrouter.openRouterProvider)(key) : void 0;
238
+ }
239
+ case "deepseek": {
240
+ const key = await this.resolveKey(account);
241
+ return key ? (0, import_deepseek.deepSeekProvider)(key) : void 0;
242
+ }
243
+ case "openai": {
244
+ const key = await this.resolveKey(account);
245
+ return key ? (0, import_openai.openAiProvider)(key) : void 0;
246
+ }
247
+ case "anthropic-api": {
248
+ const key = await this.resolveKey(account);
249
+ return key ? (0, import_anthropic_api.anthropicApiProvider)(key) : void 0;
250
+ }
251
+ default:
252
+ return void 0;
253
+ }
254
+ }
255
+ /**
256
+ * Read and decrypt a key-form credential from the central credential storage.
257
+ *
258
+ * @param account the account whose credential to resolve
259
+ * @returns the key, or undefined (with a log line) when it cannot be read
260
+ */
261
+ async resolveKey(account) {
262
+ if (!account.credentialId) {
263
+ this.log.warn(`${account.name}: no credential selected \u2014 pick one in the instance settings`);
264
+ return void 0;
265
+ }
266
+ try {
267
+ const credential = await import_adapter_core.Credentials.getCredentials(this, account.credentialId);
268
+ const values = credential.values;
269
+ const key = typeof values.key === "string" && values.key ? values.key : void 0;
270
+ if (!key) {
271
+ this.log.warn(`${account.name}: credential ${account.credentialId} carries no API key`);
272
+ }
273
+ return key;
274
+ } catch (e) {
275
+ this.log.warn(
276
+ `${account.name}: cannot read credential ${account.credentialId} (${e instanceof Error ? e.message : String(e)})`
277
+ );
278
+ return void 0;
279
+ }
280
+ }
281
+ /**
282
+ * Delete the object trees of accounts that are no longer in the table. Disabled
283
+ * rows keep their tree (they are only paused); an EMPTY table deletes nothing —
284
+ * the guard against wiping everything through an accidental clear.
285
+ */
286
+ async cleanupStaleAccounts() {
287
+ const keepIds = (0, import_pure_helpers.validAccountIds)(this.config.accounts);
288
+ if (keepIds.length === 0) {
289
+ return;
290
+ }
291
+ const keep = /* @__PURE__ */ new Set([...keepIds, "info", "total"]);
292
+ try {
293
+ const objects = await this.getAdapterObjectsAsync();
294
+ const roots = /* @__PURE__ */ new Set();
295
+ for (const id of Object.keys(objects)) {
296
+ const relative = id.substring(this.namespace.length + 1);
297
+ const root = relative.split(".")[0];
298
+ if (root && !keep.has(root)) {
299
+ roots.add(root);
300
+ }
301
+ }
302
+ for (const root of roots) {
303
+ this.log.info(`Removing objects of no longer configured account "${root}"`);
304
+ await this.delObjectAsync(root, { recursive: true });
305
+ }
306
+ } catch (e) {
307
+ this.log.warn(`Cleanup of stale accounts failed: ${e instanceof Error ? e.message : String(e)}`);
308
+ }
309
+ }
310
+ /**
311
+ * Tear down synchronously — no async/await here, else the controller kills the
312
+ * process before cleanup finishes.
313
+ *
314
+ * @param callback invoked when cleanup is done
315
+ */
316
+ onUnload(callback) {
317
+ var _a;
318
+ try {
319
+ (_a = this.engine) == null ? void 0 : _a.stop();
320
+ this.engine = null;
321
+ void this.setState("info.connection", { val: false, ack: true });
322
+ } catch {
323
+ }
324
+ callback();
325
+ }
326
+ }
327
+ if (require.main !== module) {
328
+ module.exports = (options) => new AiUsageAdapter(options);
329
+ } else {
330
+ (() => new AiUsageAdapter())();
331
+ }
332
+ // Annotate the CommonJS export names for ESM import in node:
333
+ 0 && (module.exports = {
334
+ AiUsageAdapter
335
+ });
336
+ //# sourceMappingURL=main.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/main.ts"],
4
+ "sourcesContent": ["import * as utils from \"@iobroker/adapter-core\";\nimport { Credentials } from \"@iobroker/adapter-core\";\nimport { mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { postJson } from \"./lib/http\";\nimport { PollEngine } from \"./lib/poll-engine\";\nimport { clampPollInterval, parseAccounts, sanitizeId, validAccountIds, type AccountConfig } from \"./lib/pure-helpers\";\nimport type { UsageProvider } from \"./lib/provider\";\nimport {\n buildAuthorizeUrl,\n exchangeCode,\n generatePkce,\n type PkcePair,\n type TokenSet,\n} from \"./lib/providers/claude-auth\";\nimport { anthropicApiProvider } from \"./lib/providers/anthropic-api\";\nimport { claudeSubProvider, type TokenStore } from \"./lib/providers/claude-sub\";\nimport { deepSeekProvider } from \"./lib/providers/deepseek\";\nimport { openAiProvider } from \"./lib/providers/openai\";\nimport { openRouterProvider } from \"./lib/providers/openrouter\";\n\n/** A cancellable handle: interval or timeout \u2014 the engine treats them uniformly. */\ntype TimerHandle =\n | { kind: \"interval\"; handle: ioBroker.Interval | undefined }\n | { kind: \"timeout\"; handle: ioBroker.Timeout | undefined };\n\n/**\n * AI Usage adapter \u2014 polls the usage/limit/cost sources of configured AI accounts\n * (Claude subscription, OpenRouter, DeepSeek, OpenAI API, Anthropic API) and\n * mirrors them into read-only states. Orchestration lives in the\n * fully unit-tested {@link PollEngine}; this class only wires ioBroker IO to it.\n */\nexport class AiUsageAdapter extends utils.Adapter {\n private engine: PollEngine | null = null;\n /** Pending Claude sign-in attempts, keyed by account id (PKCE lives only in memory). */\n private readonly pendingClaudeAuth = new Map<string, PkcePair>();\n\n /**\n * @param options the adapter options\n */\n public constructor(options: Partial<utils.AdapterOptions> = {}) {\n super({ ...options, name: \"ai-usage\" });\n this.on(\"ready\", this.onReady.bind(this));\n this.on(\"message\", this.onMessage.bind(this));\n this.on(\"unload\", this.onUnload.bind(this));\n }\n\n /**\n * Handle admin messages \u2014 the guided Claude subscription sign-in.\n *\n * @param obj the message\n */\n private async onMessage(obj: ioBroker.Message): Promise<void> {\n try {\n switch (obj.command) {\n case \"claudeAuthStart\": {\n const accountId = this.claudeAccountIdFrom(obj.message);\n if (!accountId) {\n // Plain string \u2014 this feeds a textSendTo display in the admin.\n this.respond(obj, \"\u2192 Enter the exact name of a Claude subscription row from the table above\");\n return;\n }\n const pkce = generatePkce();\n this.pendingClaudeAuth.set(accountId, pkce);\n this.respond(obj, buildAuthorizeUrl(pkce));\n return;\n }\n case \"claudeAuthCode\": {\n const accountId = this.claudeAccountIdFrom(obj.message);\n const code =\n typeof (obj.message as { code?: unknown })?.code === \"string\" ? (obj.message as { code: string }).code : \"\";\n const pkce = accountId ? this.pendingClaudeAuth.get(accountId) : undefined;\n if (!accountId || !pkce) {\n this.respond(obj, { error: \"Generate the sign-in link first (step 1)\" });\n return;\n }\n if (!code.trim()) {\n this.respond(obj, { error: \"Paste the code from the Anthropic page first\" });\n return;\n }\n try {\n const tokens = await exchangeCode(code, pkce, postJson, Date.now());\n await this.claudeTokenStore(accountId).save(tokens);\n this.pendingClaudeAuth.delete(accountId);\n this.respond(obj, { result: \"Signed in \u2014 restart the instance (or save the settings) to start polling\" });\n } catch (e) {\n this.respond(obj, { error: `Sign-in failed: ${e instanceof Error ? e.message : String(e)}` });\n }\n return;\n }\n default:\n // Always answer, or the caller's callback would dangle until timeout.\n this.respond(obj, { error: `Unknown command: ${obj.command}` });\n }\n } catch (e) {\n this.log.error(`onMessage failed: ${e instanceof Error ? e.message : String(e)}`);\n this.respond(obj, { error: \"internal error \u2014 see log\" });\n }\n }\n\n /**\n * Send a message response, when the caller expects one.\n *\n * @param obj the request message\n * @param response the response payload\n */\n private respond(obj: ioBroker.Message, response: unknown): void {\n if (obj.callback) {\n this.sendTo(obj.from, obj.command, response, obj.callback);\n }\n }\n\n /**\n * Resolve the account id for a Claude sign-in message: the given name must match\n * a claude-sub row of the accounts table.\n *\n * @param message the message payload ({ account })\n * @returns the id-safe account id, or undefined\n */\n private claudeAccountIdFrom(message: unknown): string | undefined {\n const name =\n typeof (message as { account?: unknown })?.account === \"string\" ? (message as { account: string }).account : \"\";\n const id = sanitizeId(name);\n if (!id) {\n return undefined;\n }\n const accounts = parseAccounts(this.config.accounts);\n return accounts.some(account => account.id === id && account.provider === \"claude-sub\") ? id : undefined;\n }\n\n /**\n * The persistent token storage for one Claude account: an encrypted JSON file in\n * the instance data directory (a `native` write would restart the instance).\n *\n * @param accountId the id-safe account id\n * @returns the store\n */\n private claudeTokenStore(accountId: string): TokenStore {\n const dir = utils.getAbsoluteInstanceDataDir(this);\n const file = join(dir, `claude-tokens-${accountId}.json`);\n return {\n load: async (): Promise<TokenSet | null> => {\n try {\n const encrypted = await readFile(file, \"utf8\");\n const parsed = JSON.parse(this.decrypt(encrypted)) as Partial<TokenSet>;\n if (typeof parsed.accessToken !== \"string\" || typeof parsed.refreshToken !== \"string\") {\n return null;\n }\n return {\n accessToken: parsed.accessToken,\n refreshToken: parsed.refreshToken,\n expiresAt: Number(parsed.expiresAt) || 0,\n };\n } catch {\n return null; // never signed in (or unreadable) \u2014 the provider reports auth-required\n }\n },\n save: async (tokens: TokenSet): Promise<void> => {\n await mkdir(dir, { recursive: true });\n await writeFile(file, this.encrypt(JSON.stringify(tokens)), \"utf8\");\n },\n };\n }\n\n /** Validate the configuration, clean up stale account trees and start the engine. */\n private async onReady(): Promise<void> {\n try {\n const accounts = parseAccounts(this.config.accounts);\n const interval = clampPollInterval(this.config.pollInterval);\n await this.cleanupStaleAccounts();\n if (accounts.length === 0) {\n this.log.info(\"No AI accounts configured \u2014 add accounts in the instance settings\");\n await this.setState(\"info.connection\", { val: false, ack: true });\n return;\n }\n const providers = new Map<string, UsageProvider>();\n for (const account of accounts) {\n const provider = await this.makeProvider(account);\n if (provider) {\n providers.set(account.id, provider);\n }\n }\n this.engine = new PollEngine(accounts, providers, interval, {\n upsertObject: async def => {\n await this.extendObject(def.id, { type: def.type, common: def.common as ioBroker.ObjectCommon, native: {} });\n },\n setState: (id, value) => {\n void this.setState(id, { val: value, ack: true }).catch(() => {\n /* states DB going down \u2014 never crash the poll loop */\n });\n },\n schedule: (cb, ms): TimerHandle => ({ kind: \"interval\", handle: this.setInterval(cb, ms) }),\n scheduleOnce: (cb, ms): TimerHandle => ({ kind: \"timeout\", handle: this.setTimeout(cb, ms) }),\n cancel: handle => {\n const timer = handle as TimerHandle;\n if (timer.kind === \"interval\") {\n this.clearInterval(timer.handle);\n } else {\n this.clearTimeout(timer.handle);\n }\n },\n now: () => Date.now(),\n log: {\n debug: m => this.log.debug(m),\n info: m => this.log.info(m),\n warn: m => this.log.warn(m),\n error: m => this.log.error(m),\n },\n notify: this.config.notifications\n ? (_account, message) =>\n void this.registerNotification(\"ai-usage\", \"userActionRequired\", message).catch(e =>\n this.log.debug(`Could not raise notification: ${e instanceof Error ? e.message : String(e)}`),\n )\n : undefined,\n });\n await this.engine.start();\n this.log.info(`Monitoring ${providers.size} of ${accounts.length} AI account(s), polling every ${interval} s`);\n } catch (e) {\n this.log.error(`Startup failed: ${e instanceof Error ? e.message : String(e)}`);\n }\n }\n\n /**\n * Build the provider for one account, resolving its credential from the central\n * storage. Accounts whose provider is not implemented yet, or whose credential\n * cannot be read, are skipped (the engine logs the skip).\n *\n * @param account the validated account config\n * @returns the provider, or undefined to skip the account\n */\n private async makeProvider(account: AccountConfig): Promise<UsageProvider | undefined> {\n switch (account.provider) {\n case \"claude-sub\":\n return claudeSubProvider(this.claudeTokenStore(account.id), undefined, postJson);\n case \"openrouter\": {\n const key = await this.resolveKey(account);\n return key ? openRouterProvider(key) : undefined;\n }\n case \"deepseek\": {\n const key = await this.resolveKey(account);\n return key ? deepSeekProvider(key) : undefined;\n }\n case \"openai\": {\n const key = await this.resolveKey(account);\n return key ? openAiProvider(key) : undefined;\n }\n case \"anthropic-api\": {\n const key = await this.resolveKey(account);\n return key ? anthropicApiProvider(key) : undefined;\n }\n default:\n return undefined;\n }\n }\n\n /**\n * Read and decrypt a key-form credential from the central credential storage.\n *\n * @param account the account whose credential to resolve\n * @returns the key, or undefined (with a log line) when it cannot be read\n */\n private async resolveKey(account: AccountConfig): Promise<string | undefined> {\n if (!account.credentialId) {\n this.log.warn(`${account.name}: no credential selected \u2014 pick one in the instance settings`);\n return undefined;\n }\n try {\n const credential = await Credentials.getCredentials(this, account.credentialId);\n const values = credential.values as { key?: unknown; password?: unknown };\n const key = typeof values.key === \"string\" && values.key ? values.key : undefined;\n if (!key) {\n this.log.warn(`${account.name}: credential ${account.credentialId} carries no API key`);\n }\n return key;\n } catch (e) {\n this.log.warn(\n `${account.name}: cannot read credential ${account.credentialId} (${e instanceof Error ? e.message : String(e)})`,\n );\n return undefined;\n }\n }\n\n /**\n * Delete the object trees of accounts that are no longer in the table. Disabled\n * rows keep their tree (they are only paused); an EMPTY table deletes nothing \u2014\n * the guard against wiping everything through an accidental clear.\n */\n private async cleanupStaleAccounts(): Promise<void> {\n const keepIds = validAccountIds(this.config.accounts);\n if (keepIds.length === 0) {\n return;\n }\n const keep = new Set([...keepIds, \"info\", \"total\"]);\n try {\n const objects = await this.getAdapterObjectsAsync();\n const roots = new Set<string>();\n for (const id of Object.keys(objects)) {\n const relative = id.substring(this.namespace.length + 1);\n const root = relative.split(\".\")[0];\n if (root && !keep.has(root)) {\n roots.add(root);\n }\n }\n for (const root of roots) {\n this.log.info(`Removing objects of no longer configured account \"${root}\"`);\n await this.delObjectAsync(root, { recursive: true });\n }\n } catch (e) {\n this.log.warn(`Cleanup of stale accounts failed: ${e instanceof Error ? e.message : String(e)}`);\n }\n }\n\n /**\n * Tear down synchronously \u2014 no async/await here, else the controller kills the\n * process before cleanup finishes.\n *\n * @param callback invoked when cleanup is done\n */\n private onUnload(callback: () => void): void {\n try {\n this.engine?.stop();\n this.engine = null;\n void this.setState(\"info.connection\", { val: false, ack: true });\n } catch {\n // never block shutdown\n }\n callback();\n }\n}\n\nif (require.main !== module) {\n // Export the constructor in compact mode\n module.exports = (options: Partial<utils.AdapterOptions> | undefined) => new AiUsageAdapter(options);\n} else {\n (() => new AiUsageAdapter())();\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }