openmausbot 0.1.68 → 0.1.69

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.
@@ -12,6 +12,9 @@ function routineRequestOwnerKey(owner) {
12
12
  const ALL_DAYS = [0, 1, 2, 3, 4, 5, 6];
13
13
  const CATCH_UP_MS = 12 * 60 * 60_000;
14
14
  const MAX_DATE_MS = 8_640_000_000_000_000;
15
+ const LOCAL_DAY_MS = 24 * 60 * 60_000;
16
+ const INTERVAL_RESTRICTION_SEARCH_MS = 9 * LOCAL_DAY_MS;
17
+ const CLOCK_TIME = /^([01]\d|2[0-3]):([0-5]\d)$/;
15
18
  const MAX_RUNS = 2_000;
16
19
  const MAX_ATTACHMENTS = 50;
17
20
  const attachmentSchema = z.object({
@@ -86,7 +89,14 @@ function cloneSchedule(schedule) {
86
89
  if (schedule.type === "once")
87
90
  return { type: "once", at: schedule.at };
88
91
  if (schedule.type === "interval") {
89
- return { type: "interval", everyMinutes: schedule.everyMinutes, anchorAt: schedule.anchorAt };
92
+ return {
93
+ type: "interval",
94
+ everyMinutes: schedule.everyMinutes,
95
+ anchorAt: schedule.anchorAt,
96
+ ...(schedule.weekdays ? { weekdays: [...schedule.weekdays] } : {}),
97
+ ...(schedule.window ? { window: { ...schedule.window } } : {}),
98
+ ...(schedule.endsAt === undefined ? {} : { endsAt: schedule.endsAt }),
99
+ };
90
100
  }
91
101
  return { type: "daily", time: schedule.time, weekdays: [...schedule.weekdays] };
92
102
  }
@@ -164,6 +174,57 @@ function composeExecutionPrompt(prompt, attachments, carry) {
164
174
  }
165
175
  return parts.filter(Boolean).join("\n\n");
166
176
  }
177
+ function cleanIntervalWeekdays(value) {
178
+ if (value == null)
179
+ return undefined;
180
+ if (!Array.isArray(value) || value.length === 0 || value.length > ALL_DAYS.length) {
181
+ throw new Error("Choose at least one valid interval day");
182
+ }
183
+ if (value.some((day) => typeof day !== "number" || !Number.isInteger(day) || day < 0 || day > 6)) {
184
+ throw new Error("Interval days must be whole numbers from 0 to 6");
185
+ }
186
+ if (new Set(value).size !== value.length)
187
+ throw new Error("Choose each interval day only once");
188
+ const weekdays = [...value].sort((a, b) => a - b);
189
+ return weekdays.length === ALL_DAYS.length ? undefined : weekdays;
190
+ }
191
+ function clockMinutes(value) {
192
+ const match = CLOCK_TIME.exec(value);
193
+ return match ? Number(match[1]) * 60 + Number(match[2]) : null;
194
+ }
195
+ function cleanIntervalWindow(value, everyMinutes) {
196
+ if (value == null)
197
+ return undefined;
198
+ if (typeof value !== "object" || Array.isArray(value)) {
199
+ throw new Error("Choose a valid interval time window");
200
+ }
201
+ const candidate = value;
202
+ const start = typeof candidate.start === "string" ? candidate.start : "";
203
+ const end = typeof candidate.end === "string" ? candidate.end : "";
204
+ const startMinutes = clockMinutes(start);
205
+ const endMinutes = clockMinutes(end);
206
+ if (startMinutes == null || endMinutes == null) {
207
+ throw new Error("Interval window times must use HH:MM");
208
+ }
209
+ if (startMinutes >= endMinutes) {
210
+ throw new Error("Interval window must start before it ends on the same day");
211
+ }
212
+ if (endMinutes - startMinutes < everyMinutes) {
213
+ throw new Error("Interval window must be at least as long as the interval");
214
+ }
215
+ return { start, end };
216
+ }
217
+ function cleanIntervalEndsAt(value, anchorAt) {
218
+ if (value == null)
219
+ return undefined;
220
+ if (typeof value !== "number" ||
221
+ !Number.isSafeInteger(value) ||
222
+ value < anchorAt ||
223
+ value > MAX_DATE_MS) {
224
+ throw new Error("Choose a valid interval end time after its start");
225
+ }
226
+ return value;
227
+ }
167
228
  function finishedOrder(run) {
168
229
  return run.finishedAt ?? run.createdAt;
169
230
  }
@@ -191,7 +252,17 @@ function cleanSchedule(schedule) {
191
252
  anchorAt > MAX_DATE_MS) {
192
253
  throw new Error("Choose a valid interval start time");
193
254
  }
194
- return { type: "interval", everyMinutes, anchorAt };
255
+ const weekdays = cleanIntervalWeekdays(schedule.weekdays);
256
+ const window = cleanIntervalWindow(schedule.window, everyMinutes);
257
+ const endsAt = cleanIntervalEndsAt(schedule.endsAt, anchorAt);
258
+ return {
259
+ type: "interval",
260
+ everyMinutes,
261
+ anchorAt,
262
+ ...(weekdays ? { weekdays } : {}),
263
+ ...(window ? { window } : {}),
264
+ ...(endsAt === undefined ? {} : { endsAt }),
265
+ };
195
266
  }
196
267
  throw new Error("Choose a supported schedule");
197
268
  }
@@ -203,17 +274,56 @@ function loadSchedule(value) {
203
274
  return null;
204
275
  }
205
276
  }
277
+ function intervalHasRestrictions(schedule) {
278
+ return schedule.weekdays !== undefined || schedule.window !== undefined || schedule.endsAt !== undefined;
279
+ }
280
+ function intervalAllowsOccurrence(schedule, at) {
281
+ if (schedule.endsAt !== undefined && at > schedule.endsAt)
282
+ return false;
283
+ const date = new Date(at);
284
+ if (schedule.weekdays && !schedule.weekdays.includes(date.getDay()))
285
+ return false;
286
+ if (schedule.window) {
287
+ const minute = date.getHours() * 60 + date.getMinutes();
288
+ const start = clockMinutes(schedule.window.start);
289
+ const end = clockMinutes(schedule.window.end);
290
+ if (minute < start || minute >= end)
291
+ return false;
292
+ }
293
+ return true;
294
+ }
295
+ function isSameLocalDay(left, right) {
296
+ const a = new Date(left);
297
+ const b = new Date(right);
298
+ return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
299
+ }
300
+ function nextAlignedInterval(schedule, after) {
301
+ if (schedule.anchorAt > after)
302
+ return schedule.anchorAt;
303
+ const intervalMs = schedule.everyMinutes * 60_000;
304
+ const intervalsElapsed = Math.floor((after - schedule.anchorAt) / intervalMs);
305
+ const candidate = schedule.anchorAt + (intervalsElapsed + 1) * intervalMs;
306
+ return Number.isSafeInteger(candidate) && candidate <= MAX_DATE_MS ? candidate : null;
307
+ }
206
308
  /** Next wall-clock occurrence in this computer's timezone, strictly after `after`. */
207
309
  export function nextOccurrence(schedule, after) {
208
310
  if (schedule.type === "once")
209
311
  return schedule.at > after ? schedule.at : null;
210
312
  if (schedule.type === "interval") {
211
- if (schedule.anchorAt > after)
212
- return schedule.anchorAt;
213
313
  const intervalMs = schedule.everyMinutes * 60_000;
214
- const intervalsElapsed = Math.floor((after - schedule.anchorAt) / intervalMs);
215
- const candidate = schedule.anchorAt + (intervalsElapsed + 1) * intervalMs;
216
- return Number.isSafeInteger(candidate) && candidate <= MAX_DATE_MS ? candidate : null;
314
+ let candidate = nextAlignedInterval(schedule, after);
315
+ if (!intervalHasRestrictions(schedule))
316
+ return candidate;
317
+ const maxCandidates = Math.ceil(INTERVAL_RESTRICTION_SEARCH_MS / intervalMs) + 2;
318
+ for (let checked = 0; candidate !== null && checked < maxCandidates; checked++) {
319
+ if (schedule.endsAt !== undefined && candidate > schedule.endsAt)
320
+ return null;
321
+ if (intervalAllowsOccurrence(schedule, candidate))
322
+ return candidate;
323
+ const next = candidate + intervalMs;
324
+ candidate = Number.isSafeInteger(next) && next <= MAX_DATE_MS ? next : null;
325
+ }
326
+ return null;
217
327
  }
218
328
  const [hour, minute] = schedule.time.split(":").map(Number);
219
329
  const weekdays = new Set(cleanDays(schedule.weekdays));
@@ -227,10 +337,38 @@ export function nextOccurrence(schedule, after) {
227
337
  return null;
228
338
  }
229
339
  function latestIntervalOccurrence(schedule, at) {
230
- if (schedule.anchorAt > at)
231
- return null;
232
340
  const intervalMs = schedule.everyMinutes * 60_000;
233
- return schedule.anchorAt + Math.floor((at - schedule.anchorAt) / intervalMs) * intervalMs;
341
+ const ceiling = Math.min(at, schedule.endsAt ?? at);
342
+ if (schedule.anchorAt > ceiling)
343
+ return null;
344
+ let candidate = schedule.anchorAt + Math.floor((ceiling - schedule.anchorAt) / intervalMs) * intervalMs;
345
+ if (!intervalHasRestrictions(schedule))
346
+ return candidate;
347
+ const maxCandidates = Math.ceil(INTERVAL_RESTRICTION_SEARCH_MS / intervalMs) + 2;
348
+ for (let checked = 0; checked < maxCandidates; checked++) {
349
+ if (intervalAllowsOccurrence(schedule, candidate))
350
+ return candidate;
351
+ const previous = candidate - intervalMs;
352
+ if (!Number.isSafeInteger(previous) || previous < schedule.anchorAt)
353
+ return null;
354
+ candidate = previous;
355
+ }
356
+ return null;
357
+ }
358
+ function mergeScheduleUpdate(current, incoming) {
359
+ if (current.type !== "interval" || incoming.type !== "interval")
360
+ return incoming;
361
+ const merged = { ...incoming };
362
+ if (!Object.hasOwn(incoming, "weekdays") && current.weekdays) {
363
+ merged.weekdays = [...current.weekdays];
364
+ }
365
+ if (!Object.hasOwn(incoming, "window") && current.window) {
366
+ merged.window = { ...current.window };
367
+ }
368
+ if (!Object.hasOwn(incoming, "endsAt") && current.endsAt !== undefined) {
369
+ merged.endsAt = current.endsAt;
370
+ }
371
+ return merged;
234
372
  }
235
373
  function sanitizeInput(input) {
236
374
  const name = String(input.name ?? "").trim().slice(0, 80);
@@ -467,13 +605,17 @@ export class RoutineManager {
467
605
  if (this.targetState(clean) === "missing")
468
606
  throw new Error(this.missingTargetMessage(clean.target));
469
607
  const at = this.now();
608
+ const nextRunAt = clean.enabled ? this.initialOccurrence(clean.schedule, at) : null;
609
+ if (clean.schedule.type === "interval" && clean.enabled && nextRunAt === null) {
610
+ throw new Error("This interval has no future runs. Choose a later end date or turn it off.");
611
+ }
470
612
  const routine = {
471
613
  id: randomUUID(),
472
614
  ...clean,
473
615
  // Only a confirmed chat card supplies `request`; the public calendar
474
616
  // API cannot choose an arbitrary transcript as a reporting target.
475
617
  sourceThreadId: request?.threadId,
476
- nextRunAt: clean.enabled ? this.initialOccurrence(clean.schedule, at) : null,
618
+ nextRunAt,
477
619
  createdAt: at,
478
620
  updatedAt: at,
479
621
  };
@@ -505,7 +647,7 @@ export class RoutineManager {
505
647
  groupId: Object.hasOwn(patch, "groupId") ? patch.groupId : routine.groupId,
506
648
  runOn: patch.runOn ?? routine.runOn,
507
649
  enabled: patch.enabled ?? routine.enabled,
508
- schedule: patch.schedule ?? routine.schedule,
650
+ schedule: patch.schedule ? mergeScheduleUpdate(routine.schedule, patch.schedule) : routine.schedule,
509
651
  durationMinutes: patch.durationMinutes ?? routine.durationMinutes,
510
652
  timeoutMinutes: Object.hasOwn(patch, "timeoutMinutes") ? patch.timeoutMinutes : routine.timeoutMinutes,
511
653
  attachments: patch.attachments ?? routine.attachments,
@@ -513,16 +655,19 @@ export class RoutineManager {
513
655
  });
514
656
  if (this.targetState(clean) === "missing")
515
657
  throw new Error(this.missingTargetMessage(clean.target));
516
- const cancelledRuns = [];
517
658
  const scheduleChanged = JSON.stringify(clean.schedule) !== JSON.stringify(routine.schedule);
518
659
  const enabledChanged = clean.enabled !== routine.enabled;
660
+ // Definition-only edits retain due work and offline catch-up.
661
+ const nextRunAt = !clean.enabled ? null : scheduleChanged || enabledChanged
662
+ ? this.initialOccurrence(clean.schedule, now)
663
+ : routine.nextRunAt;
664
+ if (clean.schedule.type === "interval" && clean.enabled && nextRunAt === null) {
665
+ throw new Error("This interval has no future runs. Choose a later end date or turn it off.");
666
+ }
667
+ const cancelledRuns = [];
519
668
  this.commitMutation(() => {
520
669
  Object.assign(routine, clean, {
521
- // Renaming/editing instructions must not skip an occurrence that
522
- // became due since the last tick (or erase an offline catch-up).
523
- nextRunAt: !clean.enabled ? null : scheduleChanged || enabledChanged
524
- ? this.initialOccurrence(clean.schedule, now)
525
- : routine.nextRunAt,
670
+ nextRunAt,
526
671
  // `updatedAt` doubles as the optimistic revision on durable routine
527
672
  // confirmation cards. Keep it monotonic even for two writes in one ms.
528
673
  updatedAt: Math.max(now, routine.updatedAt + 1),
@@ -871,6 +1016,10 @@ export class RoutineManager {
871
1016
  routine.enabled = false;
872
1017
  routine.updatedAt = Math.max(now, routine.updatedAt + 1);
873
1018
  }
1019
+ else if (routine.schedule.type === "interval" && routine.nextRunAt === null) {
1020
+ routine.enabled = false;
1021
+ routine.updatedAt = Math.max(now, routine.updatedAt + 1);
1022
+ }
874
1023
  this.emitRoutine(routine);
875
1024
  changed = true;
876
1025
  }
@@ -900,6 +1049,15 @@ export class RoutineManager {
900
1049
  this.save();
901
1050
  this.emitRun(run);
902
1051
  }
1052
+ const requiresCurrentDayOccurrence = definition.schedule.weekdays !== undefined || definition.schedule.window !== undefined;
1053
+ const mayDispatch = intervalAllowsOccurrence(definition.schedule, now) &&
1054
+ (!requiresCurrentDayOccurrence || (latest !== null && isSameLocalDay(latest, now)));
1055
+ if (!mayDispatch) {
1056
+ if (nextOccurrence(definition.schedule, now) === null) {
1057
+ this.missQueuedRun(run, "The routine ended before this scheduled run could start");
1058
+ }
1059
+ continue;
1060
+ }
903
1061
  }
904
1062
  const state = this.targetState(run);
905
1063
  if (state === "busy")
@@ -1071,6 +1229,15 @@ export class RoutineManager {
1071
1229
  this.emitRun(run);
1072
1230
  this.options.onRunFailed?.(cloneRun(run));
1073
1231
  }
1232
+ missQueuedRun(run, message) {
1233
+ run.status = "missed";
1234
+ run.attention = undefined;
1235
+ run.error = redactSecretsInText(message).slice(0, 500);
1236
+ run.finishedAt = this.now();
1237
+ this.save();
1238
+ this.emitRun(run);
1239
+ this.options.onRunFailed?.(cloneRun(run));
1240
+ }
1074
1241
  targetState(target) {
1075
1242
  if (target.target === "room-goal") {
1076
1243
  if (!target.groupId || !this.options.goalState)
@@ -18,6 +18,30 @@
18
18
  // the feature.
19
19
  import { newId } from "./contracts.js";
20
20
  const queues = new Map(); // threadId → waiting sends
21
+ const listeners = new Set();
22
+ const changed = () => {
23
+ for (const listener of listeners) {
24
+ try {
25
+ listener();
26
+ }
27
+ catch {
28
+ console.warn("steer-queue: change listener failed");
29
+ }
30
+ }
31
+ };
32
+ /** Public pending chips only: never expose provider prompts or reply context. */
33
+ export function queuedSteerSnapshot(ownsThread) {
34
+ return Object.fromEntries([...queues]
35
+ .filter(([threadId, entry]) => ownsThread(entry.botId, threadId))
36
+ .map(([threadId, entry]) => [threadId, entry.items.map((item) => ({
37
+ queueId: item.messageId, text: item.text, ...(item.reason ? { reason: item.reason } : {}),
38
+ }))]));
39
+ }
40
+ /** Publish changes synchronously so every client can restore/cancel the queue. */
41
+ export function onSteeredQueueChange(listener) {
42
+ listeners.add(listener);
43
+ return () => { listeners.delete(listener); };
44
+ }
21
45
  /** Hold a mid-turn send off the transcript until drain. */
22
46
  export function queueSteeredMessage(botId, threadId, text, options = {}) {
23
47
  const id = newId();
@@ -35,6 +59,7 @@ export function queueSteeredMessage(botId, threadId, text, options = {}) {
35
59
  reason: options.reason,
36
60
  });
37
61
  queues.set(threadId, entry);
62
+ changed();
38
63
  return { id };
39
64
  }
40
65
  /** Drain every queue whose task is idle: append the held lines (leaf is now
@@ -53,6 +78,7 @@ export function drainSteeredMessages(store, run, isBlocked) {
53
78
  if (!bot) {
54
79
  // the bot or task was deleted while messages waited
55
80
  queues.delete(threadId);
81
+ changed();
56
82
  continue;
57
83
  }
58
84
  if (bot.busy || isBlocked?.(entry.botId, threadId))
@@ -60,6 +86,7 @@ export function drainSteeredMessages(store, run, isBlocked) {
60
86
  // committed to draining: the entry leaves the map before anything runs,
61
87
  // so a settle racing another settle can never fire the same queue twice
62
88
  queues.delete(threadId);
89
+ changed();
63
90
  const appended = [];
64
91
  for (const item of entry.items) {
65
92
  // queueId is the pending-chip identity from the 202; append still
@@ -107,6 +134,7 @@ export function cancelSteeredMessage(botId, messageId, expectedThreadId) {
107
134
  queues.delete(threadId);
108
135
  else
109
136
  queues.set(threadId, { botId: entry.botId, items });
137
+ changed();
110
138
  return true;
111
139
  }
112
140
  return false;
@@ -0,0 +1,28 @@
1
+ /** A deliberately configured Images API endpoint, not a provider-returned URL.
2
+ * Local HTTP is useful for self-hosted routers; internet endpoints require TLS. */
3
+ export function normalizeImageGenerationUrl(value) {
4
+ const message = "Use an HTTPS API base URL, or HTTP for a local router, without passwords, query parameters or fragments";
5
+ let url;
6
+ try {
7
+ url = new URL(value.trim());
8
+ }
9
+ catch {
10
+ throw new Error(message);
11
+ }
12
+ if (value.length > 2048 || url.username || url.password || url.search || url.hash)
13
+ throw new Error(message);
14
+ const host = url.hostname.toLowerCase();
15
+ const ipv4 = /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/.exec(host);
16
+ const local = host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local")
17
+ || host === "host.docker.internal" || host === "host.containers.internal"
18
+ || host === "[::1]" || /^\[f[cd][0-9a-f]{2}:/.test(host)
19
+ || (ipv4 && (Number(ipv4[1]) === 127 || Number(ipv4[1]) === 10
20
+ || (Number(ipv4[1]) === 192 && Number(ipv4[2]) === 168)
21
+ || (Number(ipv4[1]) === 172 && Number(ipv4[2]) >= 16 && Number(ipv4[2]) <= 31)));
22
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && local))
23
+ throw new Error(message);
24
+ if (ipv4 && (Number(ipv4[1]) === 0 || (Number(ipv4[1]) === 169 && Number(ipv4[2]) === 254)))
25
+ throw new Error(message);
26
+ url.pathname = url.pathname.replace(/\/+$/, "").replace(/\/images\/generations$/, "");
27
+ return url.toString().replace(/\/+$/, "");
28
+ }
@@ -14933,6 +14933,25 @@ function date4(params) {
14933
14933
  // node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.js
14934
14934
  config(en_default());
14935
14935
 
14936
+ // shared/image-generation.ts
14937
+ function normalizeImageGenerationUrl(value) {
14938
+ const message = "Use an HTTPS API base URL, or HTTP for a local router, without passwords, query parameters or fragments";
14939
+ let url2;
14940
+ try {
14941
+ url2 = new URL(value.trim());
14942
+ } catch {
14943
+ throw new Error(message);
14944
+ }
14945
+ if (value.length > 2048 || url2.username || url2.password || url2.search || url2.hash) throw new Error(message);
14946
+ const host = url2.hostname.toLowerCase();
14947
+ const ipv43 = /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/.exec(host);
14948
+ const local = host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local") || host === "host.docker.internal" || host === "host.containers.internal" || host === "[::1]" || /^\[f[cd][0-9a-f]{2}:/.test(host) || ipv43 && (Number(ipv43[1]) === 127 || Number(ipv43[1]) === 10 || Number(ipv43[1]) === 192 && Number(ipv43[2]) === 168 || Number(ipv43[1]) === 172 && Number(ipv43[2]) >= 16 && Number(ipv43[2]) <= 31);
14949
+ if (url2.protocol !== "https:" && !(url2.protocol === "http:" && local)) throw new Error(message);
14950
+ if (ipv43 && (Number(ipv43[1]) === 0 || Number(ipv43[1]) === 169 && Number(ipv43[2]) === 254)) throw new Error(message);
14951
+ url2.pathname = url2.pathname.replace(/\/+$/, "").replace(/\/images\/generations$/, "");
14952
+ return url2.toString().replace(/\/+$/, "");
14953
+ }
14954
+
14936
14955
  // server/contracts.ts
14937
14956
  var EFFORT_LEVELS = ["none", "low", "medium", "high", "xhigh", "max"];
14938
14957
 
@@ -15133,8 +15152,25 @@ var appConfigSchema = external_exports.object({
15133
15152
  * engine: "elevenlabs" (default; needs a key) or "system" (the Mac's
15134
15153
  * built-in voices, no key). */
15135
15154
  tts: external_exports.object({ key: optionalText, voice: optionalText, provider: external_exports.enum(["elevenlabs", "system"]).optional() }).optional(),
15136
- /** OpenAI key used only by the in-process avatar image generator. */
15137
- imageGen: external_exports.object({ key: optionalText }).optional(),
15155
+ /** Avatar provider credentials stay separate; choosing a router never reuses a cloud key. */
15156
+ imageGen: external_exports.object({
15157
+ provider: external_exports.enum(["openai", "xai", "custom"]).optional(),
15158
+ key: optionalText,
15159
+ customApiKey: optionalText,
15160
+ customUrl: external_exports.string().trim().max(2048).transform((value, ctx) => {
15161
+ if (!value) return "";
15162
+ try {
15163
+ return normalizeImageGenerationUrl(value);
15164
+ } catch (error51) {
15165
+ ctx.addIssue({ code: "custom", message: error51.message });
15166
+ return external_exports.NEVER;
15167
+ }
15168
+ }).optional(),
15169
+ customModel: external_exports.string().trim().max(200).refine(
15170
+ (value) => !["\r", "\n", "\0"].some((character) => value.includes(character)),
15171
+ "Use a model ID without control characters"
15172
+ ).optional()
15173
+ }).optional(),
15138
15174
  /** Non-secret profile details shown in the sidebar. */
15139
15175
  profile: external_exports.object({ name: optionalText, email: optionalText }).optional(),
15140
15176
  /** UI language override (BCP-47, lowercase). Empty/absent = follow the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openmausbot",
3
- "version": "0.1.68",
3
+ "version": "0.1.69",
4
4
  "description": "Run the OpenMausBot server anywhere and pair your devices to it",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",