owui-cli 0.4.0__tar.gz → 0.5.0__tar.gz

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.
@@ -13,6 +13,10 @@ Bump both when cutting a release.
13
13
 
14
14
  Every push to `main` auto-publishes to PyPI via GitHub Actions trusted publishing. Bump the version before pushing, and don't push broken code to main.
15
15
 
16
+ ## Local development install
17
+
18
+ After making changes, reinstall locally with `uv tool install --force .` so your modified `owui-cli` is the one on your PATH.
19
+
16
20
  ## Testing before committing
17
21
 
18
22
  Always verify changes work against a **production Open WebUI server** before committing. Set `OWUI_URL` and `OWUI_TOKEN` and run the relevant commands to confirm they succeed with real API responses — don't rely on local-only reasoning.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: owui-cli
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: Admin CLI for Open WebUI instances
5
5
  Project-URL: Homepage, https://github.com/rndmcnlly/owui-cli
6
6
  Project-URL: Repository, https://github.com/rndmcnlly/owui-cli
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "owui-cli"
3
- version = "0.4.0"
3
+ version = "0.5.0"
4
4
  description = "Admin CLI for Open WebUI instances"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -0,0 +1 @@
1
+ __version__ = "0.5.0"
@@ -43,8 +43,8 @@ def _api(url: str, path: str) -> str:
43
43
  return f"{url}{path}"
44
44
 
45
45
 
46
- def _get(c: httpx.Client, url: str, path: str, token: str) -> httpx.Response:
47
- r = c.get(_api(url, path), headers=_headers(token))
46
+ def _get(c: httpx.Client, url: str, path: str, token: str, params=None) -> httpx.Response:
47
+ r = c.get(_api(url, path), headers=_headers(token), params=params)
48
48
  r.raise_for_status()
49
49
  return r
50
50
 
@@ -369,81 +369,79 @@ functions_res = Resource("functions", "/api/v1/functions",
369
369
  workspace_path="/workspace/functions")
370
370
 
371
371
 
372
- # ── valves (user valves for tools and functions) ─────────────────────
372
+ # ── valves (admin/global and per-user, for tools and functions) ──────
373
+ #
374
+ # OWUI plugins can declare two valve schemas:
375
+ # class Valves(BaseModel) -> /api/v1/{kind}/id/{id}/valves[/spec|/update]
376
+ # class UserValves(BaseModel) -> /api/v1/{kind}/id/{id}/valves/user[/spec|/update]
377
+ #
378
+ # The unprefixed `valves*` commands target the admin/global path (most common
379
+ # case: an admin-set API key). The `valves-user*` commands target the per-user
380
+ # path (the calling user's personal valves on the plugin).
373
381
 
374
- def _valves_get(url, token, kind, item_id):
375
- """GET valves for a tool or function. kind is 'tools' or 'functions'."""
382
+ def _valves_get(url, token, kind, item_id, scope=""):
376
383
  with httpx.Client(timeout=TIMEOUT) as c:
377
- r = _get(c, url, f"/api/v1/{kind}/id/{item_id}/valves/user", token)
384
+ r = _get(c, url, f"/api/v1/{kind}/id/{item_id}/valves{scope}", token)
378
385
  out(r.json())
379
386
 
380
- def _valves_spec(url, token, kind, item_id):
381
- """GET the UserValves spec (schema) for a tool or function."""
387
+ def _valves_spec(url, token, kind, item_id, scope=""):
382
388
  with httpx.Client(timeout=TIMEOUT) as c:
383
- r = _get(c, url, f"/api/v1/{kind}/id/{item_id}/valves/user/spec", token)
389
+ r = _get(c, url, f"/api/v1/{kind}/id/{item_id}/valves{scope}/spec", token)
384
390
  out(r.json())
385
391
 
386
- def _valves_set(url, token, kind, item_id, json_path):
387
- """POST updated user valves from a JSON file."""
392
+ def _valves_set(url, token, kind, item_id, json_path, scope=""):
388
393
  with open(json_path) as f:
389
394
  payload = json.load(f)
390
395
  with httpx.Client(timeout=TIMEOUT) as c:
391
- r = _post(c, url, f"/api/v1/{kind}/id/{item_id}/valves/user/update", token, payload)
396
+ r = _post(c, url, f"/api/v1/{kind}/id/{item_id}/valves{scope}/update", token, payload)
392
397
  out(r.json())
393
398
 
394
- def _valves_set_field(url, token, kind, item_id, key, value):
395
- """Set a single field in the user valves. Value is parsed as JSON; falls back to string."""
399
+ def _valves_set_field(url, token, kind, item_id, key, value, scope=""):
400
+ """Set a single field. Value is parsed as JSON; falls back to string."""
396
401
  try:
397
402
  parsed = json.loads(value)
398
403
  except (json.JSONDecodeError, ValueError):
399
404
  parsed = value
400
405
  with httpx.Client(timeout=TIMEOUT) as c:
401
- current = _get(c, url, f"/api/v1/{kind}/id/{item_id}/valves/user", token).json()
406
+ current = _get(c, url, f"/api/v1/{kind}/id/{item_id}/valves{scope}", token).json()
402
407
  current[key] = parsed
403
- r = _post(c, url, f"/api/v1/{kind}/id/{item_id}/valves/user/update", token, current)
408
+ r = _post(c, url, f"/api/v1/{kind}/id/{item_id}/valves{scope}/update", token, current)
404
409
  out(r.json())
405
410
 
406
- def _valves_unset_field(url, token, kind, item_id, key):
407
- """Remove a single field from the user valves."""
411
+ def _valves_unset_field(url, token, kind, item_id, key, scope=""):
408
412
  with httpx.Client(timeout=TIMEOUT) as c:
409
- current = _get(c, url, f"/api/v1/{kind}/id/{item_id}/valves/user", token).json()
413
+ current = _get(c, url, f"/api/v1/{kind}/id/{item_id}/valves{scope}", token).json()
410
414
  if key not in current:
411
415
  die(f"key '{key}' not found in valves")
412
416
  del current[key]
413
- r = _post(c, url, f"/api/v1/{kind}/id/{item_id}/valves/user/update", token, current)
417
+ r = _post(c, url, f"/api/v1/{kind}/id/{item_id}/valves{scope}/update", token, current)
414
418
  out(r.json())
415
419
 
416
- # Wrappers that bind kind='tools'
417
- def tools_valves_get(url, token, item_id):
418
- _valves_get(url, token, "tools", item_id)
419
-
420
- def tools_valves_spec(url, token, item_id):
421
- _valves_spec(url, token, "tools", item_id)
422
-
423
- def tools_valves_set(url, token, item_id, json_path):
424
- _valves_set(url, token, "tools", item_id, json_path)
425
-
426
- def tools_valves_set_field(url, token, item_id, key, value):
427
- _valves_set_field(url, token, "tools", item_id, key, value)
428
-
429
- def tools_valves_unset_field(url, token, item_id, key):
430
- _valves_unset_field(url, token, "tools", item_id, key)
431
-
432
- # Wrappers that bind kind='functions'
433
- def functions_valves_get(url, token, item_id):
434
- _valves_get(url, token, "functions", item_id)
435
-
436
- def functions_valves_spec(url, token, item_id):
437
- _valves_spec(url, token, "functions", item_id)
438
-
439
- def functions_valves_set(url, token, item_id, json_path):
440
- _valves_set(url, token, "functions", item_id, json_path)
441
-
442
- def functions_valves_set_field(url, token, item_id, key, value):
443
- _valves_set_field(url, token, "functions", item_id, key, value)
444
-
445
- def functions_valves_unset_field(url, token, item_id, key):
446
- _valves_unset_field(url, token, "functions", item_id, key)
420
+ # Wrappers admin/global valves (unprefixed: the common case)
421
+ def tools_valves_get(url, token, item_id): _valves_get(url, token, "tools", item_id)
422
+ def tools_valves_spec(url, token, item_id): _valves_spec(url, token, "tools", item_id)
423
+ def tools_valves_set(url, token, item_id, json_path): _valves_set(url, token, "tools", item_id, json_path)
424
+ def tools_valves_set_field(url, token, item_id, key, value):_valves_set_field(url, token, "tools", item_id, key, value)
425
+ def tools_valves_unset_field(url, token, item_id, key): _valves_unset_field(url, token, "tools", item_id, key)
426
+
427
+ def functions_valves_get(url, token, item_id): _valves_get(url, token, "functions", item_id)
428
+ def functions_valves_spec(url, token, item_id): _valves_spec(url, token, "functions", item_id)
429
+ def functions_valves_set(url, token, item_id, json_path): _valves_set(url, token, "functions", item_id, json_path)
430
+ def functions_valves_set_field(url, token, item_id, key, value):_valves_set_field(url, token, "functions", item_id, key, value)
431
+ def functions_valves_unset_field(url, token, item_id, key): _valves_unset_field(url, token, "functions", item_id, key)
432
+
433
+ # Wrappers per-user valves (`valves-user*`)
434
+ def tools_valves_user_get(url, token, item_id): _valves_get(url, token, "tools", item_id, "/user")
435
+ def tools_valves_user_spec(url, token, item_id): _valves_spec(url, token, "tools", item_id, "/user")
436
+ def tools_valves_user_set(url, token, item_id, json_path): _valves_set(url, token, "tools", item_id, json_path, "/user")
437
+ def tools_valves_user_set_field(url, token, item_id, key, value):_valves_set_field(url, token, "tools", item_id, key, value, "/user")
438
+ def tools_valves_user_unset_field(url, token, item_id, key): _valves_unset_field(url, token, "tools", item_id, key, "/user")
439
+
440
+ def functions_valves_user_get(url, token, item_id): _valves_get(url, token, "functions", item_id, "/user")
441
+ def functions_valves_user_spec(url, token, item_id): _valves_spec(url, token, "functions", item_id, "/user")
442
+ def functions_valves_user_set(url, token, item_id, json_path): _valves_set(url, token, "functions", item_id, json_path, "/user")
443
+ def functions_valves_user_set_field(url, token, item_id, key, value):_valves_set_field(url, token, "functions", item_id, key, value, "/user")
444
+ def functions_valves_user_unset_field(url, token, item_id, key): _valves_unset_field(url, token, "functions", item_id, key, "/user")
447
445
 
448
446
 
449
447
  class SkillsResource(Resource):
@@ -691,9 +689,20 @@ def models_pull_all(url, token, out_dir="."):
691
689
  # ── knowledge (special: files subresource, file/remove is destructive) ─
692
690
 
693
691
  def knowledge_list(url, token):
692
+ kbs = []
693
+ page = 1
694
694
  with httpx.Client(timeout=TIMEOUT) as c:
695
- data = _get(c, url, "/api/v1/knowledge/", token).json()
696
- kbs = data if isinstance(data, list) else data.get("items", [])
695
+ while True:
696
+ data = _get(c, url, "/api/v1/knowledge/", token, params={"page": page}).json()
697
+ batch = data["items"] if isinstance(data, dict) else data
698
+ if not batch:
699
+ break
700
+ kbs.extend(batch)
701
+ if isinstance(data, dict):
702
+ if len(kbs) >= data.get("total", 0): break
703
+ else:
704
+ break
705
+ page += 1
697
706
  rows = [{"id": k.get("id",""), "name": k.get("name",""),
698
707
  "desc": (k.get("description") or "")[:50]}
699
708
  for k in sorted(kbs, key=lambda k: k.get("name",""))]
@@ -744,12 +753,25 @@ def knowledge_remove_file(url, token, kb_id, file_id):
744
753
  # ── files ─────────────────────────────────────────────────────────────
745
754
 
746
755
  def files_list(url, token):
756
+ rows = []
757
+ page = 1
747
758
  with httpx.Client(timeout=TIMEOUT) as c:
748
- data = _get(c, url, "/api/v1/files/", token).json()
749
- rows = [{"id": f.get("id",""),
750
- "name": (f.get("meta") or {}).get("name") or f.get("filename",""),
751
- "size": str((f.get("meta") or {}).get("size","?"))}
752
- for f in data]
759
+ while True:
760
+ data = _get(c, url, "/api/v1/files/", token,
761
+ params={"page": page, "content": "false"}).json()
762
+ # OWUI >= 0.9 returns {items, total}; older versions return a bare list.
763
+ batch = data["items"] if isinstance(data, dict) else data
764
+ if not batch:
765
+ break
766
+ rows.extend({"id": f.get("id",""),
767
+ "name": (f.get("meta") or {}).get("name") or f.get("filename",""),
768
+ "size": str((f.get("meta") or {}).get("size","?"))}
769
+ for f in batch)
770
+ if isinstance(data, dict):
771
+ if len(rows) >= data.get("total", 0): break
772
+ else:
773
+ break # bare-list response is unpaginated
774
+ page += 1
753
775
  out_table(rows, [("FILE_ID","id",36), ("NAME","name",30), ("SIZE","size",8)])
754
776
 
755
777
  def files_show(url, token, file_id):
@@ -1119,11 +1141,21 @@ COMMANDS.update({
1119
1141
  ("tools", "valves-set"): (tools_valves_set, "<id> <valves.json>", (2, 2)),
1120
1142
  ("tools", "valves-set-field"): (tools_valves_set_field, "<id> <key> <value>", (3, 3)),
1121
1143
  ("tools", "valves-unset-field"):(tools_valves_unset_field, "<id> <key>", (2, 2)),
1144
+ ("tools", "valves-user"): (tools_valves_user_get, "<id>", (1, 1)),
1145
+ ("tools", "valves-user-spec"): (tools_valves_user_spec, "<id>", (1, 1)),
1146
+ ("tools", "valves-user-set"): (tools_valves_user_set, "<id> <valves.json>", (2, 2)),
1147
+ ("tools", "valves-user-set-field"): (tools_valves_user_set_field, "<id> <key> <value>", (3, 3)),
1148
+ ("tools", "valves-user-unset-field"):(tools_valves_user_unset_field, "<id> <key>", (2, 2)),
1122
1149
  ("functions", "valves"): (functions_valves_get, "<id>", (1, 1)),
1123
1150
  ("functions", "valves-spec"): (functions_valves_spec, "<id>", (1, 1)),
1124
1151
  ("functions", "valves-set"): (functions_valves_set, "<id> <valves.json>", (2, 2)),
1125
1152
  ("functions", "valves-set-field"): (functions_valves_set_field, "<id> <key> <value>", (3, 3)),
1126
1153
  ("functions", "valves-unset-field"):(functions_valves_unset_field, "<id> <key>", (2, 2)),
1154
+ ("functions", "valves-user"): (functions_valves_user_get, "<id>", (1, 1)),
1155
+ ("functions", "valves-user-spec"): (functions_valves_user_spec, "<id>", (1, 1)),
1156
+ ("functions", "valves-user-set"): (functions_valves_user_set, "<id> <valves.json>", (2, 2)),
1157
+ ("functions", "valves-user-set-field"): (functions_valves_user_set_field, "<id> <key> <value>", (3, 3)),
1158
+ ("functions", "valves-user-unset-field"):(functions_valves_user_unset_field, "<id> <key>", (2, 2)),
1127
1159
  ("models", "list"): (models_list, "", (0, 0)),
1128
1160
  ("models", "show"): (models_show, "<id>", (1, 1)),
1129
1161
  ("models", "create"): (models_create, "<model.json>", (1, 1)),
@@ -1217,7 +1249,12 @@ def main():
1217
1249
  resource, command = args[0], args[1]
1218
1250
  key = (resource, command)
1219
1251
 
1252
+ # Subcommands are plural (chats, models, etc.) to mirror the OWUI API
1253
+ # paths (/api/v1/chats/, /api/v1/models, ...), not typical CLI convention.
1220
1254
  if key not in COMMANDS:
1255
+ plural = resource + "s"
1256
+ if (plural, command) in COMMANDS:
1257
+ die(f"resources are plural: use '{plural} {command}' not '{resource} {command}'")
1221
1258
  die(f"unknown: {resource} {command}")
1222
1259
 
1223
1260
  fn, arg_spec, (min_args, max_args) = COMMANDS[key]
owui_cli-0.5.0/uv.lock ADDED
@@ -0,0 +1,91 @@
1
+ version = 1
2
+ revision = 2
3
+ requires-python = ">=3.11"
4
+
5
+ [[package]]
6
+ name = "anyio"
7
+ version = "4.13.0"
8
+ source = { registry = "https://pypi.org/simple" }
9
+ dependencies = [
10
+ { name = "idna" },
11
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
12
+ ]
13
+ sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" }
14
+ wheels = [
15
+ { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" },
16
+ ]
17
+
18
+ [[package]]
19
+ name = "certifi"
20
+ version = "2026.2.25"
21
+ source = { registry = "https://pypi.org/simple" }
22
+ sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" }
23
+ wheels = [
24
+ { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" },
25
+ ]
26
+
27
+ [[package]]
28
+ name = "h11"
29
+ version = "0.16.0"
30
+ source = { registry = "https://pypi.org/simple" }
31
+ sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
32
+ wheels = [
33
+ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
34
+ ]
35
+
36
+ [[package]]
37
+ name = "httpcore"
38
+ version = "1.0.9"
39
+ source = { registry = "https://pypi.org/simple" }
40
+ dependencies = [
41
+ { name = "certifi" },
42
+ { name = "h11" },
43
+ ]
44
+ sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
45
+ wheels = [
46
+ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
47
+ ]
48
+
49
+ [[package]]
50
+ name = "httpx"
51
+ version = "0.28.1"
52
+ source = { registry = "https://pypi.org/simple" }
53
+ dependencies = [
54
+ { name = "anyio" },
55
+ { name = "certifi" },
56
+ { name = "httpcore" },
57
+ { name = "idna" },
58
+ ]
59
+ sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
60
+ wheels = [
61
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
62
+ ]
63
+
64
+ [[package]]
65
+ name = "idna"
66
+ version = "3.11"
67
+ source = { registry = "https://pypi.org/simple" }
68
+ sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
69
+ wheels = [
70
+ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
71
+ ]
72
+
73
+ [[package]]
74
+ name = "owui-cli"
75
+ version = "0.2.0"
76
+ source = { editable = "." }
77
+ dependencies = [
78
+ { name = "httpx" },
79
+ ]
80
+
81
+ [package.metadata]
82
+ requires-dist = [{ name = "httpx", specifier = ">=0.27" }]
83
+
84
+ [[package]]
85
+ name = "typing-extensions"
86
+ version = "4.15.0"
87
+ source = { registry = "https://pypi.org/simple" }
88
+ sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
89
+ wheels = [
90
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
91
+ ]
@@ -1 +0,0 @@
1
- __version__ = "0.4.0"
@@ -1,103 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>OWUI Token Viewer</title>
7
- <style>
8
- * { box-sizing: border-box; margin: 0; padding: 0; }
9
- body {
10
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
11
- background: #1a1a2e;
12
- color: #eee;
13
- min-height: 100vh;
14
- display: flex;
15
- align-items: center;
16
- justify-content: center;
17
- padding: 1rem;
18
- }
19
- .card {
20
- background: #16213e;
21
- border-radius: 12px;
22
- padding: 1.5rem;
23
- max-width: 480px;
24
- width: 100%;
25
- box-shadow: 0 8px 32px rgba(0,0,0,.4);
26
- }
27
- h1 { font-size: 1.25rem; margin-bottom: .25rem; }
28
- .sub { color: #888; font-size: .85rem; margin-bottom: 1rem; }
29
- .token-box {
30
- background: #0f0f23;
31
- border: 1px solid #333;
32
- border-radius: 8px;
33
- padding: .75rem;
34
- word-break: break-all;
35
- font-family: "SF Mono", "Fira Code", monospace;
36
- font-size: .8rem;
37
- line-height: 1.5;
38
- max-height: 40vh;
39
- overflow-y: auto;
40
- user-select: all;
41
- -webkit-user-select: all;
42
- }
43
- .token-box.empty { color: #f55; font-family: sans-serif; font-size: .9rem; }
44
- .btn {
45
- display: inline-block;
46
- margin-top: .75rem;
47
- padding: .6rem 1.2rem;
48
- background: #0f3460;
49
- color: #eee;
50
- border: none;
51
- border-radius: 8px;
52
- font-size: .9rem;
53
- cursor: pointer;
54
- -webkit-tap-highlight-color: transparent;
55
- }
56
- .btn:active { background: #1a5276; }
57
- .copied { color: #5f8; font-size: .85rem; margin-left: .5rem; opacity: 0; transition: opacity .2s; }
58
- .copied.show { opacity: 1; }
59
- </style>
60
- </head>
61
- <body>
62
- <div class="card">
63
- <h1>🔑 OWUI Token</h1>
64
- <p class="sub">Reads <code>token</code> from localStorage</p>
65
- <div id="token" class="token-box empty">Checking…</div>
66
- <button class="btn" id="copy" style="display:none">Copy to clipboard</button>
67
- <span class="copied" id="copied">✓ Copied!</span>
68
- </div>
69
- <script>
70
- const el = document.getElementById('token');
71
- const copyBtn = document.getElementById('copy');
72
- const copiedMsg = document.getElementById('copied');
73
- const raw = localStorage.getItem('token');
74
-
75
- if (raw) {
76
- el.textContent = raw;
77
- el.classList.remove('empty');
78
- copyBtn.style.display = 'inline-block';
79
- } else {
80
- el.textContent = 'No "token" found in localStorage.\nMake sure you\'re on the same origin as Open WebUI.';
81
- }
82
-
83
- copyBtn.addEventListener('click', () => {
84
- navigator.clipboard.writeText(raw).then(() => {
85
- copiedMsg.classList.add('show');
86
- setTimeout(() => copiedMsg.classList.remove('show'), 1500);
87
- }).catch(() => {
88
- // fallback for older mobile browsers
89
- const ta = document.createElement('textarea');
90
- ta.value = raw;
91
- ta.style.position = 'fixed';
92
- ta.style.opacity = 0;
93
- document.body.appendChild(ta);
94
- ta.select();
95
- document.execCommand('copy');
96
- document.body.removeChild(ta);
97
- copiedMsg.classList.add('show');
98
- setTimeout(() => copiedMsg.classList.remove('show'), 1500);
99
- });
100
- });
101
- </script>
102
- </body>
103
- </html>
File without changes
File without changes
File without changes
File without changes