@amoshydra/oktoso 0.1.6 → 0.1.7

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.
package/dist/hub.js CHANGED
@@ -8,7 +8,7 @@ var __export = (target, all) => {
8
8
  // packages/hub/src/main.ts
9
9
  import { spawn } from "child_process";
10
10
  import { existsSync as existsSync4 } from "fs";
11
- import { dirname as dirname4, join as join4, resolve as resolve2 } from "path";
11
+ import { dirname as dirname5, join as join4, resolve as resolve3 } from "path";
12
12
  import { fileURLToPath } from "url";
13
13
 
14
14
  // node_modules/.pnpm/@hono+node-server@2.1.5_hono@4.13.14/node_modules/@hono/node-server/dist/constants-BLSFu_RU.mjs
@@ -429,7 +429,7 @@ var readBodyDirect = (request) => {
429
429
  request[bodyBufferKey] = buffered;
430
430
  return Promise.resolve(buffered);
431
431
  }
432
- const promise2 = new Promise((resolve3, reject) => {
432
+ const promise2 = new Promise((resolve4, reject) => {
433
433
  const chunks = [];
434
434
  let settled = false;
435
435
  const finish = (callback) => {
@@ -447,7 +447,7 @@ var readBodyDirect = (request) => {
447
447
  else if (recovered === void 0) reject(error62 ?? normalizeAbortError(request, incoming));
448
448
  else {
449
449
  request[bodyBufferKey] = recovered;
450
- resolve3(recovered);
450
+ resolve4(recovered);
451
451
  }
452
452
  });
453
453
  return true;
@@ -459,7 +459,7 @@ var readBodyDirect = (request) => {
459
459
  finish(() => {
460
460
  const buffer = chunks.length === 1 ? chunks[0] : Buffer.concat(chunks);
461
461
  request[bodyBufferKey] = buffer;
462
- resolve3(buffer);
462
+ resolve4(buffer);
463
463
  });
464
464
  };
465
465
  const onError = (error62) => {
@@ -1006,7 +1006,7 @@ var responseViaResponseObject = async (res, outgoing, options = {}) => {
1006
1006
  });
1007
1007
  if (!chunk) {
1008
1008
  if (i === 1) {
1009
- await new Promise((resolve3) => setTimeout(resolve3));
1009
+ await new Promise((resolve4) => setTimeout(resolve4));
1010
1010
  maxReadCount = 3;
1011
1011
  continue;
1012
1012
  }
@@ -1192,9 +1192,9 @@ var setupWebSocket = (options) => {
1192
1192
  }
1193
1193
  };
1194
1194
  const waitForWebSocket = (request, connectionSymbol) => {
1195
- return new Promise((resolve3, reject) => {
1195
+ return new Promise((resolve4, reject) => {
1196
1196
  waiterMap.set(request, {
1197
- resolve: resolve3,
1197
+ resolve: resolve4,
1198
1198
  reject,
1199
1199
  connectionSymbol
1200
1200
  });
@@ -6411,7 +6411,7 @@ var recursive = /* @__PURE__ */ new WeakMap();
6411
6411
  var NONE = 0;
6412
6412
  var ASSUMED = 1;
6413
6413
  var PROVEN = 2;
6414
- function isRecursive(inst, stack, resolve3) {
6414
+ function isRecursive(inst, stack, resolve4) {
6415
6415
  const cached2 = recursive.get(inst);
6416
6416
  if (cached2 !== void 0)
6417
6417
  return cached2 ? PROVEN : NONE;
@@ -6421,7 +6421,7 @@ function isRecursive(inst, stack, resolve3) {
6421
6421
  let result = NONE;
6422
6422
  const check2 = (child) => {
6423
6423
  if (result !== PROVEN && child?._zod) {
6424
- const answer = isRecursive(child, stack, resolve3);
6424
+ const answer = isRecursive(child, stack, resolve4);
6425
6425
  if (answer > result)
6426
6426
  result = answer;
6427
6427
  }
@@ -6432,7 +6432,7 @@ function isRecursive(inst, stack, resolve3) {
6432
6432
  const desc = Object.getOwnPropertyDescriptor(sh, key);
6433
6433
  if (spread && !desc.enumerable)
6434
6434
  continue;
6435
- const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve3) : NONE;
6435
+ const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve4) : NONE;
6436
6436
  if (child > answer)
6437
6437
  answer = child;
6438
6438
  }
@@ -6496,7 +6496,7 @@ function isRecursive(inst, stack, resolve3) {
6496
6496
  break;
6497
6497
  // `$ZodLazy` caches its inner on the def, so a resolved edge is followed exactly
6498
6498
  case "lazy": {
6499
- const inner = def._cachedInner ?? (resolve3 ? inst._zod.innerType : void 0);
6499
+ const inner = def._cachedInner ?? (resolve4 ? inst._zod.innerType : void 0);
6500
6500
  merge2(inner ? isRecursive(inner, stack, false) : ASSUMED);
6501
6501
  break;
6502
6502
  }
@@ -21078,6 +21078,51 @@ var zError = external_exports.object({
21078
21078
  })
21079
21079
  });
21080
21080
  var zProviderStatus = external_exports.enum(["online", "offline", "auth-error", "unknown"]);
21081
+ var zProviderAuthKind = external_exports.enum(["basic", "bearer", "none"]);
21082
+ var zProviderIDInput = external_exports.string().min(1).max(64).regex(
21083
+ /^[a-zA-Z0-9._-]+$/,
21084
+ "Provider IDs may contain letters, numbers, dot, underscore, or dash"
21085
+ );
21086
+ var zProviderTypeInput = external_exports.enum(["opencode", "openwebui"]);
21087
+ var zProviderAuthInput = external_exports.discriminatedUnion("kind", [
21088
+ external_exports.object({
21089
+ kind: external_exports.literal("basic"),
21090
+ username: external_exports.string().min(1).default("opencode"),
21091
+ password: external_exports.string().min(1)
21092
+ }),
21093
+ external_exports.object({
21094
+ kind: external_exports.literal("bearer"),
21095
+ token: external_exports.string().min(1)
21096
+ }),
21097
+ external_exports.object({ kind: external_exports.literal("none") })
21098
+ ]);
21099
+ var zProviderAuthUpdate = external_exports.discriminatedUnion("kind", [
21100
+ external_exports.object({
21101
+ kind: external_exports.literal("basic"),
21102
+ username: external_exports.string().min(1).default("opencode"),
21103
+ password: external_exports.string().min(1).optional()
21104
+ }),
21105
+ external_exports.object({
21106
+ kind: external_exports.literal("bearer"),
21107
+ token: external_exports.string().min(1).optional()
21108
+ }),
21109
+ external_exports.object({ kind: external_exports.literal("none") })
21110
+ ]);
21111
+ var zProviderInput = external_exports.object({
21112
+ id: zProviderIDInput,
21113
+ type: zProviderTypeInput,
21114
+ name: external_exports.string().min(1).max(80).optional(),
21115
+ baseUrl: external_exports.string().url(),
21116
+ auth: zProviderAuthInput.optional(),
21117
+ enabled: external_exports.boolean().default(true)
21118
+ });
21119
+ var zProviderUpdateInput = external_exports.object({
21120
+ type: zProviderTypeInput,
21121
+ name: external_exports.string().min(1).max(80).optional(),
21122
+ baseUrl: external_exports.string().url(),
21123
+ auth: zProviderAuthUpdate.optional(),
21124
+ enabled: external_exports.boolean().default(true)
21125
+ });
21081
21126
  var zProviderCapabilities = external_exports.object({
21082
21127
  list: external_exports.boolean(),
21083
21128
  read: external_exports.boolean(),
@@ -21098,6 +21143,8 @@ var zProvider = external_exports.object({
21098
21143
  name: external_exports.string(),
21099
21144
  baseUrl: external_exports.string(),
21100
21145
  enabled: external_exports.boolean(),
21146
+ authKind: zProviderAuthKind.optional(),
21147
+ authUsername: external_exports.string().optional(),
21101
21148
  status: zProviderStatus,
21102
21149
  version: external_exports.string().optional(),
21103
21150
  lastCheckedAt: external_exports.number().int().optional(),
@@ -21935,14 +21982,14 @@ async function probeHub(config2) {
21935
21982
  }
21936
21983
  function isPortOpen(config2) {
21937
21984
  const host = isWildcardHost(config2.host) ? "127.0.0.1" : formatHost(config2.host).replace(/^\[|\]$/g, "");
21938
- return new Promise((resolve3) => {
21985
+ return new Promise((resolve4) => {
21939
21986
  const socket = connect({ host, port: config2.port });
21940
21987
  let settled = false;
21941
21988
  const done = (open3) => {
21942
21989
  if (settled) return;
21943
21990
  settled = true;
21944
21991
  socket.destroy();
21945
- resolve3(open3);
21992
+ resolve4(open3);
21946
21993
  };
21947
21994
  socket.setTimeout(400);
21948
21995
  socket.once("connect", () => done(true));
@@ -22032,10 +22079,10 @@ var RotatingFileSink = class {
22032
22079
  async close() {
22033
22080
  this.closed = true;
22034
22081
  while (this.running || this.queue.length > 0) {
22035
- await new Promise((resolve3) => setTimeout(resolve3, 5));
22082
+ await new Promise((resolve4) => setTimeout(resolve4, 5));
22036
22083
  }
22037
22084
  if (this.stream) {
22038
- await new Promise((resolve3) => this.stream?.end(resolve3));
22085
+ await new Promise((resolve4) => this.stream?.end(resolve4));
22039
22086
  this.stream = void 0;
22040
22087
  }
22041
22088
  }
@@ -22088,8 +22135,8 @@ var RotatingFileSink = class {
22088
22135
  flags: "a",
22089
22136
  mode: 384
22090
22137
  });
22091
- await new Promise((resolve3, reject) => {
22092
- this.stream?.once("open", () => resolve3());
22138
+ await new Promise((resolve4, reject) => {
22139
+ this.stream?.once("open", () => resolve4());
22093
22140
  this.stream?.once("error", reject);
22094
22141
  });
22095
22142
  await chmod(this.filePath, 384).catch(() => void 0);
@@ -22099,29 +22146,29 @@ var RotatingFileSink = class {
22099
22146
  if (!this.stream) return;
22100
22147
  const stream2 = this.stream;
22101
22148
  this.stream = void 0;
22102
- await new Promise((resolve3) => stream2.end(resolve3));
22149
+ await new Promise((resolve4) => stream2.end(resolve4));
22103
22150
  await rm(this.previousPath, { force: true });
22104
22151
  await rename2(this.filePath, this.previousPath);
22105
22152
  this.stream = createWriteStream(this.filePath, {
22106
22153
  flags: "a",
22107
22154
  mode: 384
22108
22155
  });
22109
- await new Promise((resolve3, reject) => {
22110
- this.stream?.once("open", () => resolve3());
22156
+ await new Promise((resolve4, reject) => {
22157
+ this.stream?.once("open", () => resolve4());
22111
22158
  this.stream?.once("error", reject);
22112
22159
  });
22113
22160
  await chmod(this.filePath, 384).catch(() => void 0);
22114
22161
  this.size = 0;
22115
22162
  }
22116
22163
  writeLine(line) {
22117
- return new Promise((resolve3, reject) => {
22164
+ return new Promise((resolve4, reject) => {
22118
22165
  if (!this.stream) {
22119
22166
  reject(new Error("Log stream is not open"));
22120
22167
  return;
22121
22168
  }
22122
22169
  this.stream.write(line, (error62) => {
22123
22170
  if (error62) reject(error62);
22124
- else resolve3();
22171
+ else resolve4();
22125
22172
  });
22126
22173
  });
22127
22174
  }
@@ -24968,17 +25015,17 @@ function findSessionID(value, depth = 0) {
24968
25015
  return void 0;
24969
25016
  }
24970
25017
  async function delay(ms, signal) {
24971
- await new Promise((resolve3) => {
25018
+ await new Promise((resolve4) => {
24972
25019
  if (signal.aborted) {
24973
- resolve3();
25020
+ resolve4();
24974
25021
  return;
24975
25022
  }
24976
- const timer = setTimeout(resolve3, ms);
25023
+ const timer = setTimeout(resolve4, ms);
24977
25024
  signal.addEventListener(
24978
25025
  "abort",
24979
25026
  () => {
24980
25027
  clearTimeout(timer);
24981
- resolve3();
25028
+ resolve4();
24982
25029
  },
24983
25030
  { once: true }
24984
25031
  );
@@ -25592,17 +25639,17 @@ function decodeCursor(cursor) {
25592
25639
  }
25593
25640
  }
25594
25641
  async function delay2(ms, signal) {
25595
- await new Promise((resolve3) => {
25642
+ await new Promise((resolve4) => {
25596
25643
  if (signal.aborted) {
25597
- resolve3();
25644
+ resolve4();
25598
25645
  return;
25599
25646
  }
25600
- const timer = setTimeout(resolve3, ms);
25647
+ const timer = setTimeout(resolve4, ms);
25601
25648
  signal.addEventListener(
25602
25649
  "abort",
25603
25650
  () => {
25604
25651
  clearTimeout(timer);
25605
- resolve3();
25652
+ resolve4();
25606
25653
  },
25607
25654
  { once: true }
25608
25655
  );
@@ -25619,28 +25666,46 @@ var ProviderRegistry = class {
25619
25666
  constructor(providers, logger) {
25620
25667
  this.logger = logger;
25621
25668
  for (const config2 of providers) {
25622
- this.runtimes.set(config2.id, {
25623
- config: config2,
25624
- status: "unknown",
25625
- ...config2.type === "opencode" ? {
25626
- adapter: new OpenCodeAdapter({
25627
- id: config2.id,
25628
- baseUrl: config2.baseUrl,
25629
- username: config2.auth?.kind === "basic" ? config2.auth.username : void 0,
25630
- password: config2.auth?.kind === "basic" ? config2.auth.password : ""
25631
- })
25632
- } : config2.type === "openwebui" ? {
25633
- adapter: new OpenWebUIAdapter({
25634
- id: config2.id,
25635
- baseUrl: config2.baseUrl,
25636
- token: config2.auth?.kind === "bearer" ? config2.auth.token : ""
25637
- })
25638
- } : {}
25639
- });
25669
+ this.runtimes.set(config2.id, this.createRuntime(config2));
25640
25670
  }
25641
25671
  }
25642
25672
  logger;
25643
25673
  runtimes = /* @__PURE__ */ new Map();
25674
+ add(config2) {
25675
+ if (this.runtimes.has(config2.id)) {
25676
+ throw new Error(`Provider ${config2.id} is already configured`);
25677
+ }
25678
+ this.runtimes.set(config2.id, this.createRuntime(config2));
25679
+ }
25680
+ remove(id) {
25681
+ this.runtimes.delete(id);
25682
+ }
25683
+ replace(config2) {
25684
+ if (!this.runtimes.has(config2.id)) {
25685
+ throw new Error(`Provider ${config2.id} is not configured`);
25686
+ }
25687
+ this.runtimes.set(config2.id, this.createRuntime(config2));
25688
+ }
25689
+ createRuntime(config2) {
25690
+ return {
25691
+ config: config2,
25692
+ status: "unknown",
25693
+ ...config2.type === "opencode" ? {
25694
+ adapter: new OpenCodeAdapter({
25695
+ id: config2.id,
25696
+ baseUrl: config2.baseUrl,
25697
+ username: config2.auth?.kind === "basic" ? config2.auth.username : void 0,
25698
+ password: config2.auth?.kind === "basic" ? config2.auth.password : ""
25699
+ })
25700
+ } : config2.type === "openwebui" ? {
25701
+ adapter: new OpenWebUIAdapter({
25702
+ id: config2.id,
25703
+ baseUrl: config2.baseUrl,
25704
+ token: config2.auth?.kind === "bearer" ? config2.auth.token : ""
25705
+ })
25706
+ } : {}
25707
+ };
25708
+ }
25644
25709
  adapters() {
25645
25710
  return [...this.runtimes.values()].map((runtime) => runtime.adapter).filter((adapter) => Boolean(adapter));
25646
25711
  }
@@ -25743,6 +25808,8 @@ var ProviderRegistry = class {
25743
25808
  name: runtime.config.name ?? runtime.config.id,
25744
25809
  baseUrl: runtime.config.baseUrl,
25745
25810
  enabled: runtime.config.enabled,
25811
+ authKind: runtime.config.auth?.kind ?? "none",
25812
+ ...runtime.config.auth?.kind === "basic" ? { authUsername: runtime.config.auth.username } : {},
25746
25813
  status: runtime.status,
25747
25814
  ...runtime.version ? { version: runtime.version } : {},
25748
25815
  ...runtime.lastCheckedAt ? { lastCheckedAt: runtime.lastCheckedAt } : {},
@@ -25751,6 +25818,10 @@ var ProviderRegistry = class {
25751
25818
  }
25752
25819
  };
25753
25820
 
25821
+ // packages/hub/src/services/provider-admin.ts
25822
+ import { mkdir as mkdir3, readFile as readFile3, rename as rename3, writeFile as writeFile2 } from "fs/promises";
25823
+ import { dirname as dirname4, resolve } from "path";
25824
+
25754
25825
  // packages/hub/src/domain/errors.ts
25755
25826
  var AppError = class extends Error {
25756
25827
  code;
@@ -25782,6 +25853,157 @@ function conflict(message, details) {
25782
25853
  return new AppError("conflict", message, 409, { details });
25783
25854
  }
25784
25855
 
25856
+ // packages/hub/src/services/provider-admin.ts
25857
+ var ProviderAdminService = class {
25858
+ constructor(registry2, sessions, attention, providerEvents, logger, configPath) {
25859
+ this.registry = registry2;
25860
+ this.sessions = sessions;
25861
+ this.attention = attention;
25862
+ this.providerEvents = providerEvents;
25863
+ this.logger = logger;
25864
+ this.configPath = resolve(configPath);
25865
+ }
25866
+ registry;
25867
+ sessions;
25868
+ attention;
25869
+ providerEvents;
25870
+ logger;
25871
+ configPath;
25872
+ queue = Promise.resolve();
25873
+ add(input2) {
25874
+ return this.serialize(async () => {
25875
+ const raw2 = await this.read();
25876
+ const providers = raw2.providers ?? [];
25877
+ if (providers.some((provider) => provider.id === input2.id)) {
25878
+ throw conflict(`Provider ${input2.id} is already configured`);
25879
+ }
25880
+ const config2 = this.toConfig(input2);
25881
+ await this.write({
25882
+ ...raw2,
25883
+ providers: [...providers, config2]
25884
+ });
25885
+ this.registry.add(config2);
25886
+ this.invalidate();
25887
+ const adapter = this.registry.adapter(config2.id);
25888
+ if (adapter) void this.providerEvents?.subscribe(adapter);
25889
+ const saved = await this.check(config2.id);
25890
+ this.logger.info("provider added", {
25891
+ providerID: config2.id,
25892
+ status: saved.status
25893
+ });
25894
+ return saved;
25895
+ });
25896
+ }
25897
+ update(id, input2) {
25898
+ return this.serialize(async () => {
25899
+ const raw2 = await this.read();
25900
+ const providers = raw2.providers ?? [];
25901
+ const index = providers.findIndex((provider) => provider.id === id);
25902
+ if (index < 0) throw notFound(`Provider ${id} is not configured`);
25903
+ const current = providers[index];
25904
+ const auth = input2.auth ? this.mergeAuth(current.auth, input2.auth) : current.auth;
25905
+ this.assertTypeAuth(input2.type, auth);
25906
+ const config2 = zProviderConfig.parse({
25907
+ ...current,
25908
+ type: input2.type,
25909
+ ...input2.name ? { name: input2.name } : {},
25910
+ baseUrl: input2.baseUrl,
25911
+ enabled: input2.enabled,
25912
+ ...auth ? { auth } : {}
25913
+ });
25914
+ const next = [...providers];
25915
+ next[index] = config2;
25916
+ await this.write({ ...raw2, providers: next });
25917
+ this.registry.replace(config2);
25918
+ this.invalidate();
25919
+ const adapter = this.registry.adapter(config2.id);
25920
+ if (adapter) void this.providerEvents?.subscribe(adapter);
25921
+ const saved = await this.check(config2.id);
25922
+ this.logger.info("provider updated", {
25923
+ providerID: config2.id,
25924
+ status: saved.status
25925
+ });
25926
+ return saved;
25927
+ });
25928
+ }
25929
+ toConfig(input2) {
25930
+ this.assertTypeAuth(input2.type, input2.auth);
25931
+ return zProviderConfig.parse({
25932
+ id: input2.id,
25933
+ type: input2.type,
25934
+ ...input2.name ? { name: input2.name } : {},
25935
+ baseUrl: input2.baseUrl,
25936
+ ...input2.auth ? { auth: input2.auth } : {},
25937
+ enabled: input2.enabled
25938
+ });
25939
+ }
25940
+ assertTypeAuth(type, auth) {
25941
+ if (!auth || auth.kind === "none") return;
25942
+ if (type === "openwebui" && auth.kind !== "bearer") {
25943
+ throw badRequest("Open WebUI requires bearer authentication");
25944
+ }
25945
+ if (type === "opencode" && auth.kind !== "basic") {
25946
+ throw badRequest("OpenCode requires basic authentication");
25947
+ }
25948
+ }
25949
+ mergeAuth(current, input2) {
25950
+ if (input2.kind === "none") return { kind: "none" };
25951
+ if (input2.kind === "basic") {
25952
+ const password = input2.password ?? (current?.kind === "basic" ? current.password : void 0);
25953
+ if (!password) {
25954
+ throw badRequest("A password is required for basic authentication");
25955
+ }
25956
+ return { kind: "basic", username: input2.username, password };
25957
+ }
25958
+ const token = input2.token ?? (current?.kind === "bearer" ? current.token : void 0);
25959
+ if (!token) {
25960
+ throw badRequest("A token is required for bearer authentication");
25961
+ }
25962
+ return { kind: "bearer", token };
25963
+ }
25964
+ invalidate() {
25965
+ this.sessions.invalidate();
25966
+ this.attention.invalidate();
25967
+ }
25968
+ async check(id) {
25969
+ try {
25970
+ await this.registry.check(id);
25971
+ } catch (error62) {
25972
+ this.logger.warn("provider health check failed", {
25973
+ providerID: id,
25974
+ error: error62 instanceof Error ? error62.message : String(error62)
25975
+ });
25976
+ }
25977
+ const provider = this.registry.get(id);
25978
+ if (!provider) throw notFound(`Provider ${id} is not configured`);
25979
+ return provider;
25980
+ }
25981
+ async read() {
25982
+ const raw2 = JSON.parse(await readFile3(this.configPath, "utf8"));
25983
+ if (!raw2 || typeof raw2 !== "object" || Array.isArray(raw2)) {
25984
+ throw badRequest("Hub config must be a JSON object");
25985
+ }
25986
+ return raw2;
25987
+ }
25988
+ async write(config2) {
25989
+ const content = `${JSON.stringify(config2, null, 2)}
25990
+ `;
25991
+ const directory = dirname4(this.configPath);
25992
+ await mkdir3(directory, { recursive: true });
25993
+ const tempPath = `${this.configPath}.${process.pid}.${Date.now()}.tmp`;
25994
+ await writeFile2(tempPath, content, { mode: 384 });
25995
+ await rename3(tempPath, this.configPath);
25996
+ }
25997
+ serialize(action) {
25998
+ const run2 = this.queue.then(action, action);
25999
+ this.queue = run2.then(
26000
+ () => void 0,
26001
+ () => void 0
26002
+ );
26003
+ return run2;
26004
+ }
26005
+ };
26006
+
25785
26007
  // packages/hub/src/services/session-service.ts
25786
26008
  var DEFAULT_LIMIT = 50;
25787
26009
  var MAX_LIMIT = 200;
@@ -26042,7 +26264,7 @@ var ProviderEventService = class {
26042
26264
  coalesceMs;
26043
26265
  messageCoalesceMs;
26044
26266
  controller = new AbortController();
26045
- unsubscribes = [];
26267
+ subscriptions = /* @__PURE__ */ new Map();
26046
26268
  timers = /* @__PURE__ */ new Map();
26047
26269
  messageTimers = /* @__PURE__ */ new Map();
26048
26270
  pendingMessages = /* @__PURE__ */ new Map();
@@ -26051,29 +26273,43 @@ var ProviderEventService = class {
26051
26273
  if (this.started) return;
26052
26274
  this.started = true;
26053
26275
  for (const adapter of this.registry.adapters()) {
26054
- if (!adapter.subscribe) continue;
26055
- try {
26056
- const unsubscribe = await adapter.subscribe(
26057
- (event) => this.handle(event),
26058
- this.controller.signal
26059
- );
26060
- this.unsubscribes.push(unsubscribe);
26061
- } catch (error62) {
26062
- this.logger.warn("Provider event subscription failed", {
26063
- providerID: adapter.id,
26064
- error: error62 instanceof Error ? error62.message : String(error62)
26065
- });
26066
- }
26276
+ await this.subscribe(adapter);
26277
+ }
26278
+ }
26279
+ async subscribe(adapter) {
26280
+ if (!this.started || !adapter.subscribe) return;
26281
+ this.unsubscribe(adapter.id);
26282
+ try {
26283
+ const unsubscribe = await adapter.subscribe(
26284
+ (event) => this.handle(event),
26285
+ this.controller.signal
26286
+ );
26287
+ this.subscriptions.set(adapter.id, unsubscribe);
26288
+ } catch (error62) {
26289
+ this.logger.warn("Provider event subscription failed", {
26290
+ providerID: adapter.id,
26291
+ error: error62 instanceof Error ? error62.message : String(error62)
26292
+ });
26293
+ }
26294
+ }
26295
+ unsubscribe(id) {
26296
+ const unsubscribe = this.subscriptions.get(id);
26297
+ if (!unsubscribe) return;
26298
+ this.subscriptions.delete(id);
26299
+ try {
26300
+ unsubscribe();
26301
+ } catch {
26067
26302
  }
26068
26303
  }
26069
26304
  stop() {
26070
26305
  this.controller.abort();
26071
- for (const unsubscribe of this.unsubscribes.splice(0)) {
26306
+ for (const unsubscribe of this.subscriptions.values()) {
26072
26307
  try {
26073
26308
  unsubscribe();
26074
26309
  } catch {
26075
26310
  }
26076
26311
  }
26312
+ this.subscriptions.clear();
26077
26313
  for (const timer of this.timers.values()) clearTimeout(timer);
26078
26314
  for (const timer of this.messageTimers.values()) clearTimeout(timer);
26079
26315
  this.timers.clear();
@@ -27296,8 +27532,8 @@ var QuestionService = class {
27296
27532
 
27297
27533
  // packages/hub/src/app.ts
27298
27534
  import { existsSync as existsSync3 } from "fs";
27299
- import { readFile as readFile3 } from "fs/promises";
27300
- import { join as join3, resolve } from "path";
27535
+ import { readFile as readFile4 } from "fs/promises";
27536
+ import { join as join3, resolve as resolve2 } from "path";
27301
27537
 
27302
27538
  // node_modules/.pnpm/hono@4.13.14/node_modules/hono/dist/request/constants.js
27303
27539
  var GET_MATCH_RESULT = /* @__PURE__ */ Symbol();
@@ -29456,7 +29692,7 @@ var IdempotencyService = class {
29456
29692
  };
29457
29693
 
29458
29694
  // package.json
29459
- var version2 = "0.1.6";
29695
+ var version2 = "0.1.7";
29460
29696
 
29461
29697
  // packages/hub/src/version.ts
29462
29698
  var VERSION = version2;
@@ -29978,8 +30214,32 @@ function registerPairRoutes(app, deps) {
29978
30214
  }
29979
30215
 
29980
30216
  // packages/hub/src/http/routes/providers.ts
29981
- function registerProviderRoutes(app, registry2) {
30217
+ function registerProviderRoutes(app, registry2, admin) {
29982
30218
  app.get("/api/v1/providers", (c) => c.json({ data: registry2.list() }));
30219
+ app.post("/api/v1/providers", async (c) => {
30220
+ if (!admin) throw notImplemented("Provider management is not configured");
30221
+ const body = await c.req.json().catch(() => void 0);
30222
+ const parsed = zProviderInput.safeParse(body);
30223
+ if (!parsed.success) {
30224
+ throw badRequest("Invalid provider input", {
30225
+ issues: parsed.error.issues
30226
+ });
30227
+ }
30228
+ return c.json({ data: await admin.add(parsed.data) }, 201);
30229
+ });
30230
+ app.put("/api/v1/providers/:providerID", async (c) => {
30231
+ if (!admin) throw notImplemented("Provider management is not configured");
30232
+ const providerID = c.req.param("providerID");
30233
+ if (!providerID) throw notFound("Provider not found");
30234
+ const body = await c.req.json().catch(() => void 0);
30235
+ const parsed = zProviderUpdateInput.safeParse(body);
30236
+ if (!parsed.success) {
30237
+ throw badRequest("Invalid provider input", {
30238
+ issues: parsed.error.issues
30239
+ });
30240
+ }
30241
+ return c.json({ data: await admin.update(providerID, parsed.data) });
30242
+ });
29983
30243
  app.post("/api/v1/providers/:providerID/check", async (c) => {
29984
30244
  const providerID = c.req.param("providerID");
29985
30245
  if (!registry2.get(providerID)) throw notFound("Provider not found");
@@ -30558,7 +30818,7 @@ function createApp(deps) {
30558
30818
  });
30559
30819
  registerAuthRoutes(app, { auth, version: VERSION });
30560
30820
  registerPairRoutes(app, { auth });
30561
- registerProviderRoutes(app, deps.registry);
30821
+ registerProviderRoutes(app, deps.registry, deps.providerAdmin);
30562
30822
  registerSessionRoutes(
30563
30823
  app,
30564
30824
  deps.sessions,
@@ -30578,7 +30838,7 @@ function createApp(deps) {
30578
30838
  store: deps.store
30579
30839
  });
30580
30840
  if (deps.appsRoot) {
30581
- const appsRoot = resolve(deps.appsRoot);
30841
+ const appsRoot = resolve2(deps.appsRoot);
30582
30842
  app.get(
30583
30843
  "/apps/:app",
30584
30844
  (c) => c.redirect(`/apps/${encodeURIComponent(c.req.param("app"))}/`, 308)
@@ -30597,12 +30857,12 @@ function createApp(deps) {
30597
30857
  if (leaf.includes(".")) return next();
30598
30858
  const indexPath = join3(appsRoot, appID, "index.html");
30599
30859
  if (!existsSync3(indexPath)) return next();
30600
- return c.html(await readFile3(indexPath, "utf8"));
30860
+ return c.html(await readFile4(indexPath, "utf8"));
30601
30861
  });
30602
30862
  app.all("/apps/*", (c) => c.notFound());
30603
30863
  }
30604
30864
  if (deps.webRoot) {
30605
- const root = resolve(deps.webRoot);
30865
+ const root = resolve2(deps.webRoot);
30606
30866
  const indexPath = join3(root, "index.html");
30607
30867
  if (existsSync3(indexPath)) {
30608
30868
  app.use("*", serveStatic({ root }));
@@ -30610,7 +30870,7 @@ function createApp(deps) {
30610
30870
  if (c.req.path.startsWith("/api/") || c.req.path.startsWith("/apps/")) {
30611
30871
  return next();
30612
30872
  }
30613
- const html = await readFile3(indexPath, "utf8");
30873
+ const html = await readFile4(indexPath, "utf8");
30614
30874
  return c.html(html);
30615
30875
  });
30616
30876
  }
@@ -30762,8 +31022,8 @@ function printPairingPanel(config2, pairing, options = {}) {
30762
31022
  }
30763
31023
  async function waitForListen(server) {
30764
31024
  if (server.listening) return;
30765
- await new Promise((resolve3, reject) => {
30766
- server.once("listening", resolve3);
31025
+ await new Promise((resolve4, reject) => {
31026
+ server.once("listening", resolve4);
30767
31027
  server.once("error", reject);
30768
31028
  });
30769
31029
  }
@@ -31037,7 +31297,7 @@ Start Oktoso on another port with: npx @amoshydra/oktoso --port ${config2.port <
31037
31297
  }
31038
31298
  createLogFile();
31039
31299
  auth = new AuthService(config2.token);
31040
- const stateFile = join4(resolve2(config2.dataDir), "state.json");
31300
+ const stateFile = join4(resolve3(config2.dataDir), "state.json");
31041
31301
  const store = await JsonStateStore.open({ filePath: stateFile });
31042
31302
  const eventBus = new EventBus(store, logger);
31043
31303
  await eventBus.init();
@@ -31061,9 +31321,17 @@ Start Oktoso on another port with: npx @amoshydra/oktoso --port ${config2.port <
31061
31321
  logger,
31062
31322
  attention
31063
31323
  );
31064
- const bundledRoot = dirname4(fileURLToPath(import.meta.url));
31065
- const configuredWebRoot = config2.webRoot ? resolve2(config2.webRoot) : existsSync4(resolve2(bundledRoot, "web")) ? resolve2(bundledRoot, "web") : existsSync4(resolve2("dist/web")) ? resolve2("dist/web") : void 0;
31066
- const configuredAppsRoot = config2.appsRoot ? resolve2(config2.appsRoot) : existsSync4(resolve2(bundledRoot, "apps")) ? resolve2(bundledRoot, "apps") : existsSync4(resolve2("dist/apps")) ? resolve2("dist/apps") : void 0;
31324
+ const providerAdmin = new ProviderAdminService(
31325
+ registry2,
31326
+ sessions,
31327
+ attention,
31328
+ providerEvents,
31329
+ logger,
31330
+ options.configPath
31331
+ );
31332
+ const bundledRoot = dirname5(fileURLToPath(import.meta.url));
31333
+ const configuredWebRoot = config2.webRoot ? resolve3(config2.webRoot) : existsSync4(resolve3(bundledRoot, "web")) ? resolve3(bundledRoot, "web") : existsSync4(resolve3("dist/web")) ? resolve3("dist/web") : void 0;
31334
+ const configuredAppsRoot = config2.appsRoot ? resolve3(config2.appsRoot) : existsSync4(resolve3(bundledRoot, "apps")) ? resolve3(bundledRoot, "apps") : existsSync4(resolve3("dist/apps")) ? resolve3("dist/apps") : void 0;
31067
31335
  const hubWebUrl = webUrl(config2);
31068
31336
  const startedAt = Date.now();
31069
31337
  const app = createApp({
@@ -31071,6 +31339,7 @@ Start Oktoso on another port with: npx @amoshydra/oktoso --port ${config2.port <
31071
31339
  store,
31072
31340
  eventBus,
31073
31341
  registry: registry2,
31342
+ providerAdmin,
31074
31343
  sessions,
31075
31344
  attention,
31076
31345
  drafts,
@@ -31201,7 +31470,7 @@ Start Oktoso on another port with: npx @amoshydra/oktoso --port ${config2.port <
31201
31470
  host: info.address,
31202
31471
  port: info.port,
31203
31472
  providers: config2.providers.length,
31204
- dataDir: resolve2(config2.dataDir),
31473
+ dataDir: resolve3(config2.dataDir),
31205
31474
  webRoot: configuredWebRoot ?? null
31206
31475
  });
31207
31476
  if (tuiEnabled && ui) {