owui-cli 0.2.0__tar.gz → 0.4.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.
@@ -1,3 +1,4 @@
1
+ .env
1
2
  __pycache__/
2
3
  *.pyc
3
4
  dist/
@@ -0,0 +1,45 @@
1
+ # AGENTS.md — owui-cli
2
+
3
+ ## Version management
4
+
5
+ The version number appears in two places and **must be kept in sync**:
6
+
7
+ - `pyproject.toml` → `version = "X.Y.Z"`
8
+ - `src/owui_cli/__init__.py` → `__version__ = "X.Y.Z"`
9
+
10
+ Bump both when cutting a release.
11
+
12
+ ## Publishing
13
+
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
+
16
+ ## Testing before committing
17
+
18
+ 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.
19
+
20
+ ## Architecture
21
+
22
+ Single-file CLI in `src/owui_cli/cli.py`. The `Resource` base class handles generic CRUD (list, show, deploy, pull, pull-all, delete) for tools, functions, and skills. Special resources (models, knowledge, files, groups, users, chats, configs, prompts) use standalone functions. All commands are registered in the `COMMANDS` dispatch table at the bottom of the file.
23
+
24
+ ## API endpoint patterns
25
+
26
+ The Open WebUI API is not fully consistent across resources:
27
+
28
+ - **Tools/functions/skills:** `/api/v1/{resource}/id/{id}`
29
+ - **Models:** `/api/v1/models/model?id={id}`
30
+ - **Knowledge/users:** `/api/v1/{resource}/{id}`
31
+ - **Groups:** `/api/v1/groups/id/{id}`
32
+
33
+ Always verify the actual endpoint against a running instance when adding new commands.
34
+
35
+ ## Output conventions
36
+
37
+ All commands support `--json` for machine-readable output. New commands should use the existing output helpers which handle JSON mode automatically:
38
+
39
+ - `out(data)` — generic output (JSON in `--json` mode, pretty-printed otherwise)
40
+ - `out_table(rows, cols)` — aligned columnar table
41
+ - `out_kv(pairs)` — key-value display
42
+
43
+ ## Schema updates
44
+
45
+ `update-schema.py` fetches OWUI router sources from GitHub and generates a prompt to feed an LLM. The LLM output goes in `src/owui_cli/data/api-schema.json`. See the script header for the full workflow.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: owui-cli
3
- Version: 0.2.0
3
+ Version: 0.4.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.2.0"
3
+ version = "0.4.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.4.0"
@@ -369,6 +369,83 @@ 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) ─────────────────────
373
+
374
+ def _valves_get(url, token, kind, item_id):
375
+ """GET valves for a tool or function. kind is 'tools' or 'functions'."""
376
+ with httpx.Client(timeout=TIMEOUT) as c:
377
+ r = _get(c, url, f"/api/v1/{kind}/id/{item_id}/valves/user", token)
378
+ out(r.json())
379
+
380
+ def _valves_spec(url, token, kind, item_id):
381
+ """GET the UserValves spec (schema) for a tool or function."""
382
+ with httpx.Client(timeout=TIMEOUT) as c:
383
+ r = _get(c, url, f"/api/v1/{kind}/id/{item_id}/valves/user/spec", token)
384
+ out(r.json())
385
+
386
+ def _valves_set(url, token, kind, item_id, json_path):
387
+ """POST updated user valves from a JSON file."""
388
+ with open(json_path) as f:
389
+ payload = json.load(f)
390
+ with httpx.Client(timeout=TIMEOUT) as c:
391
+ r = _post(c, url, f"/api/v1/{kind}/id/{item_id}/valves/user/update", token, payload)
392
+ out(r.json())
393
+
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."""
396
+ try:
397
+ parsed = json.loads(value)
398
+ except (json.JSONDecodeError, ValueError):
399
+ parsed = value
400
+ with httpx.Client(timeout=TIMEOUT) as c:
401
+ current = _get(c, url, f"/api/v1/{kind}/id/{item_id}/valves/user", token).json()
402
+ current[key] = parsed
403
+ r = _post(c, url, f"/api/v1/{kind}/id/{item_id}/valves/user/update", token, current)
404
+ out(r.json())
405
+
406
+ def _valves_unset_field(url, token, kind, item_id, key):
407
+ """Remove a single field from the user valves."""
408
+ with httpx.Client(timeout=TIMEOUT) as c:
409
+ current = _get(c, url, f"/api/v1/{kind}/id/{item_id}/valves/user", token).json()
410
+ if key not in current:
411
+ die(f"key '{key}' not found in valves")
412
+ del current[key]
413
+ r = _post(c, url, f"/api/v1/{kind}/id/{item_id}/valves/user/update", token, current)
414
+ out(r.json())
415
+
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)
447
+
448
+
372
449
  class SkillsResource(Resource):
373
450
  """Skills use frontmatter and have grant/revoke commands."""
374
451
 
@@ -505,6 +582,7 @@ def models_show(url, token, model_id):
505
582
  ("base", info.get("base_model_id","(none)")),
506
583
  ("active", str(info.get("is_active","?"))),
507
584
  ("tools", ", ".join(meta.get("toolIds") or []) or "(none)"),
585
+ ("filters", ", ".join(params.get("filter_ids") or []) or "(none)"),
508
586
  ("knowledge", ", ".join(k.get("name","?") for k in (meta.get("knowledge") or [])) or "(none)"),
509
587
  ("system", f"{len(params.get('system',''))} chars"),
510
588
  ("grants", str(len(info.get("access_grants") or [])))]
@@ -530,6 +608,40 @@ def models_delete(url, token, model_id):
530
608
  _post(c, url, "/api/v1/models/model/delete", token, {"id": model_id})
531
609
  out(f"deleted {model_id}")
532
610
 
611
+ def _models_fetch(c, url, token, model_id):
612
+ """Fetch a model by ID, returning the parsed JSON."""
613
+ r = _get(c, url, f"/api/v1/models/model?id={model_id}", token)
614
+ return r.json()
615
+
616
+ def models_set_tools(url, token, model_id, *tool_ids):
617
+ """Set the tool bindings for a workspace model (pass no IDs to clear)."""
618
+ with httpx.Client(timeout=TIMEOUT) as c:
619
+ model = _models_fetch(c, url, token, model_id)
620
+ info = model.get("info") or {}
621
+ meta = info.setdefault("meta", {})
622
+ params = info.setdefault("params", {})
623
+ 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)
629
+ label = ", ".join(ids) if ids else "(none)"
630
+ out(f"tools for {model_id}: {label}")
631
+
632
+ def models_set_filters(url, token, model_id, *filter_ids):
633
+ """Set the filter bindings for a workspace model (pass no IDs to clear)."""
634
+ with httpx.Client(timeout=TIMEOUT) as c:
635
+ model = _models_fetch(c, url, token, model_id)
636
+ info = model.get("info") or {}
637
+ params = info.setdefault("params", {})
638
+ 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)
642
+ label = ", ".join(ids) if ids else "(none)"
643
+ out(f"filters for {model_id}: {label}")
644
+
533
645
 
534
646
  def models_pull_all(url, token, out_dir="."):
535
647
  """Pull all workspace models into <out_dir>/<id>/model.json, extracting profile images."""
@@ -1002,11 +1114,23 @@ for res in [tools_res, functions_res, skills_res]:
1002
1114
 
1003
1115
  # Register special resources
1004
1116
  COMMANDS.update({
1117
+ ("tools", "valves"): (tools_valves_get, "<id>", (1, 1)),
1118
+ ("tools", "valves-spec"): (tools_valves_spec, "<id>", (1, 1)),
1119
+ ("tools", "valves-set"): (tools_valves_set, "<id> <valves.json>", (2, 2)),
1120
+ ("tools", "valves-set-field"): (tools_valves_set_field, "<id> <key> <value>", (3, 3)),
1121
+ ("tools", "valves-unset-field"):(tools_valves_unset_field, "<id> <key>", (2, 2)),
1122
+ ("functions", "valves"): (functions_valves_get, "<id>", (1, 1)),
1123
+ ("functions", "valves-spec"): (functions_valves_spec, "<id>", (1, 1)),
1124
+ ("functions", "valves-set"): (functions_valves_set, "<id> <valves.json>", (2, 2)),
1125
+ ("functions", "valves-set-field"): (functions_valves_set_field, "<id> <key> <value>", (3, 3)),
1126
+ ("functions", "valves-unset-field"):(functions_valves_unset_field, "<id> <key>", (2, 2)),
1005
1127
  ("models", "list"): (models_list, "", (0, 0)),
1006
1128
  ("models", "show"): (models_show, "<id>", (1, 1)),
1007
1129
  ("models", "create"): (models_create, "<model.json>", (1, 1)),
1008
1130
  ("models", "update"): (models_update, "<model.json>", (1, 1)),
1009
1131
  ("models", "delete"): (models_delete, "<id>", (1, 1)),
1132
+ ("models", "set-tools"): (models_set_tools, "<id> [tool-id]...", (1, 999)),
1133
+ ("models", "set-filters"): (models_set_filters, "<id> [filter-id]...", (1, 999)),
1010
1134
  ("models", "pull-all"): (models_pull_all, "[dir]", (0, 1)),
1011
1135
  ("knowledge", "list"): (knowledge_list, "", (0, 0)),
1012
1136
  ("knowledge", "show"): (knowledge_show, "<id>", (1, 1)),
@@ -0,0 +1,103 @@
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>
@@ -1 +0,0 @@
1
- __version__ = "0.2.0"
File without changes
File without changes
File without changes