omnigateway 0.3.0 → 0.4.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 (72) hide show
  1. package/README.md +172 -1
  2. package/bin/omni.js +1117 -40
  3. package/gateway.js +947 -89
  4. package/package.json +1 -1
  5. package/public/assets/{Chip-DbKk1ExR.js → Chip-BltJSw8A.js} +5 -5
  6. package/public/assets/Confirm-gd8Y6snu.js +4 -0
  7. package/public/assets/CopyValue-Dds-Ow9a.js +27 -0
  8. package/public/assets/{Field-B7GOsJf8.js → Field-D-14HF0x.js} +11 -11
  9. package/public/assets/Lamp-BDf7-dMa.js +25 -0
  10. package/public/assets/Match-CgNxphvS.js +1 -0
  11. package/public/assets/Meter-stpt3vQu.js +14 -0
  12. package/public/assets/Modal-Dj3LvMS9.js +82 -0
  13. package/public/assets/Rack-B4c0dbIb.js +180 -0
  14. package/public/assets/{Readout--CQnPjoO.js → Readout-Du15WYx0.js} +5 -5
  15. package/public/assets/{States-CQFkw1UZ.js → States-CxLDlCaw.js} +5 -5
  16. package/public/assets/{Table-Ba58Wg4-.js → Table-CVDZ69Zx.js} +1 -1
  17. package/public/assets/Toggle-DMk46RX2.js +39 -0
  18. package/public/assets/TokenBreakdown-BySwFK6Q.js +31 -0
  19. package/public/assets/_app-Dx_xnlud.js +1 -0
  20. package/public/assets/_app.accounts-DvBYnbfS.js +64 -0
  21. package/public/assets/_app.console-CoaRdkB1.js +44 -0
  22. package/public/assets/_app.database-CES5ec5g.js +39 -0
  23. package/public/assets/_app.index-DwOZe1A-.js +62 -0
  24. package/public/assets/_app.keys-B5HrAu2U.js +76 -0
  25. package/public/assets/_app.logs-NbfIexz0.js +68 -0
  26. package/public/assets/_app.models-B1MdFRtQ.js +148 -0
  27. package/public/assets/_app.plugins._pluginId-ByMziW8p.js +20 -0
  28. package/public/assets/_app.settings-BkDkGkKF.js +46 -0
  29. package/public/assets/_app.usage-DNpM8Nub.js +83 -0
  30. package/public/assets/dist-9ps0Qd15.js +1 -0
  31. package/public/assets/index-gMv8Qs7m.js +178 -0
  32. package/public/assets/login-cCXnl34r.js +36 -0
  33. package/public/assets/matchContext-BOenO3J2.js +1 -0
  34. package/public/assets/plus-fXfUTt6_.js +1 -0
  35. package/public/assets/queries-CIOPbRcE.js +142 -0
  36. package/public/assets/shared-B4o-2quR.js +84 -0
  37. package/public/assets/{trash-2-DTWWI3vA.js → trash-2-ZPh3619D.js} +1 -1
  38. package/public/index.html +7 -2
  39. package/public/shared/chunks/jsx-runtime-CznXPbDH.js +1 -0
  40. package/public/shared/chunks/react-GitiQ0dt.js +1 -0
  41. package/public/shared/chunks/react-dom-BTlfKTOo.js +1 -0
  42. package/public/shared/chunks/rolldown-runtime-CbXtAM7H.js +1 -0
  43. package/public/shared/jsx-runtime.js +1 -0
  44. package/public/shared/react-dom-client.js +9 -0
  45. package/public/shared/react-dom.js +1 -0
  46. package/public/shared/react-query.js +1 -0
  47. package/public/shared/react.js +1 -0
  48. package/public/shared/styled-components.js +7 -0
  49. package/public/assets/Confirm-C5DYoI93.js +0 -4
  50. package/public/assets/CopyValue-CXjietiZ.js +0 -27
  51. package/public/assets/Lamp-QRSv-SqM.js +0 -25
  52. package/public/assets/Meter-Dngwbpjo.js +0 -14
  53. package/public/assets/Modal-DfltaSCz.js +0 -82
  54. package/public/assets/Rack-D1zBw9SW.js +0 -151
  55. package/public/assets/Toggle-DWrWc9-9.js +0 -39
  56. package/public/assets/TokenBreakdown-CyHJv0O5.js +0 -31
  57. package/public/assets/_app-CTDgfstQ.js +0 -1
  58. package/public/assets/_app.accounts-DP6T8z7I.js +0 -64
  59. package/public/assets/_app.console-DYr7TCAl.js +0 -44
  60. package/public/assets/_app.database-BzO1S_VR.js +0 -39
  61. package/public/assets/_app.index-CBt6b-lA.js +0 -62
  62. package/public/assets/_app.keys-B8HGsPPS.js +0 -76
  63. package/public/assets/_app.logs-DKsxa2Su.js +0 -68
  64. package/public/assets/_app.models-CZD2UpM-.js +0 -148
  65. package/public/assets/_app.settings-B2C1LQIF.js +0 -46
  66. package/public/assets/_app.usage-CDWUXtZy.js +0 -83
  67. package/public/assets/dist-IkJ0qg-4.js +0 -1
  68. package/public/assets/index-ClU3Xqui.js +0 -186
  69. package/public/assets/login-E3gh2_Cl.js +0 -36
  70. package/public/assets/plus-DkO4vG-T.js +0 -1
  71. package/public/assets/queries-BJHk_wso.js +0 -144
  72. package/public/assets/shared-Bp35cgFf.js +0 -84
package/bin/omni.js CHANGED
@@ -114,7 +114,7 @@ function formatBytes(bytes) {
114
114
  }
115
115
 
116
116
  // apps/cli/src/run.ts
117
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync as renameSync2, rmSync as rmSync3, writeFileSync as writeFileSync2 } from "fs";
117
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync4, renameSync as renameSync2, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
118
118
  import { homedir as homedir3 } from "os";
119
119
 
120
120
  // packages/control/src/adminAuth.ts
@@ -19551,6 +19551,39 @@ CREATE TABLE usage_rollup (
19551
19551
  -- cannot drift into disagreeing about which rows count.
19552
19552
  `;
19553
19553
 
19554
+ // packages/store/src/sqlite/migrations/011_plugin_migrations.sql
19555
+ var _011_plugin_migrations_default = `-- The plugin migration ledger, kept deliberately apart from core's \`migrations\`.
19556
+ --
19557
+ -- Core's track is a single ascending integer owned by this repository. A plugin
19558
+ -- ships its own numbered migrations and knows nothing about ours, so the two
19559
+ -- cannot share a counter: if they did, installing a plugin would consume numbers
19560
+ -- that core's next migration then collides with, and the collision would surface
19561
+ -- as "migration already applied" on an upgrade rather than as anything legible.
19562
+ -- Core's next migration is 012 regardless of how many migrations any plugin
19563
+ -- ships, and a plugin's version 1 is its own version 1 forever.
19564
+ --
19565
+ -- The primary key is (plugin_id, version) rather than a surrogate id because
19566
+ -- that pair *is* the identity: applying the same version twice is the thing this
19567
+ -- table exists to prevent, and a uniqueness constraint states it once instead of
19568
+ -- leaving it to whichever code path happens to check first.
19569
+ --
19570
+ -- \`applied_at\` is milliseconds, matching every other timestamp in this schema.
19571
+ -- It is recorded for an operator reading the table during an incident, not read
19572
+ -- by the host: the host asks only whether a row exists.
19573
+ --
19574
+ -- Note what is *not* here: no row for the plugin's tables, and no cascade. A
19575
+ -- plugin's tables outlive its rows here on purpose \u2014 see \`PluginRepo.dropAll\`
19576
+ -- and \`orphanTables\`. Dropping a table because a plugin is currently absent
19577
+ -- destroys data irreversibly, and a restore is precisely the moment a plugin is
19578
+ -- most likely to be absent.
19579
+ CREATE TABLE plugin_migrations (
19580
+ plugin_id TEXT NOT NULL,
19581
+ version INTEGER NOT NULL,
19582
+ applied_at INTEGER NOT NULL,
19583
+ PRIMARY KEY (plugin_id, version)
19584
+ );
19585
+ `;
19586
+
19554
19587
  // packages/store/src/sqlite/rollup.ts
19555
19588
  var HOUR_MS = 3600000;
19556
19589
  function hourOf(at) {
@@ -19785,7 +19818,8 @@ var MIGRATIONS = [
19785
19818
  { id: 7, sql: _007_quota_samples_default },
19786
19819
  { id: 8, sql: _008_body_logging_default },
19787
19820
  { id: 9, sql: _009_key_limits_default },
19788
- { id: 10, sql: _010_usage_rollup_default, after: rebuildRollup }
19821
+ { id: 10, sql: _010_usage_rollup_default, after: rebuildRollup },
19822
+ { id: 11, sql: _011_plugin_migrations_default }
19789
19823
  ];
19790
19824
  function openDb(path) {
19791
19825
  const db = new Database(path, { create: true });
@@ -19938,6 +19972,168 @@ function createMaintenanceRepo(db) {
19938
19972
  }
19939
19973
  };
19940
19974
  }
19975
+ // packages/store/src/sqlite/plugins.ts
19976
+ var PLUGIN_ID = /^[a-z][a-z0-9-]{0,31}$/;
19977
+ var TABLE_NAME = /^[a-z][a-z0-9_]{0,31}$/;
19978
+ var PLACEHOLDER = /\{\{([^{}]*)\}\}/g;
19979
+ var CORE_TABLES = [
19980
+ "api_keys",
19981
+ "credential_health",
19982
+ "credentials",
19983
+ "migrations",
19984
+ "plugin_migrations",
19985
+ "quota_samples",
19986
+ "quota_windows",
19987
+ "request_bodies",
19988
+ "request_logs",
19989
+ "settings",
19990
+ "usage_daily",
19991
+ "usage_rollup",
19992
+ "virtual_models"
19993
+ ];
19994
+ var CORE_TABLE_REFERENCE = new RegExp(`\\b(?:${CORE_TABLES.join("|")})\\b`);
19995
+ var PREFIX3 = "plugin_";
19996
+ function stripNoise(sql) {
19997
+ let out = "";
19998
+ let i = 0;
19999
+ while (i < sql.length) {
20000
+ const c = sql[i];
20001
+ const next = sql[i + 1];
20002
+ if (c === "'") {
20003
+ i += 1;
20004
+ while (i < sql.length) {
20005
+ if (sql[i] === "'") {
20006
+ if (sql[i + 1] === "'")
20007
+ i += 2;
20008
+ else
20009
+ break;
20010
+ } else
20011
+ i += 1;
20012
+ }
20013
+ i += 1;
20014
+ out += " ";
20015
+ } else if (c === "-" && next === "-") {
20016
+ while (i < sql.length && sql[i] !== `
20017
+ `)
20018
+ i += 1;
20019
+ out += " ";
20020
+ } else if (c === "/" && next === "*") {
20021
+ i += 2;
20022
+ while (i < sql.length && !(sql[i] === "*" && sql[i + 1] === "/"))
20023
+ i += 1;
20024
+ i += 2;
20025
+ out += " ";
20026
+ } else {
20027
+ out += c;
20028
+ i += 1;
20029
+ }
20030
+ }
20031
+ return out;
20032
+ }
20033
+ function tableFor(pluginId, name) {
20034
+ return `"${PREFIX3}${pluginId}_${name}"`;
20035
+ }
20036
+ function prefixFor(pluginId) {
20037
+ return `${PREFIX3}${pluginId}_`;
20038
+ }
20039
+ function assertPluginId(pluginId) {
20040
+ if (!PLUGIN_ID.test(pluginId)) {
20041
+ throw new Error(`plugin id ${JSON.stringify(pluginId)} is not a valid identifier`);
20042
+ }
20043
+ }
20044
+ function prepare(pluginId, sql) {
20045
+ assertPluginId(pluginId);
20046
+ const expanded = sql.replace(PLACEHOLDER, (_match, name) => {
20047
+ if (!TABLE_NAME.test(name)) {
20048
+ throw new Error(`plugin table placeholder ${JSON.stringify(name)} must match ${TABLE_NAME.source}`);
20049
+ }
20050
+ return tableFor(pluginId, name);
20051
+ });
20052
+ const found = CORE_TABLE_REFERENCE.exec(stripNoise(expanded));
20053
+ if (found !== null) {
20054
+ throw new Error(`plugin sql may not reference the core table ${found[0]}`);
20055
+ }
20056
+ return expanded;
20057
+ }
20058
+ function toBinding(value, index) {
20059
+ if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean" || value instanceof Uint8Array) {
20060
+ return value;
20061
+ }
20062
+ throw new Error(`plugin sql parameter ${index} is not a bindable value`);
20063
+ }
20064
+ var toBindings = (params) => (params ?? []).map(toBinding);
20065
+ function createPluginRepo(db) {
20066
+ const pluginTables = () => db.query("SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name").all().map((r) => r.name).filter((name) => name.startsWith(PREFIX3) && name !== "plugin_migrations");
20067
+ const tablesOf = (pluginId) => {
20068
+ const prefix = prefixFor(pluginId);
20069
+ return pluginTables().filter((name) => name.startsWith(prefix));
20070
+ };
20071
+ const appliedVersions = (pluginId) => new Set(db.query("SELECT version FROM plugin_migrations WHERE plugin_id = ?").all(pluginId).map((r) => r.version));
20072
+ return {
20073
+ migrate(pluginId, migrations) {
20074
+ assertPluginId(pluginId);
20075
+ const done = appliedVersions(pluginId);
20076
+ const applied = [];
20077
+ const ordered = [...migrations].sort((a, b) => a.version - b.version);
20078
+ for (const migration of ordered) {
20079
+ if (done.has(migration.version))
20080
+ continue;
20081
+ try {
20082
+ if (!Number.isSafeInteger(migration.version) || migration.version < 1) {
20083
+ throw new Error("migration version must be a positive integer");
20084
+ }
20085
+ const sql = prepare(pluginId, migration.sql);
20086
+ db.transaction(() => {
20087
+ db.run(sql);
20088
+ db.run("INSERT INTO plugin_migrations (plugin_id, version, applied_at) VALUES (?,?,?)", [pluginId, migration.version, Date.now()]);
20089
+ })();
20090
+ applied.push(migration.version);
20091
+ } catch (error51) {
20092
+ return {
20093
+ applied,
20094
+ failed: {
20095
+ version: migration.version,
20096
+ reason: error51 instanceof Error ? error51.message : String(error51)
20097
+ }
20098
+ };
20099
+ }
20100
+ }
20101
+ return { applied };
20102
+ },
20103
+ run(pluginId, sql, params) {
20104
+ db.run(prepare(pluginId, sql), toBindings(params));
20105
+ },
20106
+ all(pluginId, sql, params) {
20107
+ return db.prepare(prepare(pluginId, sql)).all(...toBindings(params));
20108
+ },
20109
+ get(pluginId, sql, params) {
20110
+ const row = db.prepare(prepare(pluginId, sql)).get(...toBindings(params));
20111
+ return row === null || row === undefined ? null : row;
20112
+ },
20113
+ transaction(pluginId, fn) {
20114
+ assertPluginId(pluginId);
20115
+ return db.transaction(fn)();
20116
+ },
20117
+ listTables(pluginId) {
20118
+ assertPluginId(pluginId);
20119
+ return tablesOf(pluginId);
20120
+ },
20121
+ dropAll(pluginId) {
20122
+ assertPluginId(pluginId);
20123
+ const tables = tablesOf(pluginId);
20124
+ db.transaction(() => {
20125
+ for (const table2 of tables)
20126
+ db.run(`DROP TABLE IF EXISTS "${table2}"`);
20127
+ db.run("DELETE FROM plugin_migrations WHERE plugin_id = ?", [pluginId]);
20128
+ })();
20129
+ return tables.length;
20130
+ },
20131
+ orphanTables(installedIds) {
20132
+ const prefixes = installedIds.filter((id) => PLUGIN_ID.test(id)).map(prefixFor);
20133
+ return pluginTables().filter((name) => !prefixes.some((p) => name.startsWith(p)));
20134
+ }
20135
+ };
20136
+ }
19941
20137
  // packages/rtk/src/catalog.ts
19942
20138
  var RTK_FILTER_IDS = [
19943
20139
  "git-diff",
@@ -20250,7 +20446,8 @@ async function createStore(opts) {
20250
20446
  keys: createKeyRepo(db, logger2),
20251
20447
  usage: createUsageRepo(db),
20252
20448
  bodies: createBodyRepo(db, opts.encryptionKey, bodiesDirFor(opts.path)),
20253
- maintenance: createMaintenanceRepo(db)
20449
+ maintenance: createMaintenanceRepo(db),
20450
+ plugins: createPluginRepo(db)
20254
20451
  };
20255
20452
  };
20256
20453
  let handle = open();
@@ -20323,6 +20520,22 @@ async function createStore(opts) {
20323
20520
  snapshotTo: (path) => handle.maintenance.snapshotTo(path),
20324
20521
  inspect: (path) => handle.maintenance.inspect(path)
20325
20522
  },
20523
+ plugins: {
20524
+ migrate: (pluginId, migrations) => handle.plugins.migrate(pluginId, migrations),
20525
+ run: (pluginId, sql, params) => handle.plugins.run(pluginId, sql, params),
20526
+ all(pluginId, sql, params) {
20527
+ return handle.plugins.all(pluginId, sql, params);
20528
+ },
20529
+ get(pluginId, sql, params) {
20530
+ return handle.plugins.get(pluginId, sql, params);
20531
+ },
20532
+ transaction(pluginId, fn) {
20533
+ return handle.plugins.transaction(pluginId, fn);
20534
+ },
20535
+ listTables: (pluginId) => handle.plugins.listTables(pluginId),
20536
+ dropAll: (pluginId) => handle.plugins.dropAll(pluginId),
20537
+ orphanTables: (installedIds) => handle.plugins.orphanTables(installedIds)
20538
+ },
20326
20539
  routing: {
20327
20540
  version: () => handle.db.query("PRAGMA data_version").get()?.data_version ?? 0,
20328
20541
  subscribe(listener) {
@@ -20727,6 +20940,13 @@ async function swapIn(deps, candidate) {
20727
20940
  reason: error51 instanceof Error ? error51.message : "unknown"
20728
20941
  });
20729
20942
  }
20943
+ try {
20944
+ await deps.reapplyPluginSchema?.();
20945
+ } catch (error51) {
20946
+ (deps.logger ?? noopLogger).warn("plugin schema not reapplied after the swap; restart", {
20947
+ reason: error51 instanceof Error ? error51.message : "unknown"
20948
+ });
20949
+ }
20730
20950
  return {
20731
20951
  ok: true,
20732
20952
  counts: inspection.counts,
@@ -21028,13 +21248,594 @@ import {
21028
21248
  copyFileSync,
21029
21249
  mkdirSync,
21030
21250
  readdirSync,
21251
+ readFileSync,
21031
21252
  realpathSync,
21032
21253
  renameSync,
21033
21254
  rmSync,
21034
21255
  statfsSync,
21035
- statSync
21256
+ statSync,
21257
+ writeFileSync
21036
21258
  } from "fs";
21037
- import { join as join3 } from "path";
21259
+ import { join as join4 } from "path";
21260
+
21261
+ // packages/control/src/plugins.ts
21262
+ import { Buffer as Buffer2 } from "buffer";
21263
+ import { basename as basename2, join as join3, resolve as resolve2, sep as sep2 } from "path";
21264
+
21265
+ // packages/plugin-api/src/version.ts
21266
+ var PLUGIN_API_VERSION = 1;
21267
+ var DASHBOARD_SDK_VERSION = "0.1.0";
21268
+ // packages/plugin-api/src/manifest.ts
21269
+ var CAPABILITIES = [
21270
+ "storage",
21271
+ "files",
21272
+ "net:outbound",
21273
+ "events:request",
21274
+ "events:limit"
21275
+ ];
21276
+ var ID_PATTERN = /^[a-z][a-z0-9-]{0,31}$/;
21277
+ var idSchema = exports_external.string().regex(ID_PATTERN, "id must match /^[a-z][a-z0-9-]{0,31}$/");
21278
+ var entrySchema = exports_external.string().min(1).refine((p) => !p.startsWith("/") && !p.split("/").includes(".."), {
21279
+ message: "entry must be a relative path with no '..' segment"
21280
+ });
21281
+ var originSchema = exports_external.string().refine((value) => {
21282
+ let url2;
21283
+ try {
21284
+ url2 = new URL(value);
21285
+ } catch {
21286
+ return false;
21287
+ }
21288
+ if (url2.protocol !== "https:" && url2.protocol !== "http:")
21289
+ return false;
21290
+ if (url2.hostname === "")
21291
+ return false;
21292
+ if (url2.pathname !== "/" || url2.search !== "" || url2.hash !== "")
21293
+ return false;
21294
+ return value === url2.origin || value === `${url2.origin}/`;
21295
+ }, { message: "origin must be scheme://host with no path, query or fragment" });
21296
+ var navSchema = exports_external.object({
21297
+ label: exports_external.string().min(1).max(32)
21298
+ }).strict();
21299
+ var manifestSchema = exports_external.object({
21300
+ id: idSchema,
21301
+ name: exports_external.string().min(1).max(64),
21302
+ version: exports_external.string().min(1).max(32),
21303
+ api: exports_external.number().int().nonnegative(),
21304
+ sdk: exports_external.string().min(1).max(32).optional(),
21305
+ server: entrySchema.optional(),
21306
+ ui: entrySchema.optional(),
21307
+ nav: navSchema.optional(),
21308
+ capabilities: exports_external.array(exports_external.enum(CAPABILITIES)).default([]),
21309
+ origins: exports_external.array(originSchema).min(1).optional(),
21310
+ defaults: exports_external.record(exports_external.string(), exports_external.unknown()).optional()
21311
+ }).strict().refine((m) => m.server !== undefined || m.ui !== undefined, {
21312
+ message: "manifest must declare a server entry, a ui entry, or both"
21313
+ }).refine((m) => m.ui === undefined || m.sdk !== undefined, {
21314
+ message: "a ui entry requires an sdk range"
21315
+ }).refine((m) => !m.capabilities.includes("net:outbound") || m.origins !== undefined, {
21316
+ message: "net:outbound requires origins"
21317
+ }).refine((m) => m.capabilities.includes("net:outbound") || m.origins === undefined, {
21318
+ message: "origins requires the net:outbound capability"
21319
+ });
21320
+ function safeParseManifest(input) {
21321
+ const parsed = manifestSchema.safeParse(input);
21322
+ if (parsed.success)
21323
+ return { ok: true, manifest: parsed.data };
21324
+ const first = parsed.error.issues[0];
21325
+ const path = first?.path.join(".") ?? "";
21326
+ const message = first?.message ?? "invalid manifest";
21327
+ return { ok: false, reason: path === "" ? message : `${path}: ${message}` };
21328
+ }
21329
+ function isApiCompatible(manifest) {
21330
+ return manifest.api === PLUGIN_API_VERSION;
21331
+ }
21332
+
21333
+ // packages/control/src/plugins.ts
21334
+ var PLUGINS_DIRNAME = "plugins";
21335
+ function pluginsDir(root) {
21336
+ return join3(root, PLUGINS_DIRNAME);
21337
+ }
21338
+ var MANIFEST_FILENAME = "omni-plugin.json";
21339
+ var MAX_PLUGIN_BYTES = 32 * 1024 * 1024;
21340
+ var ID_PATTERN2 = /^[a-z][a-z0-9-]{0,31}$/;
21341
+ var NPM_TARBALL_ROOT = "package";
21342
+ function readManifest(fs, home) {
21343
+ const raw = fs.readText(join3(home, MANIFEST_FILENAME));
21344
+ if (raw === null) {
21345
+ return { check: "manifest", reason: `no ${MANIFEST_FILENAME}`, fatal: true };
21346
+ }
21347
+ let document;
21348
+ try {
21349
+ document = JSON.parse(raw);
21350
+ } catch (error51) {
21351
+ const detail = error51 instanceof Error ? error51.message : String(error51);
21352
+ return {
21353
+ check: "manifest",
21354
+ reason: `${MANIFEST_FILENAME} is not valid JSON: ${detail}`,
21355
+ fatal: true
21356
+ };
21357
+ }
21358
+ const parsed = safeParseManifest(document);
21359
+ if (!parsed.ok)
21360
+ return { check: "manifest", reason: parsed.reason, fatal: true };
21361
+ return parsed.manifest;
21362
+ }
21363
+ function checkEntry(fs, home, entry, label2, fatal) {
21364
+ const target = resolve2(home, entry);
21365
+ if (target !== home && !target.startsWith(`${home}${sep2}`)) {
21366
+ return {
21367
+ check: "entry",
21368
+ reason: `${label2} entry resolves outside the plugin directory`,
21369
+ fatal
21370
+ };
21371
+ }
21372
+ if (!fs.isFile(target)) {
21373
+ return { check: "entry", reason: `${label2} entry ${entry} does not exist`, fatal };
21374
+ }
21375
+ return null;
21376
+ }
21377
+ function verifyPlugin(deps, root, id) {
21378
+ const home = join3(pluginsDir(root), id);
21379
+ if (!ID_PATTERN2.test(id) || !deps.fs.isDirectory(home)) {
21380
+ throw new GatewayError("BAD_REQUEST", `no plugin "${id}" installed under ${pluginsDir(root)}`);
21381
+ }
21382
+ return reportFor(deps, home, id);
21383
+ }
21384
+ function reportFor(deps, home, id) {
21385
+ const problems = [];
21386
+ const read = readManifest(deps.fs, home);
21387
+ if (!("id" in read)) {
21388
+ problems.push(read);
21389
+ return { id, path: home, manifest: null, problems, loadable: false };
21390
+ }
21391
+ const manifest = read;
21392
+ if (manifest.id !== id) {
21393
+ problems.push({
21394
+ check: "id",
21395
+ reason: `manifest id ${manifest.id} does not match its directory ${id}`,
21396
+ fatal: true
21397
+ });
21398
+ }
21399
+ if (!isApiCompatible(manifest)) {
21400
+ problems.push({
21401
+ check: "api",
21402
+ reason: `plugin api ${manifest.api} is not supported by this host (api ${PLUGIN_API_VERSION})`,
21403
+ fatal: true
21404
+ });
21405
+ }
21406
+ if (manifest.server !== undefined) {
21407
+ const problem = checkEntry(deps.fs, home, manifest.server, "server", true);
21408
+ if (problem !== null)
21409
+ problems.push(problem);
21410
+ }
21411
+ if (manifest.ui !== undefined) {
21412
+ const problem = checkEntry(deps.fs, home, manifest.ui, "ui", false);
21413
+ if (problem !== null)
21414
+ problems.push(problem);
21415
+ }
21416
+ const sdk = sdkProblem(deps, manifest);
21417
+ if (sdk !== null)
21418
+ problems.push(sdk);
21419
+ return {
21420
+ id,
21421
+ path: home,
21422
+ manifest,
21423
+ problems,
21424
+ loadable: !problems.some((problem) => problem.fatal)
21425
+ };
21426
+ }
21427
+ function sdkProblem(deps, manifest) {
21428
+ if (manifest.ui === undefined || manifest.sdk === undefined)
21429
+ return null;
21430
+ if (deps.sdkVersion === undefined)
21431
+ return null;
21432
+ if (Bun.semver.satisfies(deps.sdkVersion, manifest.sdk))
21433
+ return null;
21434
+ return {
21435
+ check: "sdk",
21436
+ reason: `requires dashboard sdk ${manifest.sdk}, host ships ${deps.sdkVersion}`,
21437
+ fatal: false
21438
+ };
21439
+ }
21440
+ function listPlugins(deps, root) {
21441
+ const dir = pluginsDir(root);
21442
+ const summaries = [];
21443
+ for (const entry of [...deps.fs.readdir(dir)].sort()) {
21444
+ const home = join3(dir, entry);
21445
+ if (!deps.fs.isDirectory(home))
21446
+ continue;
21447
+ if (!ID_PATTERN2.test(entry)) {
21448
+ summaries.push({
21449
+ id: entry,
21450
+ path: home,
21451
+ manifest: null,
21452
+ problems: [
21453
+ {
21454
+ check: "id",
21455
+ reason: `directory name ${entry} is not a valid plugin id`,
21456
+ fatal: true
21457
+ }
21458
+ ],
21459
+ loadable: false,
21460
+ name: null,
21461
+ version: null,
21462
+ api: null,
21463
+ sdk: null,
21464
+ capabilities: [],
21465
+ origins: []
21466
+ });
21467
+ continue;
21468
+ }
21469
+ const report = reportFor(deps, home, entry);
21470
+ const manifest = report.manifest;
21471
+ summaries.push({
21472
+ ...report,
21473
+ name: manifest?.name ?? null,
21474
+ version: manifest?.version ?? null,
21475
+ api: manifest?.api ?? null,
21476
+ sdk: manifest?.sdk ?? null,
21477
+ capabilities: manifest?.capabilities ?? [],
21478
+ origins: manifest?.origins ?? []
21479
+ });
21480
+ }
21481
+ return summaries;
21482
+ }
21483
+ var BLOCK = 512;
21484
+ var decoder2 = new TextDecoder;
21485
+ function fieldText(header, offset, length) {
21486
+ const slice = header.subarray(offset, offset + length);
21487
+ const end = slice.indexOf(0);
21488
+ return decoder2.decode(end === -1 ? slice : slice.subarray(0, end));
21489
+ }
21490
+ function fieldOctal(header, offset, length) {
21491
+ const text = fieldText(header, offset, length).trim();
21492
+ if (text.length === 0)
21493
+ return 0;
21494
+ const value = Number.parseInt(text, 8);
21495
+ return Number.isSafeInteger(value) && value >= 0 ? value : -1;
21496
+ }
21497
+ function isSafeArchivePath(path) {
21498
+ if (path.length === 0 || path.startsWith("/") || path.includes("\x00"))
21499
+ return false;
21500
+ if (path.includes("\\"))
21501
+ return false;
21502
+ return !path.split("/").includes("..");
21503
+ }
21504
+ function readTar(bytes) {
21505
+ const files = new Map;
21506
+ let total = 0;
21507
+ let longName = null;
21508
+ let offset = 0;
21509
+ while (offset + BLOCK <= bytes.length) {
21510
+ const header = bytes.subarray(offset, offset + BLOCK);
21511
+ if (header.every((byte) => byte === 0))
21512
+ break;
21513
+ const size = fieldOctal(header, 124, 12);
21514
+ if (size < 0)
21515
+ throw new GatewayError("BAD_REQUEST", "archive has an unreadable entry size");
21516
+ const type = String.fromCharCode(header[156] ?? 0);
21517
+ const name = fieldText(header, 0, 100);
21518
+ const prefix = fieldText(header, 345, 155);
21519
+ offset += BLOCK;
21520
+ total += size;
21521
+ if (total > MAX_PLUGIN_BYTES) {
21522
+ throw new GatewayError("BAD_REQUEST", `archive unpacks to more than ${MAX_PLUGIN_BYTES} bytes`);
21523
+ }
21524
+ if (offset + size > bytes.length) {
21525
+ throw new GatewayError("BAD_REQUEST", `archive is truncated: entry ${name || "(unnamed)"} declares ${size} bytes and the archive ends before them`);
21526
+ }
21527
+ const data = bytes.subarray(offset, offset + size);
21528
+ offset += Math.ceil(size / BLOCK) * BLOCK;
21529
+ if (type === "1" || type === "2") {
21530
+ throw new GatewayError("BAD_REQUEST", `archive contains a link (${name}); plugin packages must be plain files`);
21531
+ }
21532
+ if (type === "L") {
21533
+ longName = decoder2.decode(data).replace(/\0+$/, "");
21534
+ continue;
21535
+ }
21536
+ if (type === "K" || type === "x" || type === "g")
21537
+ continue;
21538
+ if (type !== "0" && type !== "\x00") {
21539
+ longName = null;
21540
+ continue;
21541
+ }
21542
+ const path = longName ?? (prefix.length > 0 ? `${prefix}/${name}` : name);
21543
+ longName = null;
21544
+ if (!isSafeArchivePath(path)) {
21545
+ throw new GatewayError("BAD_REQUEST", `archive contains an unsafe path: ${path}`);
21546
+ }
21547
+ files.set(path, new Uint8Array(data));
21548
+ }
21549
+ return files;
21550
+ }
21551
+ function gunzipIfNeeded(bytes) {
21552
+ if (bytes[0] !== 31 || bytes[1] !== 139)
21553
+ return bytes;
21554
+ try {
21555
+ return Bun.gunzipSync(new Uint8Array(bytes));
21556
+ } catch (error51) {
21557
+ const detail = error51 instanceof Error ? error51.message : String(error51);
21558
+ throw new GatewayError("BAD_REQUEST", `could not decompress the archive: ${detail}`);
21559
+ }
21560
+ }
21561
+ function stripRoot(files) {
21562
+ const roots = new Set;
21563
+ for (const path of files.keys()) {
21564
+ const slash = path.indexOf("/");
21565
+ if (slash <= 0)
21566
+ return { files, claimedId: null };
21567
+ roots.add(path.slice(0, slash));
21568
+ }
21569
+ const [only] = [...roots];
21570
+ if (roots.size !== 1 || only === undefined)
21571
+ return { files, claimedId: null };
21572
+ const stripped = new Map;
21573
+ for (const [path, bytes] of files)
21574
+ stripped.set(path.slice(only.length + 1), bytes);
21575
+ return { files: stripped, claimedId: only === NPM_TARBALL_ROOT ? null : only };
21576
+ }
21577
+ function readTree(fs, dir, prefix = "") {
21578
+ const files = new Map;
21579
+ let total = 0;
21580
+ for (const entry of fs.readdir(dir)) {
21581
+ const path = join3(dir, entry);
21582
+ const relative = prefix.length === 0 ? entry : `${prefix}/${entry}`;
21583
+ if (fs.isDirectory(path)) {
21584
+ for (const [nested, bytes2] of readTree(fs, path, relative))
21585
+ files.set(nested, bytes2);
21586
+ continue;
21587
+ }
21588
+ const bytes = fs.readBytes(path);
21589
+ if (bytes === null)
21590
+ continue;
21591
+ total += bytes.byteLength;
21592
+ if (total > MAX_PLUGIN_BYTES) {
21593
+ throw new GatewayError("BAD_REQUEST", `${dir} holds more than ${MAX_PLUGIN_BYTES} bytes`);
21594
+ }
21595
+ files.set(relative, bytes);
21596
+ }
21597
+ return files;
21598
+ }
21599
+ var DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org";
21600
+ var NPM_PACKUMENT_ACCEPT = "application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*";
21601
+ var NPM_NAME_PATTERN = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
21602
+ var EXACT_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)*$/;
21603
+ var SRI_ALGORITHMS = ["sha512", "sha384", "sha256"];
21604
+ function asRecord(value) {
21605
+ if (typeof value !== "object" || value === null || Array.isArray(value))
21606
+ return null;
21607
+ return value;
21608
+ }
21609
+ function asString(value) {
21610
+ return typeof value === "string" ? value : null;
21611
+ }
21612
+ function parseNpmSpec(spec) {
21613
+ const at = spec.lastIndexOf("@");
21614
+ const name = at <= 0 ? spec : spec.slice(0, at);
21615
+ const version2 = at <= 0 ? null : spec.slice(at + 1);
21616
+ if (name.length > 214 || !NPM_NAME_PATTERN.test(name))
21617
+ return null;
21618
+ return { name, version: version2 };
21619
+ }
21620
+ function registryUrl(deps) {
21621
+ const raw = deps.registry ?? DEFAULT_NPM_REGISTRY;
21622
+ let url2;
21623
+ try {
21624
+ url2 = new URL(raw);
21625
+ } catch {
21626
+ throw new GatewayError("BAD_REQUEST", `registry ${raw} is not a URL`);
21627
+ }
21628
+ if (url2.protocol !== "https:") {
21629
+ throw new GatewayError("BAD_REQUEST", `plugins may only be installed over https; ${raw} is not`);
21630
+ }
21631
+ return url2;
21632
+ }
21633
+ function packumentUrl(registry2, name) {
21634
+ return `${registry2.href.replace(/\/+$/, "")}/${name.replace("/", "%2f")}`;
21635
+ }
21636
+ function resolveDist(document, spec) {
21637
+ const root = asRecord(document);
21638
+ if (root === null) {
21639
+ throw new GatewayError("BAD_REQUEST", `the registry's answer for ${spec.name} is not a package`);
21640
+ }
21641
+ const versions2 = asRecord(root.versions);
21642
+ if (versions2 === null) {
21643
+ throw new GatewayError("BAD_REQUEST", `the registry lists no versions of ${spec.name}`);
21644
+ }
21645
+ let wanted = spec.version;
21646
+ if (wanted === null) {
21647
+ const tags = asRecord(root["dist-tags"]);
21648
+ wanted = tags === null ? null : asString(tags.latest);
21649
+ if (wanted === null) {
21650
+ throw new GatewayError("BAD_REQUEST", `the registry names no dist-tags.latest for ${spec.name}; install ${spec.name}@<exact version>`);
21651
+ }
21652
+ }
21653
+ const entry = asRecord(versions2[wanted]);
21654
+ if (entry === null) {
21655
+ throw new GatewayError("BAD_REQUEST", `the registry has no version ${wanted} of ${spec.name}`);
21656
+ }
21657
+ const dist = asRecord(entry.dist);
21658
+ const tarball = dist === null ? null : asString(dist.tarball);
21659
+ if (dist === null || tarball === null) {
21660
+ throw new GatewayError("BAD_REQUEST", `the registry's entry for ${spec.name}@${wanted} names no tarball`);
21661
+ }
21662
+ return {
21663
+ version: wanted,
21664
+ dist: { tarball, integrity: asString(dist.integrity), shasum: asString(dist.shasum) }
21665
+ };
21666
+ }
21667
+ function checkTarballUrl(tarball, registry2, what) {
21668
+ let url2;
21669
+ try {
21670
+ url2 = new URL(tarball);
21671
+ } catch {
21672
+ throw new GatewayError("BAD_REQUEST", `the registry gave ${what} a tarball that is not a URL`);
21673
+ }
21674
+ if (url2.protocol !== "https:") {
21675
+ throw new GatewayError("BAD_REQUEST", `plugins may only be installed over https; the registry points ${what} at ${tarball}`);
21676
+ }
21677
+ if (url2.host !== registry2.host) {
21678
+ throw new GatewayError("BAD_REQUEST", `registry ${registry2.host} points ${what} at ${url2.host}; ` + "install that URL directly if it is the one you meant");
21679
+ }
21680
+ }
21681
+ function digestsFor(dist, what) {
21682
+ const digests = [];
21683
+ if (dist.integrity !== null) {
21684
+ for (const entry of dist.integrity.trim().split(/\s+/)) {
21685
+ const dash = entry.indexOf("-");
21686
+ const algorithm = dash === -1 ? "" : entry.slice(0, dash);
21687
+ const value = dash === -1 ? "" : entry.slice(dash + 1);
21688
+ if (value.length === 0)
21689
+ continue;
21690
+ const known = SRI_ALGORITHMS.find((candidate) => candidate === algorithm);
21691
+ if (known === undefined)
21692
+ continue;
21693
+ digests.push({
21694
+ algorithm: known,
21695
+ expected: Buffer2.from(value, "base64").toString("hex"),
21696
+ source: `${known} integrity`
21697
+ });
21698
+ }
21699
+ }
21700
+ if (digests.length === 0 && dist.shasum !== null && /^[0-9a-f]{40}$/i.test(dist.shasum)) {
21701
+ digests.push({ algorithm: "sha1", expected: dist.shasum.toLowerCase(), source: "sha1 shasum" });
21702
+ }
21703
+ if (digests.length === 0) {
21704
+ throw new GatewayError("BAD_REQUEST", `the registry advertises no usable integrity or shasum for ${what}; ` + "refusing to install bytes nothing vouches for");
21705
+ }
21706
+ return digests;
21707
+ }
21708
+ function verifyDigests(bytes, digests, what) {
21709
+ for (const digest of digests) {
21710
+ const actual = new Bun.CryptoHasher(digest.algorithm).update(bytes).digest("hex");
21711
+ if (actual === digest.expected)
21712
+ return;
21713
+ }
21714
+ const first = digests[0];
21715
+ throw new GatewayError("BAD_REQUEST", `${what} does not match the ${first?.source ?? "digest"} the registry advertised; ` + "the download was corrupted or the registry served something else");
21716
+ }
21717
+ async function npmPayload(deps, spec) {
21718
+ if (spec.version !== null && !EXACT_VERSION_PATTERN.test(spec.version)) {
21719
+ throw new GatewayError("BAD_REQUEST", `${spec.name}@${spec.version} is not an exact version; this installer resolves no ranges ` + `or tags \u2014 install ${spec.name}@1.2.3, or ${spec.name} for the registry's latest`);
21720
+ }
21721
+ const fetchBytes = deps.fetchBytes;
21722
+ if (fetchBytes === undefined) {
21723
+ throw new GatewayError("BAD_REQUEST", "this caller cannot install from a registry");
21724
+ }
21725
+ const registry2 = registryUrl(deps);
21726
+ const document = parseJson(await fetchBytes(packumentUrl(registry2, spec.name), NPM_PACKUMENT_ACCEPT), `the registry's answer for ${spec.name}`);
21727
+ const { version: version2, dist } = resolveDist(document, spec);
21728
+ const what = `${spec.name}@${version2}`;
21729
+ checkTarballUrl(dist.tarball, registry2, what);
21730
+ const digests = digestsFor(dist, what);
21731
+ const bytes = await fetchBytes(dist.tarball);
21732
+ verifyDigests(bytes, digests, what);
21733
+ return stripRoot(readTar(gunzipIfNeeded(bytes)));
21734
+ }
21735
+ function parseJson(bytes, what) {
21736
+ try {
21737
+ return JSON.parse(decoder2.decode(bytes));
21738
+ } catch (error51) {
21739
+ const detail = error51 instanceof Error ? error51.message : String(error51);
21740
+ throw new GatewayError("BAD_REQUEST", `${what} is not valid JSON: ${detail}`);
21741
+ }
21742
+ }
21743
+ async function loadPayload(deps, spec) {
21744
+ if (spec.startsWith("http://")) {
21745
+ throw new GatewayError("BAD_REQUEST", "plugins may only be installed over https");
21746
+ }
21747
+ if (spec.startsWith("https://")) {
21748
+ if (deps.fetchBytes === undefined) {
21749
+ throw new GatewayError("BAD_REQUEST", "this caller cannot install from a URL");
21750
+ }
21751
+ return stripRoot(readTar(gunzipIfNeeded(await deps.fetchBytes(spec))));
21752
+ }
21753
+ const source = resolve2(spec);
21754
+ if (deps.fs.isDirectory(source)) {
21755
+ const files = readTree(deps.fs, source);
21756
+ const claimed = basename2(source);
21757
+ return { files, claimedId: ID_PATTERN2.test(claimed) ? claimed : null };
21758
+ }
21759
+ const bytes = deps.fs.readBytes(source);
21760
+ if (bytes !== null)
21761
+ return stripRoot(readTar(gunzipIfNeeded(bytes)));
21762
+ const npm = parseNpmSpec(spec);
21763
+ if (npm !== null)
21764
+ return npmPayload(deps, npm);
21765
+ throw new GatewayError("BAD_REQUEST", `no directory or archive at ${source}, and "${spec}" is not a package name`);
21766
+ }
21767
+ async function installPlugin(deps, root, spec) {
21768
+ const payload = await loadPayload(deps, spec);
21769
+ const raw = payload.files.get(MANIFEST_FILENAME);
21770
+ if (raw === undefined) {
21771
+ throw new GatewayError("BAD_REQUEST", `${spec} has no ${MANIFEST_FILENAME} at its root`);
21772
+ }
21773
+ let document;
21774
+ try {
21775
+ document = JSON.parse(decoder2.decode(raw));
21776
+ } catch (error51) {
21777
+ const detail = error51 instanceof Error ? error51.message : String(error51);
21778
+ throw new GatewayError("BAD_REQUEST", `${MANIFEST_FILENAME} is not valid JSON: ${detail}`);
21779
+ }
21780
+ const parsed = safeParseManifest(document);
21781
+ if (!parsed.ok)
21782
+ throw new GatewayError("BAD_REQUEST", `invalid ${MANIFEST_FILENAME}: ${parsed.reason}`);
21783
+ const manifest = parsed.manifest;
21784
+ const targetId = payload.claimedId ?? manifest.id;
21785
+ if (manifest.id !== targetId) {
21786
+ throw new GatewayError("BAD_REQUEST", `manifest id ${manifest.id} does not match its directory ${targetId}; ` + "rename one of them so the gateway loads what you installed");
21787
+ }
21788
+ const dir = pluginsDir(root);
21789
+ const target = join3(dir, targetId);
21790
+ const replaced = deps.fs.isDirectory(target);
21791
+ const staging = join3(dir, `.staging-${targetId}`);
21792
+ deps.fs.rm(staging);
21793
+ try {
21794
+ for (const [path, bytes] of payload.files) {
21795
+ const destination = join3(staging, path);
21796
+ deps.fs.mkdir(join3(destination, ".."));
21797
+ deps.fs.writeBytes(destination, bytes);
21798
+ }
21799
+ deps.fs.rm(target);
21800
+ deps.fs.rename(staging, target);
21801
+ } finally {
21802
+ deps.fs.rm(staging);
21803
+ }
21804
+ return {
21805
+ id: targetId,
21806
+ name: manifest.name,
21807
+ version: manifest.version,
21808
+ path: target,
21809
+ replaced,
21810
+ restartRequired: true
21811
+ };
21812
+ }
21813
+ function removePlugin(deps, root, id, options = {}) {
21814
+ if (!ID_PATTERN2.test(id)) {
21815
+ throw new GatewayError("BAD_REQUEST", `"${id}" is not a valid plugin id`);
21816
+ }
21817
+ const target = join3(pluginsDir(root), id);
21818
+ const present = deps.fs.isDirectory(target);
21819
+ const purge = options.purge === true;
21820
+ if (purge && deps.store === undefined) {
21821
+ throw new GatewayError("INTERNAL", "purging a plugin's data needs a database");
21822
+ }
21823
+ const tables = purge && deps.store !== undefined ? deps.store.plugins.listTables(id) : [];
21824
+ if (!present && tables.length === 0) {
21825
+ throw new GatewayError("BAD_REQUEST", `no plugin "${id}" installed under ${pluginsDir(root)}`);
21826
+ }
21827
+ if (present)
21828
+ deps.fs.rm(target);
21829
+ if (purge && deps.store !== undefined)
21830
+ deps.store.plugins.dropAll(id);
21831
+ return { id, removed: present, droppedTables: tables };
21832
+ }
21833
+ function orphanPluginTables(deps, root, store) {
21834
+ const installed = listPlugins(deps, root).map((plugin) => plugin.id);
21835
+ return store.plugins.orphanTables(installed);
21836
+ }
21837
+
21838
+ // packages/control/src/nodeFs.ts
21038
21839
  function nodeDatabaseFs() {
21039
21840
  const dirBytes = (dir) => {
21040
21841
  let entries;
@@ -21045,7 +21846,7 @@ function nodeDatabaseFs() {
21045
21846
  }
21046
21847
  let total = 0;
21047
21848
  for (const entry of entries) {
21048
- const path = join3(dir, entry.name);
21849
+ const path = join4(dir, entry.name);
21049
21850
  if (entry.isDirectory()) {
21050
21851
  total += dirBytes(path);
21051
21852
  continue;
@@ -21098,6 +21899,96 @@ function nodeDatabaseFs() {
21098
21899
  dirBytes
21099
21900
  };
21100
21901
  }
21902
+ function nodePluginFs() {
21903
+ return {
21904
+ readdir: (dir) => {
21905
+ try {
21906
+ return readdirSync(dir);
21907
+ } catch {
21908
+ return [];
21909
+ }
21910
+ },
21911
+ readText: (path) => {
21912
+ try {
21913
+ return readFileSync(path, "utf8");
21914
+ } catch {
21915
+ return null;
21916
+ }
21917
+ },
21918
+ readBytes: (path) => {
21919
+ try {
21920
+ return new Uint8Array(readFileSync(path));
21921
+ } catch {
21922
+ return null;
21923
+ }
21924
+ },
21925
+ writeBytes: (path, bytes) => {
21926
+ writeFileSync(path, bytes);
21927
+ },
21928
+ isDirectory: (path) => {
21929
+ try {
21930
+ return statSync(path).isDirectory();
21931
+ } catch {
21932
+ return false;
21933
+ }
21934
+ },
21935
+ isFile: (path) => {
21936
+ try {
21937
+ return statSync(path).isFile();
21938
+ } catch {
21939
+ return false;
21940
+ }
21941
+ },
21942
+ mkdir: (dir) => {
21943
+ mkdirSync(dir, { recursive: true });
21944
+ },
21945
+ rm: (path) => {
21946
+ rmSync(path, { force: true, recursive: true });
21947
+ },
21948
+ rename: (from, to) => renameSync(from, to)
21949
+ };
21950
+ }
21951
+ function nodeFetchBytes(options = {}) {
21952
+ const impl = options.fetchImpl ?? fetch;
21953
+ const maxBytes = options.maxBytes ?? MAX_PLUGIN_BYTES;
21954
+ return async (url2, accept) => {
21955
+ if (!url2.startsWith("https://")) {
21956
+ throw new GatewayError("BAD_REQUEST", `plugins may only be fetched over https; ${url2} is not`);
21957
+ }
21958
+ const response = await impl(url2, {
21959
+ headers: accept === undefined ? {} : { accept },
21960
+ redirect: "follow"
21961
+ });
21962
+ if (!response.ok) {
21963
+ throw new GatewayError("BAD_REQUEST", `${url2} answered ${response.status}`);
21964
+ }
21965
+ const reader = response.body?.getReader();
21966
+ if (reader === undefined)
21967
+ return new Uint8Array(0);
21968
+ const chunks = [];
21969
+ let total = 0;
21970
+ for (;; ) {
21971
+ const { done, value } = await reader.read();
21972
+ if (done)
21973
+ break;
21974
+ if (value === undefined)
21975
+ continue;
21976
+ total += value.byteLength;
21977
+ if (total > maxBytes) {
21978
+ await reader.cancel();
21979
+ throw new GatewayError("BAD_REQUEST", `${url2} sent more than ${maxBytes} bytes`);
21980
+ }
21981
+ chunks.push(value);
21982
+ }
21983
+ const bytes = new Uint8Array(total);
21984
+ let offset = 0;
21985
+ for (const chunk of chunks) {
21986
+ bytes.set(chunk, offset);
21987
+ offset += chunk.byteLength;
21988
+ }
21989
+ return bytes;
21990
+ };
21991
+ }
21101
21992
  // packages/control/src/oauth/pkce.ts
21102
21993
  function randomToken() {
21103
21994
  return Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString("base64url");
@@ -22305,9 +23196,9 @@ function requirePositional(parsed, index, name) {
22305
23196
  }
22306
23197
 
22307
23198
  // apps/cli/src/context.ts
22308
- import { existsSync, readFileSync } from "fs";
23199
+ import { existsSync, readFileSync as readFileSync2 } from "fs";
22309
23200
  import { homedir } from "os";
22310
- import { isAbsolute, join as join4, resolve as resolve2 } from "path";
23201
+ import { isAbsolute, join as join5, resolve as resolve3 } from "path";
22311
23202
  class CliError extends Error {
22312
23203
  exitCode;
22313
23204
  constructor(message, exitCode = 1) {
@@ -22316,21 +23207,21 @@ class CliError extends Error {
22316
23207
  this.exitCode = exitCode;
22317
23208
  }
22318
23209
  }
22319
- var DEFAULT_ROOT = join4(homedir(), ".config", "omnigateway");
23210
+ var DEFAULT_ROOT = join5(homedir(), ".config", "omnigateway");
22320
23211
  function looksLikeRoot(dir) {
22321
- return existsSync(join4(dir, ".env")) || existsSync(join4(dir, "omnigateway.db")) || existsSync(join4(dir, "apps", "gateway", "src", "index.ts"));
23212
+ return existsSync(join5(dir, ".env")) || existsSync(join5(dir, "omnigateway.db")) || existsSync(join5(dir, "apps", "gateway", "src", "index.ts"));
22322
23213
  }
22323
23214
  function resolveRoot(flags, env2, cwd) {
22324
23215
  const withEnvFile = (root, source) => {
22325
- const envFile = join4(root, ".env");
23216
+ const envFile = join5(root, ".env");
22326
23217
  return { root, source, envFile: existsSync(envFile) ? envFile : null };
22327
23218
  };
22328
23219
  if (flags.root !== undefined && flags.root.length > 0) {
22329
- return withEnvFile(resolve2(cwd, flags.root), "flag");
23220
+ return withEnvFile(resolve3(cwd, flags.root), "flag");
22330
23221
  }
22331
23222
  const fromEnv = env2.OMNI_ROOT;
22332
23223
  if (typeof fromEnv === "string" && fromEnv.length > 0) {
22333
- return withEnvFile(resolve2(cwd, fromEnv), "env");
23224
+ return withEnvFile(resolve3(cwd, fromEnv), "env");
22334
23225
  }
22335
23226
  if (looksLikeRoot(cwd))
22336
23227
  return withEnvFile(cwd, "cwd");
@@ -22361,7 +23252,7 @@ function createContext(parsed, options = {}) {
22361
23252
  const processEnv = options.env ?? process.env;
22362
23253
  const cwd = options.cwd ?? process.cwd();
22363
23254
  const root = resolveRoot({ root: stringFlag(parsed.values, "root") }, processEnv, cwd);
22364
- const fileEnv = root.envFile === null ? {} : parseEnvFile(readFileSync(root.envFile, "utf8"));
23255
+ const fileEnv = root.envFile === null ? {} : parseEnvFile(readFileSync2(root.envFile, "utf8"));
22365
23256
  const env2 = { ...processEnv, ...fileEnv };
22366
23257
  const rawDbFlag = stringFlag(parsed.values, "db");
22367
23258
  const dbFlag = rawDbFlag === undefined || rawDbFlag.trim().length === 0 ? undefined : rawDbFlag;
@@ -22377,7 +23268,7 @@ function createContext(parsed, options = {}) {
22377
23268
  configError = error51 instanceof Error ? error51.message : "invalid configuration";
22378
23269
  }
22379
23270
  const configuredPath = dbFlag ?? config2?.databasePath ?? "omnigateway.db";
22380
- const databasePath = isAbsolute(configuredPath) ? configuredPath : resolve2(root.root, configuredPath);
23271
+ const databasePath = isAbsolute(configuredPath) ? configuredPath : resolve3(root.root, configuredPath);
22381
23272
  const warnings = suppressedDbPath === null ? [] : [
22382
23273
  `ignoring OMNI_DB_PATH=${suppressedDbPath} from the environment because --root was given; using ${databasePath}`
22383
23274
  ];
@@ -22603,21 +23494,21 @@ async function pollUntilAuthorized(flows, start, deps) {
22603
23494
  }
22604
23495
 
22605
23496
  // apps/cli/src/service.ts
22606
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync } from "fs";
23497
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
22607
23498
  import { homedir as homedir2 } from "os";
22608
- import { dirname as dirname3, join as join5 } from "path";
23499
+ import { dirname as dirname3, join as join6 } from "path";
22609
23500
  var UNIT_NAME2 = "omnigateway.service";
22610
23501
  function unitPath(scope) {
22611
- return scope === "system" ? join5("/etc/systemd/system", UNIT_NAME2) : join5(homedir2(), ".config", "systemd", "user", UNIT_NAME2);
23502
+ return scope === "system" ? join6("/etc/systemd/system", UNIT_NAME2) : join6(homedir2(), ".config", "systemd", "user", UNIT_NAME2);
22612
23503
  }
22613
23504
  function systemctlArgs(scope, args) {
22614
23505
  return scope === "system" ? ["systemctl", ...args] : ["systemctl", "--user", ...args];
22615
23506
  }
22616
23507
  function pidFile(stateDir) {
22617
- return join5(stateDir, "gateway.pid");
23508
+ return join6(stateDir, "gateway.pid");
22618
23509
  }
22619
23510
  function logFile(stateDir) {
22620
- return join5(stateDir, "gateway.log");
23511
+ return join6(stateDir, "gateway.log");
22621
23512
  }
22622
23513
  function supervisedLogFile(deps) {
22623
23514
  const configured = deps.logFile?.trim();
@@ -22625,8 +23516,8 @@ function supervisedLogFile(deps) {
22625
23516
  }
22626
23517
  function defaultStateDir(env2) {
22627
23518
  const xdg = env2.XDG_STATE_HOME;
22628
- const base = typeof xdg === "string" && xdg.length > 0 ? xdg : join5(homedir2(), ".local", "state");
22629
- return join5(base, "omnigateway");
23519
+ const base = typeof xdg === "string" && xdg.length > 0 ? xdg : join6(homedir2(), ".local", "state");
23520
+ return join6(base, "omnigateway");
22630
23521
  }
22631
23522
  function unitFile(input) {
22632
23523
  return `[Unit]
@@ -22637,7 +23528,7 @@ Wants=network-online.target
22637
23528
  [Service]
22638
23529
  Type=simple
22639
23530
  WorkingDirectory=${input.root}
22640
- EnvironmentFile=${join5(input.root, ".env")}
23531
+ EnvironmentFile=${join6(input.root, ".env")}
22641
23532
  ExecStart=${input.bun} ${input.entrypoint}
22642
23533
  Restart=on-failure
22643
23534
  RestartSec=2
@@ -22653,7 +23544,7 @@ function readPid(stateDir) {
22653
23544
  const file2 = pidFile(stateDir);
22654
23545
  if (!existsSync2(file2))
22655
23546
  return null;
22656
- const pid = Number(readFileSync2(file2, "utf8").trim());
23547
+ const pid = Number(readFileSync3(file2, "utf8").trim());
22657
23548
  return Number.isInteger(pid) && pid > 0 ? pid : null;
22658
23549
  }
22659
23550
  function livePid(deps) {
@@ -22719,7 +23610,7 @@ async function start(deps, input) {
22719
23610
  env: { ...input.env, OMNI_LOG_FILE: file2 },
22720
23611
  logFile: file2
22721
23612
  });
22722
- writeFileSync(pidFile(deps.stateDir), `${pid}
23613
+ writeFileSync2(pidFile(deps.stateDir), `${pid}
22723
23614
  `);
22724
23615
  return { supervisor: "pidfile", pid, healthy: await waitForHealth(deps, input.baseUrl) };
22725
23616
  }
@@ -22765,7 +23656,7 @@ async function install(deps, input) {
22765
23656
  throw new Error(`${path} already exists; pass --force to replace it`);
22766
23657
  }
22767
23658
  mkdirSync2(dirname3(path), { recursive: true });
22768
- writeFileSync(path, unitFile({ root: deps.root, bun: input.bun, entrypoint: input.entrypoint }));
23659
+ writeFileSync2(path, unitFile({ root: deps.root, bun: input.bun, entrypoint: input.entrypoint }));
22769
23660
  const reload = await deps.run(systemctlArgs(deps.scope, ["daemon-reload"]));
22770
23661
  let enabled = false;
22771
23662
  if (input.enable) {
@@ -23716,6 +24607,168 @@ ${candidates}${excluded}`;
23716
24607
  });
23717
24608
  }
23718
24609
  };
24610
+ // packages/plugin-api/src/events.ts
24611
+ var WINDOW_MS2 = {
24612
+ "1m": 60000,
24613
+ "5h": 5 * 60 * 60 * 1000,
24614
+ "1w": 7 * 24 * 60 * 60 * 1000
24615
+ };
24616
+ // apps/cli/src/commands/plugins.ts
24617
+ function pluginDeps() {
24618
+ return { fs: nodePluginFs(), sdkVersion: DASHBOARD_SDK_VERSION };
24619
+ }
24620
+ function installDeps(ctx, args) {
24621
+ const flag = stringFlag(args.values, "registry");
24622
+ const raw = flag ?? ctx.env.OMNI_PLUGIN_REGISTRY;
24623
+ const registry2 = raw === undefined || raw.trim().length === 0 ? undefined : raw.trim();
24624
+ return {
24625
+ ...pluginDeps(),
24626
+ fetchBytes: nodeFetchBytes(),
24627
+ ...registry2 === undefined ? {} : { registry: registry2 }
24628
+ };
24629
+ }
24630
+ function pluginState(ctx, plugin) {
24631
+ if (!plugin.loadable)
24632
+ return state(ctx, false, "will not load");
24633
+ if (plugin.problems.length > 0)
24634
+ return paint(ctx, "yellow", "loads, with warnings");
24635
+ return state(ctx, true, "ok");
24636
+ }
24637
+ var pluginList = {
24638
+ usage: "plugin list",
24639
+ summary: "List installed plugins and whether the gateway would load each one",
24640
+ async run(_args, { ctx, writer }) {
24641
+ const root = ctx.root.root;
24642
+ const plugins = listPlugins(pluginDeps(), root);
24643
+ emit(ctx, writer, { pluginsDir: pluginsDir(root), plugins }, () => {
24644
+ if (plugins.length === 0) {
24645
+ return `no plugins installed; unpack one into ${pluginsDir(root)} with: omni plugin install <path-or-package>`;
24646
+ }
24647
+ const rows = plugins.map((plugin) => [
24648
+ plugin.id,
24649
+ plugin.name ?? "\u2014",
24650
+ plugin.version ?? "\u2014",
24651
+ plugin.api === null ? "\u2014" : String(plugin.api),
24652
+ plugin.sdk ?? "\u2014",
24653
+ plugin.capabilities.length === 0 ? "\u2014" : plugin.capabilities.join(","),
24654
+ pluginState(ctx, plugin)
24655
+ ]);
24656
+ const body2 = table([
24657
+ { header: "ID" },
24658
+ { header: "NAME" },
24659
+ { header: "VERSION" },
24660
+ { header: "API" },
24661
+ { header: "SDK" },
24662
+ { header: "CAPABILITIES" },
24663
+ { header: "STATE" }
24664
+ ], rows);
24665
+ const notes = plugins.flatMap((plugin) => plugin.problems.map((problem) => `${plugin.id}: ${paint(ctx, problem.fatal ? "red" : "yellow", problem.reason)}`));
24666
+ return notes.length === 0 ? body2 : `${body2}
24667
+
24668
+ ${notes.join(`
24669
+ `)}`;
24670
+ });
24671
+ }
24672
+ };
24673
+ var pluginVerify = {
24674
+ usage: "plugin verify <id>",
24675
+ summary: "Run every load-time check on one plugin without loading it",
24676
+ async run(args, { ctx, writer }) {
24677
+ const id = requirePositional(args, 0, "plugin id");
24678
+ const report = verifyPlugin(pluginDeps(), ctx.root.root, id);
24679
+ emit(ctx, writer, report, () => {
24680
+ const manifest = report.manifest;
24681
+ const head = fields([
24682
+ ["id", report.id],
24683
+ ["path", report.path],
24684
+ ["name", manifest?.name ?? "\u2014"],
24685
+ ["version", manifest?.version ?? "\u2014"],
24686
+ ["api", manifest === null ? "\u2014" : String(manifest.api)],
24687
+ ["sdk", manifest?.sdk ?? "\u2014"],
24688
+ ["server", manifest?.server ?? "\u2014"],
24689
+ ["ui", manifest?.ui ?? "\u2014"],
24690
+ [
24691
+ "capabilities",
24692
+ manifest === null || manifest.capabilities.length === 0 ? "\u2014" : manifest.capabilities.join(",")
24693
+ ],
24694
+ ["origins", manifest?.origins?.join(",") ?? "\u2014"]
24695
+ ]);
24696
+ if (report.problems.length === 0) {
24697
+ return `${head}
24698
+
24699
+ ${state(ctx, true, "ok")}: this plugin would load`;
24700
+ }
24701
+ const lines = report.problems.map((problem) => ` ${paint(ctx, problem.fatal ? "red" : "yellow", problem.check)} ${problem.reason}`);
24702
+ const verdict = report.loadable ? paint(ctx, "yellow", "this plugin would load, with the warnings above") : state(ctx, false, "this plugin would be skipped at boot");
24703
+ return `${head}
24704
+
24705
+ ${lines.join(`
24706
+ `)}
24707
+
24708
+ ${verdict}`;
24709
+ });
24710
+ if (!report.loadable)
24711
+ throw new CliError(`plugin "${id}" would not load`);
24712
+ }
24713
+ };
24714
+ var pluginInstall = {
24715
+ usage: "plugin install <path|tarball|https url|package[@version]> [--registry <url>]",
24716
+ summary: "Unpack a plugin into this installation, running nothing from it",
24717
+ options: { registry: { type: "string" } },
24718
+ async run(args, { ctx, writer }) {
24719
+ const spec = requirePositional(args, 0, "plugin directory, tarball, url, or package name");
24720
+ const result = await installPlugin(installDeps(ctx, args), ctx.root.root, spec);
24721
+ emit(ctx, writer, result, () => {
24722
+ note(ctx, writer, paint(ctx, "dim", "no code from the package was executed; verify it before restarting"));
24723
+ const verb = result.replaced ? "replaced" : "installed";
24724
+ return [
24725
+ `${verb} ${result.name} ${result.version} at ${result.path}`,
24726
+ paint(ctx, "yellow", "restart the gateway for this to take effect: omni restart")
24727
+ ].join(`
24728
+ `);
24729
+ });
24730
+ }
24731
+ };
24732
+ var pluginRemove = {
24733
+ usage: "plugin remove <id> [--purge]",
24734
+ summary: "Remove a plugin, optionally dropping its stored data too",
24735
+ options: { purge: { type: "boolean" } },
24736
+ async run(args, { ctx, writer, prompt }) {
24737
+ const id = requirePositional(args, 0, "plugin id");
24738
+ const purge = boolFlag(args.values, "purge");
24739
+ const question = purge ? `remove plugin "${id}" AND permanently drop its stored data?` : `remove plugin "${id}"? its stored data is kept`;
24740
+ if (!await prompt.confirm(question))
24741
+ throw new CliError("cancelled");
24742
+ const deps = purge ? { fs: nodePluginFs(), store: await ctx.store() } : { fs: nodePluginFs() };
24743
+ const result = removePlugin(deps, ctx.root.root, id, { purge });
24744
+ emit(ctx, writer, result, () => {
24745
+ const lines = [];
24746
+ lines.push(result.removed ? `removed ${id}` : `no directory for ${id} to remove`);
24747
+ if (purge) {
24748
+ lines.push(result.droppedTables.length === 0 ? "no stored tables to drop" : `dropped ${result.droppedTables.length} table(s): ${result.droppedTables.join(", ")}`);
24749
+ } else {
24750
+ lines.push(paint(ctx, "dim", "stored tables and migration history kept; --purge drops them"));
24751
+ }
24752
+ lines.push(paint(ctx, "yellow", "restart the gateway for this to take effect: omni restart"));
24753
+ return lines.join(`
24754
+ `);
24755
+ });
24756
+ }
24757
+ };
24758
+ function doctorPluginDeps() {
24759
+ return pluginDeps();
24760
+ }
24761
+ function pluginDoctorLines(ctx, plugins) {
24762
+ return plugins.map((plugin) => {
24763
+ const version2 = plugin.version === null ? "" : ` ${plugin.version}`;
24764
+ const api2 = plugin.api === null ? "api ?" : `api ${plugin.api}`;
24765
+ const sdk = plugin.sdk === null ? "no ui" : `sdk ${plugin.sdk}`;
24766
+ const reasons = plugin.problems.filter((problem) => problem.fatal).map((p) => p.reason);
24767
+ const warnings = plugin.problems.length - reasons.length;
24768
+ const verdict = reasons.length > 0 ? state(ctx, false, `will not load: ${reasons.join("; ")}`) : warnings > 0 ? paint(ctx, "yellow", `loads, ${warnings} warning(s); see omni plugin verify ${plugin.id}`) : state(ctx, true, "ok");
24769
+ return `${plugin.id}${version2} (${api2}, ${sdk}) ${verdict}`;
24770
+ });
24771
+ }
23719
24772
 
23720
24773
  // apps/cli/src/commands/quota.ts
23721
24774
  var WINDOW_ORDER = {
@@ -23820,7 +24873,7 @@ import { existsSync as existsSync5 } from "fs";
23820
24873
 
23821
24874
  // apps/cli/src/runtime.ts
23822
24875
  import { closeSync as closeSync2, existsSync as existsSync4, openSync as openSync2 } from "fs";
23823
- import { join as join6 } from "path";
24876
+ import { join as join7 } from "path";
23824
24877
  var runCommand = async (argv) => {
23825
24878
  const proc = Bun.spawn([...argv], { stdout: "pipe", stderr: "pipe" });
23826
24879
  const [stdout, stderr, code] = await Promise.all([
@@ -23877,9 +24930,9 @@ function processAlive(pid) {
23877
24930
  }
23878
24931
  function gatewayEntrypoint(root, cliDir = import.meta.dir) {
23879
24932
  const candidates = [
23880
- join6(root, "apps", "gateway", "src", "index.ts"),
23881
- join6(cliDir, "..", "gateway.js"),
23882
- join6(cliDir, "gateway.js")
24933
+ join7(root, "apps", "gateway", "src", "index.ts"),
24934
+ join7(cliDir, "..", "gateway.js"),
24935
+ join7(cliDir, "gateway.js")
23883
24936
  ];
23884
24937
  return candidates.find((path) => existsSync4(path)) ?? null;
23885
24938
  }
@@ -24016,6 +25069,15 @@ async function rollupState(ctx) {
24016
25069
  return null;
24017
25070
  }
24018
25071
  }
25072
+ async function orphanTables(ctx) {
25073
+ if (ctx.configError !== null || !existsSync5(ctx.databasePath))
25074
+ return null;
25075
+ try {
25076
+ return orphanPluginTables(doctorPluginDeps(), ctx.root.root, await ctx.store());
25077
+ } catch {
25078
+ return null;
25079
+ }
25080
+ }
24019
25081
  var doctor = {
24020
25082
  usage: "doctor",
24021
25083
  summary: "Check what this CLI resolved, and whether it can do anything with it",
@@ -24026,6 +25088,8 @@ var doctor = {
24026
25088
  const unit = unitInstalled(deps);
24027
25089
  const status2 = await status(deps);
24028
25090
  const usageRollup = await rollupState(ctx);
25091
+ const plugins = listPlugins(doctorPluginDeps(), deps.root);
25092
+ const orphans = await orphanTables(ctx);
24029
25093
  const checks3 = {
24030
25094
  root: deps.root,
24031
25095
  rootSource: ctx.root.source,
@@ -24040,6 +25104,9 @@ var doctor = {
24040
25104
  supervisor: status2.supervisor,
24041
25105
  running: status2.running,
24042
25106
  consoleSource: consoleSource(deps).source,
25107
+ pluginsDir: pluginsDir(deps.root),
25108
+ plugins,
25109
+ orphanPluginTables: orphans,
24043
25110
  warnings: ctx.warnings
24044
25111
  };
24045
25112
  emit(ctx, writer, checks3, () => {
@@ -24061,6 +25128,11 @@ var doctor = {
24061
25128
  ["systemd unit", unit ? deps.scope : paint(ctx, "dim", "none")],
24062
25129
  ["gateway", ok(status2.running, status2.running ? "running" : "stopped")],
24063
25130
  ["console log", sourceHint(checks3.consoleSource)],
25131
+ ...plugins.length === 0 ? [["plugins", paint(ctx, "dim", "none")]] : pluginDoctorLines(ctx, plugins).map((line, index) => [index === 0 ? "plugins" : "", line]),
25132
+ [
25133
+ "orphan plugin tables",
25134
+ orphans === null ? paint(ctx, "dim", "not checked") : orphans.length === 0 ? ok(true, "none") : paint(ctx, "yellow", `${orphans.length}: ${orphans.join(", ")}`)
25135
+ ],
24064
25136
  ...checks3.warnings.map((warning) => ["ignored", paint(ctx, "yellow", warning)])
24065
25137
  ]);
24066
25138
  });
@@ -24141,7 +25213,7 @@ var settingsSet = {
24141
25213
  };
24142
25214
 
24143
25215
  // apps/cli/src/commands/setup.ts
24144
- import { join as join7 } from "path";
25216
+ import { join as join8 } from "path";
24145
25217
  var OPTIONS = {
24146
25218
  dir: { type: "string" },
24147
25219
  key: { type: "string" },
@@ -24173,7 +25245,7 @@ ${f.contents}`).join(`
24173
25245
  }
24174
25246
  }
24175
25247
  function at(dir, file2) {
24176
- return { path: join7(dir, file2.path), contents: file2.contents };
25248
+ return { path: join8(dir, file2.path), contents: file2.contents };
24177
25249
  }
24178
25250
  async function promptMapping(models, prompt) {
24179
25251
  const choices = models.map(({ model }) => model.id).join(", ");
@@ -24207,11 +25279,11 @@ var setupClaude = {
24207
25279
  options: OPTIONS,
24208
25280
  async run(args, { ctx, writer, prompt, setupFs }) {
24209
25281
  const models = await described(ctx);
24210
- const dir = stringFlag(args.values, "dir") ?? join7(setupFs.homeDir, ".claude");
25282
+ const dir = stringFlag(args.values, "dir") ?? join8(setupFs.homeDir, ".claude");
24211
25283
  const key = stringFlag(args.values, "key");
24212
25284
  const dryRun2 = boolFlag(args.values, "dry-run");
24213
25285
  const mapping = await promptMapping(models, prompt);
24214
- const path = join7(dir, "settings.json");
25286
+ const path = join8(dir, "settings.json");
24215
25287
  const file2 = claudeSettings(models, {
24216
25288
  baseUrl: baseUrl(ctx),
24217
25289
  discoveryMirrors: ctx.config().exposeClaudeCodeAliases,
@@ -24466,6 +25538,10 @@ var COMMANDS = {
24466
25538
  "keys create": keysCreate,
24467
25539
  "keys limits": keysLimits,
24468
25540
  "keys revoke": keysRevoke,
25541
+ "plugin list": pluginList,
25542
+ "plugin verify": pluginVerify,
25543
+ "plugin install": pluginInstall,
25544
+ "plugin remove": pluginRemove,
24469
25545
  "settings get": settingsGet,
24470
25546
  "settings set": settingsSet,
24471
25547
  "setup claude": setupClaude,
@@ -24521,6 +25597,7 @@ var GROUPS = [
24521
25597
  { title: "Accounts", prefixes: ["connect", "credentials "] },
24522
25598
  { title: "Models", prefixes: ["models "] },
24523
25599
  { title: "Keys", prefixes: ["keys "] },
25600
+ { title: "Plugins", prefixes: ["plugin "] },
24524
25601
  { title: "Configuration", prefixes: ["settings ", "admin ", "db "] },
24525
25602
  { title: "Reports", prefixes: ["usage", "quota"] }
24526
25603
  ];
@@ -24577,10 +25654,10 @@ async function readHidden() {
24577
25654
  stdin.setRawMode?.(true);
24578
25655
  stdin.resume();
24579
25656
  try {
24580
- const decoder2 = new TextDecoder;
25657
+ const decoder3 = new TextDecoder;
24581
25658
  let line = "";
24582
25659
  for await (const chunk of stdin) {
24583
- const text = decoder2.decode(chunk, { stream: true });
25660
+ const text = decoder3.decode(chunk, { stream: true });
24584
25661
  for (const char of text) {
24585
25662
  if (char === "\r" || char === `
24586
25663
  `)
@@ -24601,10 +25678,10 @@ async function readHidden() {
24601
25678
  }
24602
25679
  }
24603
25680
  async function readLine() {
24604
- const decoder2 = new TextDecoder;
25681
+ const decoder3 = new TextDecoder;
24605
25682
  let line = "";
24606
25683
  for await (const chunk of process.stdin) {
24607
- const text = decoder2.decode(chunk, { stream: true });
25684
+ const text = decoder3.decode(chunk, { stream: true });
24608
25685
  const newline = text.indexOf(`
24609
25686
  `);
24610
25687
  if (newline >= 0)
@@ -24700,10 +25777,10 @@ async function run(argv, writer, options = {}) {
24700
25777
  setupFs: options.setupFs ?? {
24701
25778
  homeDir: homedir3(),
24702
25779
  cwd: process.cwd(),
24703
- read: (path) => existsSync6(path) ? readFileSync3(path, "utf8") : null,
25780
+ read: (path) => existsSync6(path) ? readFileSync4(path, "utf8") : null,
24704
25781
  write: (path, contents) => atomicWriteFile(path, contents, {
24705
25782
  mkdir: (directory) => mkdirSync3(directory, { recursive: true }),
24706
- write: (temporary, data) => writeFileSync2(temporary, data),
25783
+ write: (temporary, data) => writeFileSync3(temporary, data),
24707
25784
  rename: renameSync2,
24708
25785
  remove: (temporary) => rmSync3(temporary, { force: true })
24709
25786
  })