owui-cli 0.4.0__tar.gz → 0.5.1__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.1
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.1"
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.1"
@@ -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,96 @@ 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)
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")
445
+
446
+
447
+ # ── functions toggle (activate / deactivate, and global) ─────────────
448
+
449
+ def functions_toggle(url, token, item_id):
450
+ """Toggle a function's is_active flag."""
451
+ with httpx.Client(timeout=TIMEOUT) as c:
452
+ r = _post(c, url, f"/api/v1/functions/id/{item_id}/toggle", token)
453
+ f = r.json()
454
+ out(f"{item_id} {'active' if f.get('is_active') else 'inactive'}")
444
455
 
445
- def functions_valves_unset_field(url, token, item_id, key):
446
- _valves_unset_field(url, token, "functions", item_id, key)
456
+ def functions_toggle_global(url, token, item_id):
457
+ """Toggle a function's is_global flag (applies to all models)."""
458
+ with httpx.Client(timeout=TIMEOUT) as c:
459
+ r = _post(c, url, f"/api/v1/functions/id/{item_id}/toggle/global", token)
460
+ f = r.json()
461
+ out(f"{item_id} {'global' if f.get('is_global') else 'not global'}")
447
462
 
448
463
 
449
464
  class SkillsResource(Resource):
@@ -575,17 +590,16 @@ def models_show(url, token, model_id):
575
590
  if JSON_OUTPUT:
576
591
  out(m)
577
592
  return
578
- info = m.get("info") or {}
579
- meta = info.get("meta") or {}
580
- params = info.get("params") or {}
593
+ meta = m.get("meta") or {}
594
+ params = m.get("params") or {}
581
595
  pairs = [("id", m.get("id","")), ("name", m.get("name","")),
582
- ("base", info.get("base_model_id","(none)")),
583
- ("active", str(info.get("is_active","?"))),
596
+ ("base", m.get("base_model_id","(none)") or "(none)"),
597
+ ("active", str(m.get("is_active","?"))),
584
598
  ("tools", ", ".join(meta.get("toolIds") or []) or "(none)"),
585
- ("filters", ", ".join(params.get("filter_ids") or []) or "(none)"),
599
+ ("filters", ", ".join(meta.get("filterIds") or []) or "(none)"),
586
600
  ("knowledge", ", ".join(k.get("name","?") for k in (meta.get("knowledge") or [])) or "(none)"),
587
- ("system", f"{len(params.get('system',''))} chars"),
588
- ("grants", str(len(info.get("access_grants") or [])))]
601
+ ("system", f"{len(params.get('system') or '')} chars"),
602
+ ("grants", str(len(m.get("access_grants") or [])))]
589
603
  out_kv(pairs)
590
604
 
591
605
  def models_create(url, token, json_path):
@@ -609,23 +623,38 @@ def models_delete(url, token, model_id):
609
623
  out(f"deleted {model_id}")
610
624
 
611
625
  def _models_fetch(c, url, token, model_id):
612
- """Fetch a model by ID, returning the parsed JSON."""
626
+ """Fetch a model by ID, returning the parsed JSON (flat ModelModel shape)."""
613
627
  r = _get(c, url, f"/api/v1/models/model?id={model_id}", token)
614
628
  return r.json()
615
629
 
630
+ def _models_form(model):
631
+ """Build a ModelForm update payload from a fetched flat model.
632
+
633
+ OWUI's Model schema is flat: top-level `meta`, `params`, `base_model_id`,
634
+ `name`, `is_active`, `access_grants`. The /model/update endpoint accepts a
635
+ ModelForm of the same shape (extra keys ignored). Rebuilding explicitly
636
+ avoids leaking response-only fields (user, write_access, timestamps) and
637
+ ensures `meta`/`params` round-trip intact.
638
+ """
639
+ return {
640
+ "id": model.get("id"),
641
+ "base_model_id": model.get("base_model_id"),
642
+ "name": model.get("name", model.get("id", "")),
643
+ "meta": model.get("meta") or {},
644
+ "params": model.get("params") or {},
645
+ "access_grants": model.get("access_grants") or [],
646
+ "is_active": model.get("is_active", True),
647
+ }
648
+
616
649
  def models_set_tools(url, token, model_id, *tool_ids):
617
650
  """Set the tool bindings for a workspace model (pass no IDs to clear)."""
618
651
  with httpx.Client(timeout=TIMEOUT) as c:
619
652
  model = _models_fetch(c, url, token, model_id)
620
- info = model.get("info") or {}
621
- meta = info.setdefault("meta", {})
622
- params = info.setdefault("params", {})
653
+ form = _models_form(model)
623
654
  ids = list(tool_ids)
624
- meta["toolIds"] = ids
625
- # keep params.tool_ids in sync (used by some OWUI versions)
626
- params["tool_ids"] = ids
627
- model["info"] = info
628
- r = _post(c, url, "/api/v1/models/model/update", token, model)
655
+ # OWUI binds per-model tools via meta.toolIds.
656
+ form["meta"]["toolIds"] = ids
657
+ r = _post(c, url, "/api/v1/models/model/update", token, form)
629
658
  label = ", ".join(ids) if ids else "(none)"
630
659
  out(f"tools for {model_id}: {label}")
631
660
 
@@ -633,12 +662,12 @@ def models_set_filters(url, token, model_id, *filter_ids):
633
662
  """Set the filter bindings for a workspace model (pass no IDs to clear)."""
634
663
  with httpx.Client(timeout=TIMEOUT) as c:
635
664
  model = _models_fetch(c, url, token, model_id)
636
- info = model.get("info") or {}
637
- params = info.setdefault("params", {})
665
+ form = _models_form(model)
638
666
  ids = list(filter_ids)
639
- params["filter_ids"] = ids
640
- model["info"] = info
641
- r = _post(c, url, "/api/v1/models/model/update", token, model)
667
+ # OWUI reads per-model filters from meta.filterIds
668
+ # (see backend/open_webui/utils/filter.py).
669
+ form["meta"]["filterIds"] = ids
670
+ r = _post(c, url, "/api/v1/models/model/update", token, form)
642
671
  label = ", ".join(ids) if ids else "(none)"
643
672
  out(f"filters for {model_id}: {label}")
644
673
 
@@ -691,9 +720,20 @@ def models_pull_all(url, token, out_dir="."):
691
720
  # ── knowledge (special: files subresource, file/remove is destructive) ─
692
721
 
693
722
  def knowledge_list(url, token):
723
+ kbs = []
724
+ page = 1
694
725
  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", [])
726
+ while True:
727
+ data = _get(c, url, "/api/v1/knowledge/", token, params={"page": page}).json()
728
+ batch = data["items"] if isinstance(data, dict) else data
729
+ if not batch:
730
+ break
731
+ kbs.extend(batch)
732
+ if isinstance(data, dict):
733
+ if len(kbs) >= data.get("total", 0): break
734
+ else:
735
+ break
736
+ page += 1
697
737
  rows = [{"id": k.get("id",""), "name": k.get("name",""),
698
738
  "desc": (k.get("description") or "")[:50]}
699
739
  for k in sorted(kbs, key=lambda k: k.get("name",""))]
@@ -744,12 +784,25 @@ def knowledge_remove_file(url, token, kb_id, file_id):
744
784
  # ── files ─────────────────────────────────────────────────────────────
745
785
 
746
786
  def files_list(url, token):
787
+ rows = []
788
+ page = 1
747
789
  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]
790
+ while True:
791
+ data = _get(c, url, "/api/v1/files/", token,
792
+ params={"page": page, "content": "false"}).json()
793
+ # OWUI >= 0.9 returns {items, total}; older versions return a bare list.
794
+ batch = data["items"] if isinstance(data, dict) else data
795
+ if not batch:
796
+ break
797
+ rows.extend({"id": f.get("id",""),
798
+ "name": (f.get("meta") or {}).get("name") or f.get("filename",""),
799
+ "size": str((f.get("meta") or {}).get("size","?"))}
800
+ for f in batch)
801
+ if isinstance(data, dict):
802
+ if len(rows) >= data.get("total", 0): break
803
+ else:
804
+ break # bare-list response is unpaginated
805
+ page += 1
753
806
  out_table(rows, [("FILE_ID","id",36), ("NAME","name",30), ("SIZE","size",8)])
754
807
 
755
808
  def files_show(url, token, file_id):
@@ -1119,11 +1172,23 @@ COMMANDS.update({
1119
1172
  ("tools", "valves-set"): (tools_valves_set, "<id> <valves.json>", (2, 2)),
1120
1173
  ("tools", "valves-set-field"): (tools_valves_set_field, "<id> <key> <value>", (3, 3)),
1121
1174
  ("tools", "valves-unset-field"):(tools_valves_unset_field, "<id> <key>", (2, 2)),
1175
+ ("tools", "valves-user"): (tools_valves_user_get, "<id>", (1, 1)),
1176
+ ("tools", "valves-user-spec"): (tools_valves_user_spec, "<id>", (1, 1)),
1177
+ ("tools", "valves-user-set"): (tools_valves_user_set, "<id> <valves.json>", (2, 2)),
1178
+ ("tools", "valves-user-set-field"): (tools_valves_user_set_field, "<id> <key> <value>", (3, 3)),
1179
+ ("tools", "valves-user-unset-field"):(tools_valves_user_unset_field, "<id> <key>", (2, 2)),
1122
1180
  ("functions", "valves"): (functions_valves_get, "<id>", (1, 1)),
1123
1181
  ("functions", "valves-spec"): (functions_valves_spec, "<id>", (1, 1)),
1124
1182
  ("functions", "valves-set"): (functions_valves_set, "<id> <valves.json>", (2, 2)),
1125
1183
  ("functions", "valves-set-field"): (functions_valves_set_field, "<id> <key> <value>", (3, 3)),
1126
1184
  ("functions", "valves-unset-field"):(functions_valves_unset_field, "<id> <key>", (2, 2)),
1185
+ ("functions", "valves-user"): (functions_valves_user_get, "<id>", (1, 1)),
1186
+ ("functions", "valves-user-spec"): (functions_valves_user_spec, "<id>", (1, 1)),
1187
+ ("functions", "valves-user-set"): (functions_valves_user_set, "<id> <valves.json>", (2, 2)),
1188
+ ("functions", "valves-user-set-field"): (functions_valves_user_set_field, "<id> <key> <value>", (3, 3)),
1189
+ ("functions", "valves-user-unset-field"):(functions_valves_user_unset_field, "<id> <key>", (2, 2)),
1190
+ ("functions", "toggle"): (functions_toggle, "<id>", (1, 1)),
1191
+ ("functions", "toggle-global"): (functions_toggle_global, "<id>", (1, 1)),
1127
1192
  ("models", "list"): (models_list, "", (0, 0)),
1128
1193
  ("models", "show"): (models_show, "<id>", (1, 1)),
1129
1194
  ("models", "create"): (models_create, "<model.json>", (1, 1)),
@@ -1217,7 +1282,12 @@ def main():
1217
1282
  resource, command = args[0], args[1]
1218
1283
  key = (resource, command)
1219
1284
 
1285
+ # Subcommands are plural (chats, models, etc.) to mirror the OWUI API
1286
+ # paths (/api/v1/chats/, /api/v1/models, ...), not typical CLI convention.
1220
1287
  if key not in COMMANDS:
1288
+ plural = resource + "s"
1289
+ if (plural, command) in COMMANDS:
1290
+ die(f"resources are plural: use '{plural} {command}' not '{resource} {command}'")
1221
1291
  die(f"unknown: {resource} {command}")
1222
1292
 
1223
1293
  fn, arg_spec, (min_args, max_args) = COMMANDS[key]
owui_cli-0.5.1/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