mirror of
https://github.com/alexbelgium/hassio-addons.git
synced 2026-10-03 06:52:41 +02:00
Compare commits
130 Commits
feat/claud
...
bcc521015b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bcc521015b | ||
|
|
6b46611a0b | ||
|
|
a72e03e88a | ||
|
|
8042ac6786 | ||
|
|
5b7de5907f | ||
|
|
0d344ecd6c | ||
|
|
f34848ae42 | ||
|
|
61627e80bb | ||
|
|
6f693f39dc | ||
|
|
0397e2b849 | ||
|
|
865e1c8e6d | ||
|
|
25945f5a37 | ||
|
|
feaf4f5c92 | ||
|
|
fa83755108 | ||
|
|
101c252b71 | ||
|
|
1f177e26f4 | ||
|
|
9232156924 | ||
|
|
766abd5ec3 | ||
|
|
49fbbbf59c | ||
|
|
a55c410b9c | ||
|
|
e32a134ee2 | ||
|
|
0019b52f4a | ||
|
|
a240dd8d00 | ||
|
|
408f9ee05b | ||
|
|
324dc560c2 | ||
|
|
cc8b402942 | ||
|
|
8eb51526a1 | ||
|
|
4c068c6792 | ||
|
|
476f02924d | ||
|
|
2964b762d1 | ||
|
|
b942bcf336 | ||
|
|
3973e5901f | ||
|
|
1b313e591d | ||
|
|
49130bafc8 | ||
|
|
6463cc964a | ||
|
|
3d2773db75 | ||
|
|
6ad59a6e54 | ||
|
|
8ad03ce68c | ||
|
|
11de95228d | ||
|
|
d6d09a0204 | ||
|
|
cd98281978 | ||
|
|
f35f0baaeb | ||
|
|
5dcb1c222a | ||
|
|
1c981e187f | ||
|
|
3725f920e0 | ||
|
|
e21496f18d | ||
|
|
4dd0e22a84 | ||
|
|
fe60b6d6b1 | ||
|
|
f4877df9cc | ||
|
|
8c1152501d | ||
|
|
ac72fac79c | ||
|
|
bb51a80700 | ||
|
|
1a50bbe522 | ||
|
|
2fa921734b | ||
|
|
2bdf65187c | ||
|
|
881821e17c | ||
|
|
63fcca6f74 | ||
|
|
dcf8073734 | ||
|
|
6ad65abc8e | ||
|
|
8ee044b697 | ||
|
|
1b32956a2a | ||
|
|
36e0800596 | ||
|
|
c7bc78f306 | ||
|
|
090b200cb1 | ||
|
|
c3e81e8761 | ||
|
|
f85151b52c | ||
|
|
179863a7b0 | ||
|
|
8d67dea5f1 | ||
|
|
4a05b9a287 | ||
|
|
acee58b78d | ||
|
|
d9e08a447b | ||
|
|
49dcc057d5 | ||
|
|
a6d93753e9 | ||
|
|
920591631d | ||
|
|
15853bd661 | ||
|
|
cf918a2c2b | ||
|
|
680a947ced | ||
|
|
b6fc7f2c3a | ||
|
|
9add537dc6 | ||
|
|
99ccd61c14 | ||
|
|
dc5c085598 | ||
|
|
778ef33359 | ||
|
|
4804041920 | ||
|
|
510bd46128 | ||
|
|
7a375df1de | ||
|
|
295c3d912f | ||
|
|
f4cfc359fa | ||
|
|
0b61865f36 | ||
|
|
8c763216c8 | ||
|
|
dd885ceaee | ||
|
|
51288da9d0 | ||
|
|
3d2f3aa193 | ||
|
|
26101a1104 | ||
|
|
a4764364da | ||
|
|
9bbd72e70a | ||
|
|
e345752d00 | ||
|
|
7e2c8eda5e | ||
|
|
65fbc7dae2 | ||
|
|
95641d253c | ||
|
|
bdc6231aa3 | ||
|
|
ae2c29b977 | ||
|
|
3114a6cc94 | ||
|
|
297102e908 | ||
|
|
39efd5602f | ||
|
|
8b3db6e325 | ||
|
|
e70b8db0fa | ||
|
|
4872bd89c9 | ||
|
|
b3c27024d8 | ||
|
|
b8c7cc3815 | ||
|
|
3354af026f | ||
|
|
3bff6b65de | ||
|
|
efc659f452 | ||
|
|
2f245c77e1 | ||
|
|
2347cb9eae | ||
|
|
0cfe28a405 | ||
|
|
26c26e3668 | ||
|
|
64c7f2dd5b | ||
|
|
259517289a | ||
|
|
b26da2e161 | ||
|
|
9ec6c44f03 | ||
|
|
ff43c4eeba | ||
|
|
a1e68ee807 | ||
|
|
2a1412d957 | ||
|
|
b328ae242f | ||
|
|
9c0521da49 | ||
|
|
7d2c6eb9b2 | ||
|
|
f990177df8 | ||
|
|
005275315a | ||
|
|
0223cc3511 | ||
|
|
e5983f4718 |
15
.github/workflows/onpr_check-pr.yaml
vendored
15
.github/workflows/onpr_check-pr.yaml
vendored
@@ -93,6 +93,21 @@ jobs:
|
||||
- name: ↩️ Checkout
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Copy templates into addon build context
|
||||
env:
|
||||
ADDON: ${{ matrix.addon }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TEMPLATES_DIR=".templates"
|
||||
ADDON_DIR="./$ADDON"
|
||||
|
||||
# Keep PR builds aligned with the production builder.
|
||||
for script in ha_automodules.sh ha_autoapps.sh ha_entrypoint.sh bashio-standalone.sh ha_lsio.sh; do
|
||||
if [ -f "$TEMPLATES_DIR/$script" ]; then
|
||||
cp "$TEMPLATES_DIR/$script" "$ADDON_DIR/$script"
|
||||
fi
|
||||
done
|
||||
|
||||
- name: ℹ️ Gather addon info
|
||||
id: information
|
||||
uses: frenck/action-addon-information@v1.4
|
||||
|
||||
@@ -258,13 +258,15 @@ If you want to do add the repository manually, please follow the procedure highl
|
||||
![amd64][amd64-badge]
|
||||
![ingress][ingress-badge]
|
||||
|
||||
✓  [Claude Desktop](claude_desktop/) : Claude Desktop with Headroom MCP context compression and RTK acceleration
|
||||
✓  [Claude Desktop](claude_desktop/) : Claude Desktop with Headroom, RTK, and TokenSave optimization
|
||||
|
||||
  
|
||||

|
||||
![aarch64][aarch64-badge]
|
||||
![amd64][amd64-badge]
|
||||
![ingress][ingress-badge]
|
||||
![smb][smb-badge]
|
||||
![localdisks][localdisks-badge]
|
||||
|
||||
✓  [Cleanuparr](cleanuparr/) : Automatically removes stuck and unwanted downloads from your *arr and download clients
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
|
||||
## 1.82.1 (2026-07-16)
|
||||
- Update to latest version from autobrr/autobrr (changelog : https://github.com/autobrr/autobrr/releases)
|
||||
|
||||
## 1.81.0 (2026-07-04)
|
||||
- Update to latest version from autobrr/autobrr (changelog : https://github.com/autobrr/autobrr/releases)
|
||||
## 1.80.0 (2026-06-05)
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
ARG BUILD_FROM
|
||||
ARG BUILD_VERSION
|
||||
ARG BUILD_UPSTREAM="1.81.0"
|
||||
ARG BUILD_UPSTREAM="1.82.1"
|
||||
FROM ${BUILD_FROM}
|
||||
|
||||
##################
|
||||
|
||||
@@ -108,4 +108,4 @@ schema:
|
||||
slug: autobrr
|
||||
udev: true
|
||||
url: https://github.com/alexbelgium/hassio-addons/tree/master/autobrr
|
||||
version: "1.81.0"
|
||||
version: "1.82.1"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"last_update": "2026-07-04",
|
||||
"last_update": "2026-07-16",
|
||||
"repository": "alexbelgium/hassio-addons",
|
||||
"slug": "autborr",
|
||||
"source": "github",
|
||||
"upstream_repo": "autobrr/autobrr",
|
||||
"upstream_version": "1.81.0"
|
||||
"upstream_version": "1.82.1"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
## source-20260717 (17-07-2026)
|
||||
- Minor bugs fixed
|
||||
## source-20260716.02 (16-07-2026)
|
||||
- Minor bugs fixed
|
||||
## source-20260716 (16-07-2026)
|
||||
- Minor bugs fixed
|
||||
## source-20260714 (14-07-2026)
|
||||
- Minor bugs fixed
|
||||
## source-20260709 (09-07-2026)
|
||||
|
||||
@@ -127,5 +127,5 @@ slug: birdnet-go-dev
|
||||
udev: true
|
||||
url: https://github.com/alexbelgium/hassio-addons
|
||||
usb: true
|
||||
version: "source-20260714"
|
||||
version: "source-20260717"
|
||||
video: true
|
||||
|
||||
@@ -77,10 +77,27 @@ for entry in "${prs[@]}"; do
|
||||
# Fetch the PR head commit by number; works unauthenticated for public repos.
|
||||
git fetch --no-tags origin "refs/pull/${number}/head"
|
||||
if ! git merge --no-edit --no-ff -m "Merge PR #${number}: ${title}" "${sha}"; then
|
||||
echo "!!! Merge conflict while merging PR #${number} (${title})." >&2
|
||||
echo "!!! Resolve the conflict in the fork or pause this PR, then rebuild." >&2
|
||||
git merge --abort || true
|
||||
exit 1
|
||||
mapfile -t conflicted_files < <(git diff --name-only --diff-filter=U)
|
||||
|
||||
# package-lock.json is generated content and stacked PRs can carry an
|
||||
# older copy even when their source changes merge cleanly. Keep the
|
||||
# lockfile already assembled from upstream and earlier PRs, but only
|
||||
# when it is the sole conflict. Any source conflict remains fatal.
|
||||
if [ "${#conflicted_files[@]}" -eq 1 ] \
|
||||
&& [ "${conflicted_files[0]}" = "frontend/package-lock.json" ]; then
|
||||
log "Resolving generated frontend/package-lock.json conflict using the accumulated tree"
|
||||
git checkout --ours -- frontend/package-lock.json
|
||||
git add frontend/package-lock.json
|
||||
git commit --no-edit
|
||||
else
|
||||
echo "!!! Merge conflict while merging PR #${number} (${title})." >&2
|
||||
if [ "${#conflicted_files[@]}" -gt 0 ]; then
|
||||
printf '!!! Conflicting file: %s\n' "${conflicted_files[@]}" >&2
|
||||
fi
|
||||
echo "!!! Resolve the conflict in the fork or pause this PR, then rebuild." >&2
|
||||
git merge --abort || true
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
|
||||
## 20260716 (2026-07-16)
|
||||
- Update to latest version from tphakala/birdnet-go (changelog : https://github.com/tphakala/birdnet-go/releases)
|
||||
|
||||
## 20260712 (2026-07-13)
|
||||
- Update to latest version from tphakala/birdnet-go (changelog : https://github.com/tphakala/birdnet-go/releases)
|
||||
## nightly-20260615-4 (09-07-2026)
|
||||
|
||||
@@ -128,4 +128,4 @@ slug: birdnet-go
|
||||
udev: true
|
||||
url: https://github.com/alexbelgium/hassio-addons/tree/master/birdnet-go
|
||||
usb: true
|
||||
version: "20260712"
|
||||
version: "20260716"
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
"github_beta": true,
|
||||
"github_exclude": "-4",
|
||||
"github_fulltag": true,
|
||||
"last_update": "2026-07-13",
|
||||
"last_update": "2026-07-16",
|
||||
"repository": "alexbelgium/hassio-addons",
|
||||
"slug": "birdnet-go",
|
||||
"source": "github",
|
||||
"upstream_repo": "tphakala/birdnet-go",
|
||||
"upstream_version": "20260712"
|
||||
"upstream_version": "20260716"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
|
||||
## 1.92.140-ls113 (2026-07-16)
|
||||
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
||||
|
||||
## 1.92.139-ls112 (2026-07-11)
|
||||
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
||||
|
||||
|
||||
@@ -69,5 +69,5 @@ slug: brave
|
||||
tmpfs: true
|
||||
udev: true
|
||||
url: https://github.com/alexbelgium/hassio-addons
|
||||
version: "1.92.139-ls112"
|
||||
version: "1.92.140-ls113"
|
||||
video: true
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"github_fulltag": "true",
|
||||
"last_update": "2026-07-11",
|
||||
"last_update": "2026-07-16",
|
||||
"repository": "alexbelgium/hassio-addons",
|
||||
"slug": "brave",
|
||||
"source": "github",
|
||||
"upstream_repo": "linuxserver/docker-brave",
|
||||
"upstream_version": "1.92.139-ls112"
|
||||
"upstream_version": "1.92.140-ls113"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
|
||||
## version-ae9a7fca (2026-07-16)
|
||||
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
|
||||
|
||||
## version-30a7c401 (2026-07-11)
|
||||
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
|
||||
|
||||
|
||||
@@ -71,5 +71,5 @@ slug: chromium
|
||||
tmpfs: true
|
||||
udev: true
|
||||
url: https://github.com/alexbelgium/hassio-addons
|
||||
version: "version-30a7c401"
|
||||
version: "version-ae9a7fca"
|
||||
video: true
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"github_fulltag": "true",
|
||||
"last_update": "2026-07-11",
|
||||
"last_update": "2026-07-16",
|
||||
"repository": "alexbelgium/hassio-addons",
|
||||
"slug": "chromium",
|
||||
"source": "github",
|
||||
"upstream_repo": "linuxserver/docker-chromium",
|
||||
"upstream_version": "version-30a7c401"
|
||||
"upstream_version": "version-ae9a7fca"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
|
||||
## 2.55.0 (2026-07-16)
|
||||
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
|
||||
|
||||
## 2.54.2 (2026-07-04)
|
||||
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
|
||||
|
||||
|
||||
@@ -86,5 +86,5 @@ schema:
|
||||
slug: browserless_chrome
|
||||
udev: true
|
||||
url: https://github.com/alexbelgium/hassio-addons/tree/master/browserless_chrome
|
||||
version: "2.54.2"
|
||||
version: "2.55.0"
|
||||
webui: "[PROTO:ssl]://[HOST]:[PORT:3000]/docs"
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"github_tagfilter": "v",
|
||||
"last_update": "2026-07-04",
|
||||
"last_update": "2026-07-16",
|
||||
"repository": "alexbelgium/hassio-addons",
|
||||
"slug": "browserless_chrome",
|
||||
"source": "github",
|
||||
"upstream_repo": "browserless/chrome",
|
||||
"upstream_version": "2.54.2"
|
||||
"upstream_version": "2.55.0"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
|
||||
## 0.55.8 (2026-07-16)
|
||||
- Update to latest version from linuxserver/docker-changedetection.io (changelog : https://github.com/linuxserver/docker-changedetection.io/releases)
|
||||
|
||||
## 0.55.7 (2026-05-30)
|
||||
- Update to latest version from linuxserver/docker-changedetection.io (changelog : https://github.com/linuxserver/docker-changedetection.io/releases)
|
||||
|
||||
@@ -34,4 +34,4 @@ schema:
|
||||
slug: changedetection.io
|
||||
udev: true
|
||||
url: https://github.com/alexbelgium/hassio-addons/tree/master/changedetection.io
|
||||
version: "0.55.7"
|
||||
version: "0.55.8"
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"github_fulltag": "false",
|
||||
"last_update": "2026-05-30",
|
||||
"last_update": "2026-07-16",
|
||||
"repository": "alexbelgium/hassio-addons",
|
||||
"slug": "changedetection.io",
|
||||
"source": "github",
|
||||
"upstream_repo": "linuxserver/docker-changedetection.io",
|
||||
"upstream_version": "0.55.7"
|
||||
"upstream_version": "0.55.8"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,56 @@
|
||||
## 1.32 (17-07-2026)
|
||||
|
||||
- Bump `tokensave` from 7.2.0 to 7.4.0 (`rtk` was already pinned to its current latest GitHub release, `v0.43.0`; `headroom-ai` is intentionally installed unpinned from PyPI, so it already tracks latest at every build and had nothing to bump). Reviewed the intervening 7.3.0/7.4.0 release notes against every tokensave surface this add-on drives (`install --agent claude --git-hook yes`, `uninstall --agent claude`, `sync`, `init`, `doctor --agent claude`, `gain --all --range 30d`, and the `mcp__tokensave__*` tool set granted in `settings.json`): no flag, output shape, or MCP tool name used here changed. Directly relevant fixes carried along: `tokensave sync` auto-migrates a v12 database (missing the trait-dispatch caller cache) to v13 as a normal part of syncing, which the add-on's corruption-quarantine logic won't mistake for corruption since the schema migration doesn't produce a "malformed"/"not a database" error; and `install`/`uninstall`'s JSON writer now resolves a symlinked `~/.claude/settings.json` before its atomic rename instead of replacing the symlink with a plain file, so a dotfiles-managed settings file survives untouched.
|
||||
- Fix startup TokenSave repository preparation silently doing nothing: both `81-tokensave_repositories.sh` and the indexing loop in `82-claude_tools.sh` read `tokensave_project_paths` through `done < <(bashio::config ...)`, but `bashio::config` begins with a `read -d ''` heredoc that always returns non-zero, and a process substitution inherits the `errexit` enabled by the bashio wrapper itself — so the subshell died before printing and every boot iterated over an empty list (no `safe.directory` persistence, no startup `sync`/`init`; command substitutions were unaffected because subshells drop `errexit` when `inherit_errexit` is off, which is why every other option lookup worked). Repositories only got indexed when tokensave's own git hooks or a manual `tokensave init` happened to run. The list is now captured with a command substitution first and the loop reads from the captured variable (here-string); same fix applied to `claude-tools-doctor.sh`. The indexing loop also moved ahead of the MCP-registration merge, and the merge now re-tightens the 0600 mode on the token-bearing configs even on no-change boots — a first-time `tokensave init` rewrites `~/.claude.json` itself at default permissions, which previously could leave the stored Home Assistant token world-readable until the next registration change.
|
||||
- Simplification pass over the startup logic: every remaining line now serves a live purpose, with no change to what gets configured — Headroom (proxy routing, MCP registration, CLAUDE.md guidance, PostToolUse auto-compression, dashboard exposure), RTK (global files + PreToolUse hook), and TokenSave (full agent integration + per-repo indexing) are still applied automatically to every new session type (terminal, Desktop cowork/dispatch, cron).
|
||||
- `82-claude_tools.sh`: the three hand-rolled `~/.claude/settings.json` hook mutators (rtk add, rtk remove, headroom PostToolUse) are replaced by one shared `manage_settings_hook` helper using the proven strip-then-re-append pass (same dedup/matcher-migration semantics; additionally no longer creates an empty `settings.json` when asked to remove a hook from a machine that never had one). The two copy-pasted CLAUDE.md guidance managers (headroom, ha-api-helper) collapse into one `manage_claude_md_block` helper producing byte-identical blocks, so existing installs are recognized without a rewrite.
|
||||
- `81-tokensave_repositories.sh` is merged into the TokenSave loop of `82-claude_tools.sh`: the same path list was parsed twice with identical trimming/validation only so `safe.directory` could be persisted before repository detection ran as root. Detection now runs directly as the runtime user with a one-shot `safe.directory` override (the persisted entry is still written for tokensave's git hooks and Claude sessions), removing the duplicate loop and the root-reads-abc-gitconfig coupling. The battle-tested defensive sync/init block (flock, retries, corruption quarantine, init sentinel) is unchanged.
|
||||
- The `/tmp/claude-desktop-command` indirection is gone: `82-claude_tools.sh` wrote the default launch command to a file that only `defaults/autostart` read, with the identical default hardcoded as its fallback — nothing else ever wrote it. `autostart` now launches Claude Desktop directly (keyring bootstrap unchanged).
|
||||
- `82-claude_tools.sh` no longer ends with its own recursive chown of `~/.claude`, `~/.claude.json`, and `~/.config/Claude`: `84-claude_runtime_ownership.sh` already reconciles exactly those paths after all Claude configuration scripts have run.
|
||||
- `83-claude_permissions.sh` drops the hidden `.addon-permission-mode.json` state file in favor of the same managed-value semantics used for `ANTHROPIC_BASE_URL`: `auto`/`bypass` set `permissions.defaultMode`, `strict` removes it only while it still holds an add-on-managed value (`auto`/`bypassPermissions`), and a hand-set custom value is never deleted. The old restore-from-state behavior could resurrect a stale value recorded on the first managed boot; the stale state file is cleaned up on upgrade.
|
||||
- `80-configuration.sh` sheds branches that were unreachable in this image: `apk`/`pacman` installers (the base is Debian), the `pip3` fallback (`uv` is always baked in), and the no-op timezone error path (invalid `TZ` values are now actually detected against `/usr/share/zoneinfo` before the symlink is written).
|
||||
- Remove the dead `auto_update` option from `config.yaml` and the README: its schema entry was removed back in 1.22 and `81-claude_update.sh` has updated Claude Desktop unconditionally (best-effort, offline-safe) ever since; the README now states that behavior instead of documenting a switch that did nothing.
|
||||
|
||||
## 1.31 (16-07-2026)
|
||||
|
||||
- Pin the LinuxServer selkies base image to a fixed version (`…-debianbookworm-45960cc3-ls113`) instead of the rolling `…-debianbookworm` tag. The rolling tag is rebuilt continuously (and itself installs selkies "latest" at base-build time), so the desktop/stream runtime could change under the add-on with no change to its own files — builds are now reproducible and the base only moves when this value is bumped deliberately. The pinned tags resolve to exactly the image the rolling tag currently points at (amd64 `sha256:6a4d5154…`, aarch64 `sha256:90914dfd…`).
|
||||
- Fix Claude Desktop never appearing — the Selkies web client stayed on "waiting for stream" forever with `libEGL warning: failed to open /dev/dri/card0: Permission denied` in the log. The LinuxServer base image grants the desktop user (`abc`) access to the `/dev/dri` render nodes in its `init-video` s6 oneshot, but that oneshot is not a dependency of `svc-xorg`/`svc-selkies`/`svc-de`, so on Home Assistant those long-running services regularly start (via `s6-setuidgid abc`) *before* `abc` has been added to the render group. Xorg/Selkies/pixelflux then open the render device without permission, the video pipeline produces no frames, and the stream never starts. Prepare the exposed DRI nodes in a new `21-gpu_permissions.sh` cont-init script instead: `cont-init.d` runs to completion before any s6-rc service starts, so `abc` is added to each node's owning group (and the node is made world read/write as a timing-independent fallback) in time for the graphical services to use the GPU. Best-effort and a no-op on hosts that expose no GPU.
|
||||
|
||||
## 1.30 (16-07-2026)
|
||||
|
||||
- Compress large tool outputs automatically in every Claude Code session with a managed `PostToolUse` hook (new `headroom_auto_compress` option, enabled by default). Desktop-spawned sessions (cowork/dispatch) pin `ANTHROPIC_BASE_URL` to the production endpoint (headroom #869), so the transparent proxy never sees their traffic and compression there depended entirely on the model remembering to call the `headroom` MCP tools per the CLAUDE.md guidance — in practice most large outputs went uncompressed. The new `/usr/local/bin/headroom-posttooluse-compress.py` hook fires on `Bash`/`Grep`/`Glob`/`WebFetch` results over ~4000 characters, compresses them with Headroom's rule-based pipeline (SmartCrusher and friends; the Kompress ML path is disabled because its background model load can never complete inside a short-lived hook process), and swaps the result in via `hookSpecificOutput.updatedToolOutput` with a retrieval marker appended. Originals are stored in the shared CCR SQLite store (`~/.headroom/ccr_store.db` — the same one the headroom MCP server reads), so `mcp__headroom__headroom_retrieve` always recovers the full output; savings are recorded to the durable ledger (client `posttooluse-hook`) and show up in the existing gains report. The hook fails open (any error leaves the tool output untouched), never touches `stderr` fields so error text reaches the model verbatim, skips anything below a 50-token savings floor, and is registered idempotently in `~/.claude/settings.json` only after a `--self-test` confirms the interpreter can import headroom; disabling the option (or Headroom) removes the managed entry without touching user-defined hooks. Measured on a representative Home Assistant `states` dump: 10781 -> 2964 tokens (73% saved) at ~1.7 s hook overhead, with sub-100 ms pass-through for small outputs.
|
||||
|
||||
## 1.29 (16-07-2026)
|
||||
|
||||
- Point the Headroom MCP server at the persistent Kompress model cache. 1.27 set `HF_HOME` on the `svc-headroom` proxy longrun only, but the MCP server is a separate process spawned by Claude Desktop / Claude Code from the registered `mcpServers` entry, so it never inherited that export and kept resolving the HuggingFace cache to `~/.cache` — symlinked to tmpfs here and wiped on every restart. Its Kompress ML path therefore never found the model, re-downloaded ~270 MB into tmpfs on each boot, and lost it again on the next one; `headroom_compress` fell back to `router:noop` (unchanged output) on prose and other unstructured content. The managed `headroom` entry in both `claude_desktop_config.json` and `~/.claude.json` now carries `env.HF_HOME` pointing at the same `~/.headroom/hf` cache the proxy warms. Rule-based compression (SmartCrusher, structured tool output) was unaffected and worked throughout.
|
||||
- Fix `~/.gitconfig` being written as `root` and left unreadable by the `abc` runtime user, which broke git for the user that actually runs it: every commit failed with `Author identity unknown` and the `gh` credential helper was invisible to authenticated pushes. `git config --global` ran as root during init and rewrites the file on every start, so `20-folders.sh`'s earlier recursive chown never stuck to it (`.config/gh` survived abc-owned only because the "already authenticated" branch skips rewriting it). The git/gh setup now runs as `abc` via `s6-setuidgid`, matching `81-tokensave_repositories.sh`, and reclaims any root-owned copies left by an earlier version before writing.
|
||||
- Fix `~/.bashrc` accumulating stale `HOME`/`FM_HOME` exports when `data_location` changes. The idempotency guard only tested for the *current* `$LOCATION`, so changing the option and later changing it back appended a second block while leaving the first, and the last one written won for every interactive shell — leaving `$HOME` pointing at a directory the add-on no longer manages. Any tool that resolves config through `$HOME` then read the wrong path (`headroom doctor` reported `claude: not routed (no ~/.claude/settings.json)` against a correctly routed install, and bare `headroom` invocations created a stray `.headroom` tree under the old location). The block is now marker-delimited and rewritten from scratch on every boot, so it is idempotent across any number of `data_location` changes.
|
||||
|
||||
## 1.27 (15-07-2026)
|
||||
|
||||
- Route Claude Desktop cowork/local-agent-mode sessions through the Headroom proxy. Desktop spawns its bundled Claude Code binary at an absolute path (bypassing the add-on's PATH wrapper) with `ANTHROPIC_BASE_URL` pinned to the production endpoint, so those sessions never produced proxy savings. The add-on now manages `env.ANTHROPIC_BASE_URL` in `~/.claude/settings.json` — settings `env` entries replace inherited environment values at CLI startup — gated on `headroom_wrap_claude_code` and never overwriting a user-customized endpoint.
|
||||
- Fix Headroom's Kompress compression engine never activating, which made even proxied traffic record zero token savings (e.g. 175 requests, 0 saved). The proxy's startup preload is deliberately cache-only, but the HuggingFace model cache defaulted to `~/.cache` — tmpfs in this add-on, wiped every restart — so the ONNX model (plus the separately fetched `answerdotai/ModernBERT-base` tokenizer) was never cached and the engine idled in "deferred" mode forever, misleadingly logged as `Kompress: not installed`. `svc-headroom` now points `HF_HOME` at persistent storage (`~/.headroom/hf`, ~270 MB); the proxy's own request path already downloads a missing model in the background on first use and passes requests through uncompressed until it lands, so no blocking startup pre-warm is needed — the port binds immediately either way, and Kompress activates within the first couple of requests on the first boot, then loads instantly on every boot after. The already-installed `proxy` extra's ONNX runtime is sufficient — the multi-gigabyte PyTorch `ml` extra is deliberately not installed.
|
||||
|
||||
## 1.26 (15-07-2026)
|
||||
|
||||
- Fix startup permission failures that prevented Claude Desktop from starting: storage was chowned to a hardcoded `1000:1000`, but the shared `abc` desktop user was never mapped to that UID. During init `abc` was still the image default (`911`), so TokenSave (`.claude.json.new`), RTK (`RTK.md`), nginx, PulseAudio, the Mesa shader cache, and Claude Desktop itself all hit `Permission denied`; the base image's `init-adduser` then remapped `abc` to root mid-startup (PUID/PGID were read from add-on options where they did not exist, falling back to `0`), which also made Claude Code reject `permission_mode: bypass`.
|
||||
- Add `PUID`/`PGID` add-on options (default `1000:1000`) and remap `abc` to that identity at the very start of folder setup, before any ownership is applied and before any service resolves the user. The base image's `init-adduser` is pinned to the same effective identity so it can no longer remap `abc` mid-startup.
|
||||
- In `permission_mode: bypass`, a configured `PUID: 0` automatically falls back to UID `1000` (Claude Code refuses bypass permissions as root), retaining the configured group.
|
||||
- Fix `bashio::config.array: command not found` in the TokenSave repository setup, tools configuration, and `claude-tools-doctor.sh`: the function only exists in the repo's standalone bashio, not in the real bashio shipped in the image. Use `bashio::config`, which prints list entries one per line.
|
||||
- Return managed Claude configuration files to the effective `abc` identity instead of the raw configured `PUID`/`PGID` (which previously fell back to `0` and left the files root-owned).
|
||||
- Pre-create `/tmp/.X11-unix` with the standard sticky mode so Xorg, which runs as the non-root `abc` user on a tmpfs `/tmp`, no longer fails to create its socket directory (`_XSERVTransmkdir: euid != 0`).
|
||||
|
||||
## 1.25 (15-07-2026)
|
||||
- Minor bugs fixed
|
||||
## 1.24 (15-07-2026)
|
||||
|
||||
- Fix the `/usr/local/bin/claude` wrapper never routing terminal Claude Code sessions through the Headroom proxy: it hardcoded `HEADROOM_BIN="/usr/local/bin/headroom"` while the binary is installed at `/usr/bin/headroom`, so the executable check always failed and the wrapper fell back to launching Claude Code directly. Resolve the binary with `command -v headroom` instead.
|
||||
- Harden startup TokenSave indexing so an interrupted `init`/`sync` or a hard add-on stop can no longer leave a corrupt semantic graph that fails every subsequent boot. Each configured repository is now prepared under a startup-scoped `flock` (serialised against overlapping restarts and mid-boot git sync hooks); an existing index is refreshed with a retried incremental `sync` (transient `SQLITE_BUSY` no longer looks like corruption); and only a genuinely unreadable index — or a half-written one flagged by an `init` sentinel — is quarantined to `.tokensave/corrupt-<timestamp>/` and rebuilt from scratch, so the graph self-heals instead of propagating corruption.
|
||||
|
||||
## 1.23 (15-07-2026)
|
||||
|
||||
- Add a `ha-cli` helper that lets Claude configure Home Assistant (automations, scripts, scenes, helpers, dashboards, area/label/floor/entity registries, and service calls) through the Home Assistant Core API instead of a filesystem mount. It authenticates automatically with the add-on's `SUPERVISOR_TOKEN` via the Supervisor Core-API proxy (no token setup), and deliberately cannot reach `configuration.yaml`/`secrets.yaml` or other add-ons' credentials. Toggle with the new `enable_ha_api_helper` option (default on), which also controls a managed guidance block appended to `~/.claude/CLAUDE.md`.
|
||||
|
||||
## 1.21 (15-07-2026)
|
||||
|
||||
- Fix Claude Code bypass permissions being rejected when the add-on uses its default root `PUID`.
|
||||
|
||||
@@ -11,7 +11,7 @@ ARG BUILD_FROM
|
||||
ARG BUILD_VERSION
|
||||
ARG RTK_VERSION="v0.43.0"
|
||||
ARG RTK_COMMIT="5a7880d404db8364d602f2ecdc41dd790f64013f"
|
||||
ARG TOKENSAVE_VERSION="7.2.0"
|
||||
ARG TOKENSAVE_VERSION="7.4.0"
|
||||
|
||||
# The upstream aarch64 release is cross-built on ubuntu-latest and requires
|
||||
# GLIBC 2.39. Build the pinned source on Bookworm instead so it is compatible
|
||||
@@ -74,7 +74,7 @@ RUN curl -fsSL --retry 3 --retry-delay 2 \
|
||||
# cannot alter executables elsewhere in the image.
|
||||
COPY rootfs/ /
|
||||
RUN find /etc/cont-init.d /etc/s6-overlay /defaults /usr/local/bin -type f \
|
||||
\( -name "*.sh" -o -name "run" -o -name "finish" \) -print -exec chmod +x {} \; && \
|
||||
\( -name "*.sh" -o -name "run" -o -name "finish" -o -name "ha-cli" \) -print -exec chmod +x {} \; && \
|
||||
chmod +x /usr/local/bin/claude
|
||||
|
||||
# Uses /bin for compatibility purposes
|
||||
@@ -108,27 +108,22 @@ RUN install -d -m 0755 /etc/apt/keyrings && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install the current upstream hadolint and actionlint releases for both supported
|
||||
# architectures. The GitHub release API resolves the latest asset at build time, so these
|
||||
# developer tools are intentionally not version-pinned.
|
||||
ARG HADOLINT_VERSION=v2.14.0
|
||||
ARG ACTIONLINT_VERSION=v1.7.12
|
||||
|
||||
RUN set -eux; \
|
||||
case "${BUILD_ARCH}" in \
|
||||
amd64) hadolint_arch="x86_64"; actionlint_arch="amd64" ;; \
|
||||
aarch64) hadolint_arch="arm64"; actionlint_arch="arm64" ;; \
|
||||
*) echo "Unsupported validation-tools architecture: ${BUILD_ARCH}" >&2; exit 1 ;; \
|
||||
esac; \
|
||||
hadolint_name="hadolint-linux-${hadolint_arch}"; \
|
||||
hadolint_url="$(curl -fsSL https://api.github.com/repos/hadolint/hadolint/releases/latest \
|
||||
| jq -r --arg name "${hadolint_name}" '.assets[] | select(.name == $name) | .browser_download_url' \
|
||||
| head -n 1)"; \
|
||||
test -n "${hadolint_url}"; \
|
||||
curl -fsSL --retry 3 --retry-delay 2 -o /usr/local/bin/hadolint "${hadolint_url}"; \
|
||||
curl -fsSL --retry 3 --retry-delay 2 \
|
||||
-o /usr/local/bin/hadolint \
|
||||
"https://github.com/hadolint/hadolint/releases/download/${HADOLINT_VERSION}/hadolint-linux-${hadolint_arch}"; \
|
||||
chmod 0755 /usr/local/bin/hadolint; \
|
||||
actionlint_suffix="_linux_${actionlint_arch}.tar.gz"; \
|
||||
actionlint_url="$(curl -fsSL https://api.github.com/repos/rhysd/actionlint/releases/latest \
|
||||
| jq -r --arg suffix "${actionlint_suffix}" '.assets[] | select(.name | endswith($suffix)) | .browser_download_url' \
|
||||
| head -n 1)"; \
|
||||
test -n "${actionlint_url}"; \
|
||||
curl -fsSL --retry 3 --retry-delay 2 -o /tmp/actionlint.tar.gz "${actionlint_url}"; \
|
||||
curl -fsSL --retry 3 --retry-delay 2 \
|
||||
-o /tmp/actionlint.tar.gz \
|
||||
"https://github.com/rhysd/actionlint/releases/download/${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION#v}_linux_${actionlint_arch}.tar.gz"; \
|
||||
tar -xzf /tmp/actionlint.tar.gz -C /tmp actionlint; \
|
||||
install -m 0755 /tmp/actionlint /usr/local/bin/actionlint; \
|
||||
rm -f /tmp/actionlint /tmp/actionlint.tar.gz; \
|
||||
@@ -143,10 +138,13 @@ RUN /usr/local/bin/rtk --version && /usr/local/bin/tokensave --version
|
||||
|
||||
# Install only the Headroom proxy, code-compression, and MCP features used by this add-on,
|
||||
# plus mcp-proxy (stdio->HTTP bridge for the Home Assistant MCP server) and uv (fast
|
||||
# installer used for the additional_pip option).
|
||||
# installer used for the additional_pip option). The `proxy` extra already ships the ONNX
|
||||
# runtime + transformers needed by the Kompress compressor — the `ml` extra (full PyTorch,
|
||||
# ~5 GB with CUDA wheels) is deliberately NOT installed; svc-headroom pre-warms the ONNX
|
||||
# model into the persistent HF cache instead.
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends nodejs && \
|
||||
pip3 install --break-system-packages "headroom-ai[proxy,code,mcp]" mcp-proxy uv && \
|
||||
pip3 install --break-system-packages "headroom-ai[proxy,code,mcp]" mcp-proxy uv websockets && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/* /root/.cache
|
||||
|
||||
|
||||
@@ -35,9 +35,10 @@ PATH tools.
|
||||
`/usr/bin/claude` directly, the session remains functional and still has the
|
||||
shared permission mode and Headroom MCP tools, but transparent proxy
|
||||
compression cannot be injected.
|
||||
- When `permission_mode: bypass` is selected while `PUID` is `0`, the add-on
|
||||
automatically remaps the shared `abc` desktop account to an unused non-root
|
||||
UID before Selkies and Claude Desktop start. Claude Code refuses bypass mode
|
||||
- The shared `abc` desktop account runs under the configured `PUID`/`PGID`
|
||||
(default `1000:1000`). When `permission_mode: bypass` is selected while
|
||||
`PUID` is `0`, the add-on automatically falls back to UID `1000` before
|
||||
Selkies and Claude Desktop start, because Claude Code refuses bypass mode
|
||||
under an effective root UID.
|
||||
- **gnome-keyring** provides the Secret Service backend Electron needs to
|
||||
persist sign-in and dispatch permission grants across restarts.
|
||||
@@ -69,7 +70,8 @@ Git synchronization hooks. A repository is indexed only when it is listed in
|
||||
approval, or explicit full bypass for trusted installations.
|
||||
- Automatic non-root runtime enforcement for bypass mode, including root-console
|
||||
wrapper launches.
|
||||
- Optional runtime Claude Desktop updates from Anthropic's apt repository.
|
||||
- Best-effort Claude Desktop update from Anthropic's apt repository at every
|
||||
startup (skipped silently when offline).
|
||||
- Optional extra apt and pip package installation (pip installs use `uv`).
|
||||
- Baked-in `git`, GitHub CLI (`gh`), `ripgrep`, `jq`, `shellcheck`, `yamllint`,
|
||||
`hadolint`, and `actionlint`.
|
||||
@@ -88,16 +90,16 @@ Git synchronization hooks. A repository is indexed only when it is listed in
|
||||
|
||||
| Option | Default | Description |
|
||||
| ------ | ------- | ----------- |
|
||||
| `PUID` / `PGID` | `0` / `0` | Numeric user and group applied by LinuxServer initialization. In bypass mode, a root `PUID` is automatically replaced at runtime by an unused non-root UID while the configured group is retained. |
|
||||
| `PUID` / `PGID` | `1000` / `1000` | Numeric user and group of the shared `abc` desktop account that owns the data location and runs Claude Desktop. In bypass mode, a root `PUID` is automatically replaced at runtime by UID `1000` while the configured group is retained. |
|
||||
| `TZ` | | Optional timezone, for example `Europe/Brussels`. |
|
||||
| `KEYBOARD` | | Optional Selkies keyboard layout. |
|
||||
| `PASSWORD` | | Optional password for direct Selkies ports. |
|
||||
| `DRINODE` | | Optional GPU device override for Selkies. |
|
||||
| `DNS_server` | `8.8.8.8` | DNS server used by the standard DNS module. |
|
||||
| `auto_update` | `true` | Upgrade `claude-desktop` from Anthropic's apt repository at startup. |
|
||||
| `permission_mode` | `auto` | Claude Code permission policy: `strict`, `auto`, or `bypass`. |
|
||||
| `install_headroom` | `true` | Register Headroom MCP and run the supervised local proxy. |
|
||||
| `headroom_wrap_claude_code` | `true` | Route PATH-based Claude Code launches through the already-running Headroom proxy. |
|
||||
| `headroom_auto_compress` | `true` | Auto-compress large tool outputs in every Claude Code session via a managed `PostToolUse` hook. |
|
||||
| `expose_headroom_dashboard` | `false` | Bind Headroom to all interfaces. Port `8787/tcp` must also be mapped manually. |
|
||||
| `install_rtk` | `true` | Configure RTK's Claude Code `PreToolUse` Bash hook. |
|
||||
| `install_tokensave` | `true` | Install TokenSave's complete global Claude integration. |
|
||||
@@ -111,6 +113,7 @@ Git synchronization hooks. A repository is indexed only when it is listed in
|
||||
| `enable_ha_mcp` | `false` | Register Home Assistant's MCP server in Claude (requires `ha_mcp_token`). |
|
||||
| `ha_mcp_url` | `http://homeassistant:8123/api/mcp` | Streamable HTTP endpoint of Home Assistant's MCP Server integration. |
|
||||
| `ha_mcp_token` | | Home Assistant long-lived access token used by the MCP bridge. |
|
||||
| `enable_ha_api_helper` | `true` | Ship the `ha-cli` Core-API helper and add guidance so Claude can configure Home Assistant without a `/config` mount. |
|
||||
| `additional_apps` | | Comma-separated Debian apt packages to install at startup. |
|
||||
| `additional_pip` | | Comma-separated pip packages installed at startup (via `uv`). |
|
||||
| `data_location` | `/data/data` | Persistent home directory for Claude and tooling. |
|
||||
@@ -130,11 +133,11 @@ permission_mode: auto
|
||||
`--dangerously-skip-permissions` for wrapper-launched sessions.
|
||||
|
||||
Claude Code does not permit bypass mode when its effective UID is `0`. If the
|
||||
add-on is configured with `PUID: 0`, selecting `bypass` remaps only the shared
|
||||
`abc` runtime account to an available non-root UID (preferring `1000`, then
|
||||
`911`) before storage ownership and Desktop startup. Its configured primary
|
||||
GID is retained, so group-based access to mounted Home Assistant paths remains
|
||||
available. Strict and auto modes keep the configured identity unchanged.
|
||||
add-on is configured with `PUID: 0`, selecting `bypass` runs the shared `abc`
|
||||
runtime account as UID `1000` instead, before storage ownership and Desktop
|
||||
startup. Its configured primary GID is retained, so group-based access to
|
||||
mounted Home Assistant paths remains available. Strict and auto modes keep the
|
||||
configured identity unchanged.
|
||||
|
||||
A root shell invoking `/usr/local/bin/claude` in bypass mode is also dropped to
|
||||
the remapped `abc` account. Directly invoking `/usr/bin/claude` as root still
|
||||
@@ -173,6 +176,16 @@ the MCP integration. The `/usr/local/bin/claude` wrapper routes PATH-based Claud
|
||||
Code sessions through `headroom wrap claude --no-proxy`, reusing the supervised
|
||||
backend without starting a second proxy.
|
||||
|
||||
With `headroom_auto_compress` enabled (the default), a managed Claude Code
|
||||
`PostToolUse` hook additionally compresses large `Bash`/`Grep`/`Glob`/`WebFetch`
|
||||
outputs (over ~4000 characters) in **every** session type — terminal, Desktop
|
||||
cowork, dispatch, and cron — without the model having to remember to call the
|
||||
MCP tools. The original output is kept in Headroom's local store for one hour
|
||||
and can always be recovered with `mcp__headroom__headroom_retrieve` using the
|
||||
hash printed in the compression marker. Error text (`stderr`) is never
|
||||
compressed, and plain prose passes through unchanged; the savings come from
|
||||
structured output such as JSON dumps, search results, and logs.
|
||||
|
||||
The dashboard is disabled externally by default. To expose it:
|
||||
|
||||
1. Set `expose_headroom_dashboard: true`.
|
||||
@@ -216,6 +229,45 @@ The add-on bridges Claude to the integration's stateless Streamable HTTP
|
||||
endpoint (`/api/mcp`) with `mcp-proxy`. Override `ha_mcp_url` only if your Home
|
||||
Assistant instance is not reachable as `homeassistant:8123` from add-ons.
|
||||
|
||||
## Configuring Home Assistant (API helper)
|
||||
|
||||
When `enable_ha_api_helper` is on (the default), the add-on ships a `ha-cli`
|
||||
command and tells Claude — via a managed block in `~/.claude/CLAUDE.md` — that
|
||||
it can configure Home Assistant through the Home Assistant **Core API** rather
|
||||
than a filesystem mount. This is deliberately more contained than mapping
|
||||
`/config`: the API cannot read `configuration.yaml`, `secrets.yaml`, or any
|
||||
other add-on's stored credentials.
|
||||
|
||||
`ha-cli` authenticates automatically with the add-on's `SUPERVISOR_TOKEN`
|
||||
through the Supervisor Core-API proxy (the add-on already sets
|
||||
`homeassistant_api: true`), so there is nothing to configure. It can create and
|
||||
edit automations, scripts, and scenes; call any service; read entity states;
|
||||
and, over WebSocket, manage helpers, dashboards, and the area/label/floor/entity
|
||||
registries. Run `ha-cli --help` inside the add-on for the full command
|
||||
reference.
|
||||
|
||||
```bash
|
||||
ha-cli config # connectivity check
|
||||
ha-cli get config/automation/config/<id> # read one automation
|
||||
ha-cli post config/automation/config/<id> @new.json # create/update it
|
||||
ha-cli call automation.reload # apply YAML-mode changes
|
||||
ha-cli ws '{"type":"config/area_registry/list"}'
|
||||
```
|
||||
|
||||
Security notes:
|
||||
|
||||
- The Supervisor proxy token grants **admin-equivalent** Core API access (it can
|
||||
call any service and edit any UI-managed configuration), but it cannot reach
|
||||
the raw YAML files or other add-ons' data. For a tighter scope, set
|
||||
`HA_BASE_URL`/`HA_TOKEN` (or the `ha_mcp_token` option) to a limited Home
|
||||
Assistant user's long-lived token — `ha-cli` prefers those when present.
|
||||
- The guidance instructs Claude to read each object and show you the intended
|
||||
change before writing, but Claude Code's own tool-permission prompts remain
|
||||
the real gate: each `ha-cli` call still needs your approval unless
|
||||
`permission_mode` is set to `bypass`.
|
||||
- Set `enable_ha_api_helper: false` to remove both the guidance block and the
|
||||
helper's registration if you do not want Claude configuring Home Assistant.
|
||||
|
||||
## Custom scripts
|
||||
|
||||
The add-on includes the repository standard custom-script executor. On first
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"build_from": {
|
||||
"aarch64": "ghcr.io/linuxserver/baseimage-selkies:arm64v8-debianbookworm",
|
||||
"amd64": "ghcr.io/linuxserver/baseimage-selkies:amd64-debianbookworm"
|
||||
"aarch64": "ghcr.io/linuxserver/baseimage-selkies:arm64v8-debianbookworm-45960cc3-ls113",
|
||||
"amd64": "ghcr.io/linuxserver/baseimage-selkies:amd64-debianbookworm-45960cc3-ls113"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,18 +34,21 @@ name: Claude Desktop
|
||||
options:
|
||||
env_vars: []
|
||||
DNS_server: 8.8.8.8
|
||||
PGID: 1000
|
||||
PUID: 1000
|
||||
data_location: /data/data
|
||||
additional_apps: ""
|
||||
additional_pip: ""
|
||||
auto_update: true
|
||||
github_email: ""
|
||||
enable_ha_mcp: false
|
||||
ha_mcp_url: http://homeassistant:8123/api/mcp
|
||||
ha_mcp_token: ""
|
||||
enable_ha_api_helper: true
|
||||
github_token: ""
|
||||
github_username: ""
|
||||
enable_tools_health_report: true
|
||||
expose_headroom_dashboard: false
|
||||
headroom_auto_compress: true
|
||||
headroom_wrap_claude_code: true
|
||||
install_caveman: false
|
||||
install_github_cli: true
|
||||
@@ -74,6 +77,8 @@ schema:
|
||||
DRINODE: list(/dev/dri/card0|/dev/dri/card1|/dev/dri/card2|/dev/dri/renderD128|/dev/dri/renderD129|)?
|
||||
KEYBOARD: list(da-dk-qwerty|de-de-qwertz|en-gb-qwerty|en-us-qwerty|es-es-qwerty|fr-ch-qwertz|fr-fr-azerty|it-it-qwerty|ja-jp-qwerty|pt-br-qwerty|sv-se-qwerty|tr-tr-qwerty)?
|
||||
PASSWORD: str?
|
||||
PGID: int
|
||||
PUID: int
|
||||
TZ: match([A-Z][a-z]*./[A-Z][a-z]*.)?
|
||||
additional_apps: str?
|
||||
additional_pip: str?
|
||||
@@ -86,10 +91,12 @@ schema:
|
||||
enable_ha_mcp: bool?
|
||||
ha_mcp_url: str?
|
||||
ha_mcp_token: password?
|
||||
enable_ha_api_helper: bool?
|
||||
github_token: password?
|
||||
github_username: str?
|
||||
enable_tools_health_report: bool
|
||||
expose_headroom_dashboard: bool
|
||||
headroom_auto_compress: bool?
|
||||
headroom_wrap_claude_code: bool
|
||||
install_caveman: bool
|
||||
install_github_cli: bool
|
||||
@@ -103,5 +110,5 @@ slug: claude_desktop
|
||||
tmpfs: true
|
||||
udev: true
|
||||
url: https://github.com/alexbelgium/hassio-addons
|
||||
version: "1.22"
|
||||
version: "1.32"
|
||||
video: true
|
||||
|
||||
@@ -12,23 +12,7 @@ export GNOME_KEYRING_CONTROL SSH_AUTH_SOCK
|
||||
dbus-update-activation-environment --all >/dev/null 2>&1 || true
|
||||
|
||||
# --password-store=gnome-libsecret forces Electron onto the libsecret backend instead of
|
||||
# falling back to plaintext (and warning that the sign-in will not be saved).
|
||||
CLAUDE_DESKTOP_COMMAND_FILE="/tmp/claude-desktop-command"
|
||||
DEFAULT_CLAUDE_DESKTOP_COMMAND="claude-desktop --no-sandbox --disable-dev-shm-usage --password-store=gnome-libsecret"
|
||||
if [ -s "$CLAUDE_DESKTOP_COMMAND_FILE" ]; then
|
||||
CLAUDE_DESKTOP_COMMAND="$(cat "$CLAUDE_DESKTOP_COMMAND_FILE")"
|
||||
else
|
||||
CLAUDE_DESKTOP_COMMAND="$DEFAULT_CLAUDE_DESKTOP_COMMAND"
|
||||
fi
|
||||
|
||||
# Headroom is intentionally not injected into the Desktop process. Claude Desktop overrides
|
||||
# ANTHROPIC_BASE_URL, so Desktop uses the registered Headroom MCP tools instead.
|
||||
|
||||
# Launch the configured command. If a custom/wrapped command fails to start, fall back to
|
||||
# the plain Claude Desktop launch so the app always comes up for the user.
|
||||
if ! sh -c "$CLAUDE_DESKTOP_COMMAND"; then
|
||||
if [ "$CLAUDE_DESKTOP_COMMAND" != "$DEFAULT_CLAUDE_DESKTOP_COMMAND" ]; then
|
||||
echo "autostart: '$CLAUDE_DESKTOP_COMMAND' failed; falling back to default Claude Desktop launch" >&2
|
||||
exec sh -c "$DEFAULT_CLAUDE_DESKTOP_COMMAND"
|
||||
fi
|
||||
fi
|
||||
# falling back to plaintext (and warning that the sign-in will not be saved). Headroom is
|
||||
# intentionally not injected into the Desktop process: Claude Desktop force-overrides
|
||||
# ANTHROPIC_BASE_URL (headroom #869), so Desktop uses the registered Headroom MCP tools.
|
||||
exec claude-desktop --no-sandbox --disable-dev-shm-usage --password-store=gnome-libsecret
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
#!/usr/bin/with-contenv bashio
|
||||
# shellcheck shell=bash
|
||||
set -e
|
||||
set -o pipefail
|
||||
|
||||
# Claude Code deliberately refuses bypass-permissions mode when its effective UID is 0.
|
||||
# The add-on historically defaults PUID to 0, so switch the shared `abc` desktop user to
|
||||
# an unused non-root UID before storage ownership and Selkies runtime directories are set up.
|
||||
# Keep abc's configured primary group (commonly group 0) so existing group-based access to
|
||||
# Home Assistant mounts is preserved. Strict and auto permission modes are unchanged.
|
||||
if [ "$(bashio::config 'permission_mode')" != "bypass" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
CURRENT_UID="$(id -u abc)"
|
||||
if [ "$CURRENT_UID" -ne 0 ]; then
|
||||
bashio::log.info "Claude bypass runtime already uses non-root UID ${CURRENT_UID}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
find_available_uid() {
|
||||
local candidate owner
|
||||
for candidate in 1000 911 $(seq 1001 1099); do
|
||||
owner="$(getent passwd "$candidate" | cut -d: -f1 || true)"
|
||||
if [ -z "$owner" ] || [ "$owner" = "abc" ]; then
|
||||
printf '%s' "$candidate"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
TARGET_UID="$(find_available_uid || true)"
|
||||
if [ -z "$TARGET_UID" ]; then
|
||||
bashio::exit.nok "Claude bypass mode requires a non-root runtime user, but no free fallback UID was found"
|
||||
fi
|
||||
|
||||
usermod --uid "$TARGET_UID" abc
|
||||
|
||||
if [ "$(id -u abc)" -eq 0 ]; then
|
||||
bashio::exit.nok "Unable to switch the Claude Desktop runtime away from root for bypass mode"
|
||||
fi
|
||||
|
||||
mkdir -p /run/s6/container_environment
|
||||
printf '%s' "$TARGET_UID" > /run/s6/container_environment/CLAUDE_RUNTIME_UID
|
||||
printf '%s' "$(id -g abc)" > /run/s6/container_environment/CLAUDE_RUNTIME_GID
|
||||
|
||||
bashio::log.warning "Claude bypass mode cannot run as root; remapped abc from UID 0 to UID ${TARGET_UID} (GID $(id -g abc))"
|
||||
@@ -3,10 +3,35 @@
|
||||
# shellcheck disable=SC2046
|
||||
set -e
|
||||
|
||||
# Use the effective shared desktop user identity. In bypass mode an earlier init script may
|
||||
# remap abc away from UID 0 because Claude Code rejects bypass permissions when run as root.
|
||||
PUID="$(id -u abc)"
|
||||
PGID="$(id -g abc)"
|
||||
# Align the shared desktop user (abc) with the configured PUID/PGID before any storage is
|
||||
# chowned and before any service or s6-setuidgid call resolves abc. The base image's
|
||||
# init-adduser applies the same remap, but it runs after cont-init, so doing it here first is
|
||||
# what lets the tokensave/rtk/git setup in the 8x scripts run under the final identity.
|
||||
PUID="$(if bashio::config.has_value 'PUID'; then bashio::config 'PUID'; else echo '1000'; fi)"
|
||||
PGID="$(if bashio::config.has_value 'PGID'; then bashio::config 'PGID'; else echo '1000'; fi)"
|
||||
|
||||
# Claude Code refuses bypass-permissions mode under an effective root UID, so bypass mode
|
||||
# always needs a non-root desktop user.
|
||||
if [ "$(bashio::config 'permission_mode')" = "bypass" ] && [ "$PUID" -eq 0 ]; then
|
||||
bashio::log.warning "permission_mode: bypass cannot run Claude Code as root; using UID 1000 instead of the configured PUID 0"
|
||||
PUID=1000
|
||||
fi
|
||||
|
||||
groupmod -o -g "$PGID" abc 2> /dev/null || true
|
||||
usermod -o -u "$PUID" abc 2> /dev/null || true
|
||||
if [ "$(id -u abc)" -ne "$PUID" ] || [ "$(id -g abc)" -ne "$PGID" ]; then
|
||||
PUID="$(id -u abc)"
|
||||
PGID="$(id -g abc)"
|
||||
bashio::log.warning "Unable to remap the abc desktop user; continuing with its current identity ${PUID}:${PGID}"
|
||||
fi
|
||||
|
||||
# The base image's init-adduser reads PUID/PGID from the raw add-on options (default 0) and
|
||||
# runs mid-startup, racing the services. Pin it to the effective identity chosen above so it
|
||||
# can never remap abc away from the ownership applied below.
|
||||
ADDUSER_RUN="/etc/s6-overlay/s6-rc.d/init-adduser/run"
|
||||
if [ -f "$ADDUSER_RUN" ]; then
|
||||
sed -i "s|^PUID=.*|PUID=${PUID}|;s|^PGID=.*|PGID=${PGID}|" "$ADDUSER_RUN"
|
||||
fi
|
||||
|
||||
# Check data location
|
||||
LOCATION="$(bashio::config 'data_location')"
|
||||
@@ -60,10 +85,23 @@ printf "%s" "$LOCATION" > "$S6_ENVDIR/HOME"
|
||||
printf "%s" "$LOCATION" > "$S6_ENVDIR/FM_HOME"
|
||||
printf "%s" "/tmp/cache" > "$S6_ENVDIR/XDG_CACHE_HOME"
|
||||
printf "%s" "$XDG_RUNTIME_DIR" > "$S6_ENVDIR/XDG_RUNTIME_DIR"
|
||||
grep -qxF "export HOME=\"$LOCATION\"" ~/.bashrc 2>/dev/null || {
|
||||
# Re-derived on every boot rather than gated on a "does it already say $LOCATION" grep: that
|
||||
# guard only ever recognized the CURRENT $LOCATION, so a user who changed data_location and
|
||||
# later changed it back left two stale HOME/FM_HOME exports in ~/.bashrc, with the last one
|
||||
# (not necessarily the correct one) winning for every interactive shell. The marker makes this
|
||||
# idempotent regardless of how many times $LOCATION has changed: strip any previously managed
|
||||
# block, then append one that reflects the current value.
|
||||
BASHRC_HOME_BEGIN="# --- BEGIN ADDON HOME (managed) ---"
|
||||
BASHRC_HOME_END="# --- END ADDON HOME (managed) ---"
|
||||
if [ -f ~/.bashrc ]; then
|
||||
sed -i "/^${BASHRC_HOME_BEGIN}\$/,/^${BASHRC_HOME_END}\$/d" ~/.bashrc
|
||||
fi
|
||||
{
|
||||
printf "%s\n" "$BASHRC_HOME_BEGIN"
|
||||
printf "%s\n" "export HOME=\"$LOCATION\""
|
||||
printf "%s\n" "export FM_HOME=\"$LOCATION\""
|
||||
printf "%s\n" "export XDG_CACHE_HOME=\"/tmp/cache\""
|
||||
printf "%s\n" "$BASHRC_HOME_END"
|
||||
} >> ~/.bashrc
|
||||
|
||||
bashio::log.info "Creating $LOCATION"
|
||||
@@ -71,6 +109,11 @@ mkdir -p "$LOCATION" /tmp/cache "$XDG_RUNTIME_DIR"
|
||||
chmod 755 /tmp/cache
|
||||
chmod 700 "$XDG_RUNTIME_DIR"
|
||||
|
||||
# /tmp is a tmpfs and Xorg runs as the non-root abc user, which cannot create the X11 socket
|
||||
# directory itself (_XSERVTransmkdir: euid != 0). Pre-create it with the standard sticky mode.
|
||||
mkdir -p /tmp/.X11-unix
|
||||
chmod 1777 /tmp/.X11-unix
|
||||
|
||||
# Pre-create the Selkies joystick log so the base image's "chmod 777 /tmp/selkies*"
|
||||
# calls (in init-selkies-config and svc-de) never fail on an empty glob.
|
||||
touch /tmp/selkies_js.log
|
||||
@@ -82,7 +125,7 @@ fi
|
||||
ln -sfn /tmp/cache "$LOCATION/.cache"
|
||||
|
||||
bashio::log.info "Setting ownership to $PUID:$PGID"
|
||||
chown -R "$PUID":"$PGID" "$LOCATION" /tmp/cache "$XDG_RUNTIME_DIR"
|
||||
chown -R "${PUID}:${PGID}" "$LOCATION" /tmp/cache "$XDG_RUNTIME_DIR" /data
|
||||
chmod -R 700 "$LOCATION"
|
||||
|
||||
# The base init-selkies-config script overrides XDG_RUNTIME_DIR to $HOME/.XDG, which lands
|
||||
|
||||
52
claude_desktop/rootfs/etc/cont-init.d/21-gpu_permissions.sh
Executable file
52
claude_desktop/rootfs/etc/cont-init.d/21-gpu_permissions.sh
Executable file
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/with-contenv bashio
|
||||
# shellcheck shell=bash
|
||||
set -e
|
||||
|
||||
# Grant the shared desktop user (abc) access to the exposed GPU render nodes *before* the
|
||||
# graphical services start.
|
||||
#
|
||||
# The LinuxServer base image already sets up /dev/dri group access, but it does so in its
|
||||
# init-video s6 oneshot, which is NOT a dependency of svc-xorg/svc-selkies/svc-de. On Home
|
||||
# Assistant those long-running services routinely start (as abc, via s6-setuidgid) before
|
||||
# init-video has added abc to the render group, so Xorg/Selkies/pixelflux open the render
|
||||
# device with the wrong credentials and fail:
|
||||
#
|
||||
# libEGL warning: failed to open /dev/dri/card0: Permission denied
|
||||
#
|
||||
# With no usable render node the video pipeline never produces frames, so the Selkies web
|
||||
# client stays on "waiting for stream" indefinitely and Claude Desktop never appears.
|
||||
#
|
||||
# cont-init.d runs to completion before any s6-rc service is started, so preparing the DRI
|
||||
# nodes here wins that race. Everything is best-effort: a host that exposes no GPU simply has
|
||||
# no nodes to touch and this is a no-op.
|
||||
|
||||
shopt -s nullglob
|
||||
dri_nodes=(/dev/dri/card* /dev/dri/render*)
|
||||
if [ "${#dri_nodes[@]}" -eq 0 ]; then
|
||||
bashio::log.info "No /dev/dri render nodes exposed; skipping GPU permission setup"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
for node in "${dri_nodes[@]}"; do
|
||||
[ -e "$node" ] || continue
|
||||
|
||||
# Mirror the base image's init-video logic (add abc to the node's owning group, creating
|
||||
# the group when the GID is unnamed) but early enough that the s6-setuidgid at service
|
||||
# start picks the membership up.
|
||||
gid="$(stat -c '%g' "$node")"
|
||||
gname="$(getent group "$gid" | awk -F: '{print $1}')"
|
||||
if [ -z "$gname" ]; then
|
||||
gname="dri${gid}"
|
||||
groupadd -o -g "$gid" "$gname" 2> /dev/null || true
|
||||
fi
|
||||
if ! id -G abc 2> /dev/null | tr ' ' '\n' | grep -qx "$gid"; then
|
||||
usermod -a -G "$gname" abc 2> /dev/null || true
|
||||
fi
|
||||
|
||||
# Guarantee access even where group propagation is unreliable inside the add-on sandbox:
|
||||
# this is a single-user desktop container, so world read/write on the local render node
|
||||
# is acceptable and removes any dependency on group-membership timing.
|
||||
chmod o+rw "$node" 2> /dev/null || true
|
||||
|
||||
bashio::log.info "GPU: prepared ${node} (group ${gname}:${gid}) for the desktop user"
|
||||
done
|
||||
@@ -1,58 +1,48 @@
|
||||
#!/usr/bin/with-contenv bashio
|
||||
# shellcheck shell=bash
|
||||
# shellcheck disable=SC2015
|
||||
set -e
|
||||
|
||||
# The image is Debian-based (apt) and always ships uv, so those are the only installers used.
|
||||
if bashio::config.has_value 'additional_apps'; then
|
||||
bashio::log.info "Installing additional apps :"
|
||||
NEWAPPS=$(bashio::config 'additional_apps')
|
||||
if command -v "apt-get" &> /dev/null; then
|
||||
apt-get update -o Acquire::http::Timeout=10 -o Acquire::https::Timeout=10 &> /dev/null || bashio::log.warning "Unable to update apt package lists"
|
||||
fi
|
||||
for packagestoinstall in ${NEWAPPS//,/ }; do
|
||||
apt-get update -o Acquire::http::Timeout=10 -o Acquire::https::Timeout=10 &> /dev/null || bashio::log.warning "Unable to update apt package lists"
|
||||
for packagestoinstall in $(bashio::config 'additional_apps' | tr ',' ' '); do
|
||||
bashio::log.green "... $packagestoinstall"
|
||||
if command -v "apk" &> /dev/null; then
|
||||
apk add --no-cache "$packagestoinstall" &> /dev/null || (bashio::log.fatal "Error : $packagestoinstall not found")
|
||||
elif command -v "apt-get" &> /dev/null; then
|
||||
apt-get install -yqq --no-install-recommends "$packagestoinstall" &> /dev/null || (bashio::log.fatal "Error : $packagestoinstall not found")
|
||||
elif command -v "pacman" &> /dev/null; then
|
||||
pacman --noconfirm -S "$packagestoinstall" &> /dev/null || (bashio::log.fatal "Error : $packagestoinstall not found")
|
||||
fi
|
||||
apt-get install -yqq --no-install-recommends "$packagestoinstall" &> /dev/null || bashio::log.fatal "Error : $packagestoinstall not found"
|
||||
done
|
||||
fi
|
||||
|
||||
if bashio::config.has_value 'additional_pip'; then
|
||||
for p in $(bashio::config 'additional_pip' | tr ',' ' '); do
|
||||
bashio::log.green "... pip: $p"
|
||||
# Prefer uv (much faster resolver/installer); fall back to pip3 when unavailable.
|
||||
if command -v uv &> /dev/null; then
|
||||
uv pip install --system --break-system-packages "$p" || bashio::log.fatal "Error: pip package $p failed"
|
||||
else
|
||||
pip3 install --break-system-packages "$p" || bashio::log.fatal "Error: pip package $p failed"
|
||||
fi
|
||||
uv pip install --system --break-system-packages "$p" || bashio::log.fatal "Error: pip package $p failed"
|
||||
done
|
||||
fi
|
||||
|
||||
if bashio::config.has_value 'TZ'; then
|
||||
TIMEZONE=$(bashio::config 'TZ')
|
||||
bashio::log.info "Setting timezone to $TIMEZONE"
|
||||
ln -snf /usr/share/zoneinfo/"$TIMEZONE" /etc/localtime
|
||||
echo "$TIMEZONE" > /etc/timezone
|
||||
fi || (bashio::log.fatal "Error : $TIMEZONE not found. Here is a list of valid timezones : https://manpages.ubuntu.com/manpages/focal/man3/DateTime::TimeZone::Catalog.3pm.html")
|
||||
if [ -f "/usr/share/zoneinfo/$TIMEZONE" ]; then
|
||||
bashio::log.info "Setting timezone to $TIMEZONE"
|
||||
ln -snf "/usr/share/zoneinfo/$TIMEZONE" /etc/localtime
|
||||
echo "$TIMEZONE" > /etc/timezone
|
||||
else
|
||||
bashio::log.fatal "Error : $TIMEZONE not found. Here is a list of valid timezones : https://manpages.ubuntu.com/manpages/focal/man3/DateTime::TimeZone::Catalog.3pm.html"
|
||||
fi
|
||||
fi
|
||||
|
||||
if bashio::config.has_value 'KEYBOARD'; then
|
||||
KEYBOARD=$(bashio::config 'KEYBOARD')
|
||||
bashio::log.info "Setting keyboard to $KEYBOARD"
|
||||
if [ -d /var/run/s6/container_environment ]; then printf "%s" "$KEYBOARD" > /var/run/s6/container_environment/KEYBOARD; fi
|
||||
grep -qxF "KEYBOARD=\"$KEYBOARD\"" ~/.bashrc 2>/dev/null || printf "%s\n" "KEYBOARD=\"$KEYBOARD\"" >> ~/.bashrc
|
||||
fi || true
|
||||
grep -qxF "KEYBOARD=\"$KEYBOARD\"" ~/.bashrc 2> /dev/null || printf "%s\n" "KEYBOARD=\"$KEYBOARD\"" >> ~/.bashrc
|
||||
fi
|
||||
|
||||
if bashio::config.has_value 'PASSWORD'; then
|
||||
bashio::log.info "Setting password to the value defined in options"
|
||||
PASSWORD=$(bashio::config 'PASSWORD')
|
||||
passwd -d abc
|
||||
echo -e "$PASSWORD\n$PASSWORD" | passwd abc
|
||||
elif ! bashio::config.has_value 'PASSWORD' && { [[ -n "$(bashio::addon.port "3000")" ]] || [[ -n "$(bashio::addon.port "3001")" ]] }; then
|
||||
elif [[ -n "$(bashio::addon.port "3000")" ]] || [[ -n "$(bashio::addon.port "3001")" ]]; then
|
||||
bashio::log.warning "SEVERE RISK IDENTIFIED"
|
||||
bashio::log.warning "You are opening an external port but your password is not defined"
|
||||
bashio::log.warning "You risk being hacked ! Please disable the external ports, or use a password"
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
#!/usr/bin/with-contenv bashio
|
||||
# shellcheck shell=bash
|
||||
set -e
|
||||
set -o pipefail
|
||||
|
||||
if ! bashio::config.true 'install_tokensave' || ! command -v git > /dev/null 2>&1; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
declare -A REPOS_SEEN=()
|
||||
while IFS= read -r configured_path; do
|
||||
configured_path="${configured_path#"${configured_path%%[![:space:]]*}"}"
|
||||
configured_path="${configured_path%"${configured_path##*[![:space:]]}"}"
|
||||
[ -n "$configured_path" ] || continue
|
||||
|
||||
case "$configured_path" in
|
||||
/*) ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
[ -d "$configured_path" ] || continue
|
||||
|
||||
# The one-shot safe.directory override is used only to discover the repository root.
|
||||
# Persist the resolved root in the shared runtime user's Git config before 82-claude_tools.sh
|
||||
# performs normal repository detection, avoiding Git's dubious-ownership rejection.
|
||||
repo_root="$(s6-setuidgid abc env HOME="$HOME" \
|
||||
git -c safe.directory='*' -C "$configured_path" rev-parse --show-toplevel 2> /dev/null || true)"
|
||||
[ -n "$repo_root" ] && [ "$repo_root" != "/" ] || continue
|
||||
[[ -z "${REPOS_SEEN[$repo_root]:-}" ]] || continue
|
||||
REPOS_SEEN[$repo_root]=1
|
||||
|
||||
if ! s6-setuidgid abc env HOME="$HOME" git config --global --get-all safe.directory \
|
||||
| grep -Fxq -- "$repo_root"; then
|
||||
s6-setuidgid abc env HOME="$HOME" git config --global --add safe.directory "$repo_root"
|
||||
bashio::log.info "Marked TokenSave repository as safe for Git: ${repo_root}"
|
||||
fi
|
||||
done < <(bashio::config.array 'tokensave_project_paths')
|
||||
@@ -3,17 +3,125 @@
|
||||
set -e
|
||||
set -o pipefail
|
||||
|
||||
PUID="$(if bashio::config.has_value 'PUID'; then bashio::config 'PUID'; else echo '0'; fi)"
|
||||
PGID="$(if bashio::config.has_value 'PGID'; then bashio::config 'PGID'; else echo '0'; fi)"
|
||||
mkdir -p "$HOME/.claude"
|
||||
CLAUDE_MD="$HOME/.claude/CLAUDE.md"
|
||||
|
||||
run_as_runtime_user() {
|
||||
s6-setuidgid abc env HOME="$HOME" "$@"
|
||||
}
|
||||
|
||||
CLAUDE_DESKTOP_COMMAND_FILE="/tmp/claude-desktop-command"
|
||||
DEFAULT_CLAUDE_DESKTOP_COMMAND='claude-desktop --no-sandbox --disable-dev-shm-usage --password-store=gnome-libsecret'
|
||||
printf '%s\n' "$DEFAULT_CLAUDE_DESKTOP_COMMAND" > "$CLAUDE_DESKTOP_COMMAND_FILE"
|
||||
# Managed, idempotent guidance block in the user's global CLAUDE.md, delimited by
|
||||
# "<!-- BEGIN/END <name> (managed by claude_desktop addon) -->" markers. `add` appends the
|
||||
# block (body on stdin) unless the marker is already present; `remove` strips the whole
|
||||
# block, surrounding blank padding included, and leaves everything else untouched.
|
||||
manage_claude_md_block() {
|
||||
local name="$1" action="$2"
|
||||
local begin="<!-- BEGIN ${name} (managed by claude_desktop addon) -->"
|
||||
if [ "$action" = "add" ]; then
|
||||
if ! { [ -f "$CLAUDE_MD" ] && grep -qF "$begin" "$CLAUDE_MD"; }; then
|
||||
bashio::log.info "Adding ${name} guidance to CLAUDE.md"
|
||||
mkdir -p "$(dirname "$CLAUDE_MD")"
|
||||
{
|
||||
[ -s "$CLAUDE_MD" ] && printf '\n'
|
||||
printf '%s\n' "$begin"
|
||||
cat
|
||||
printf '%s\n' "<!-- END ${name} (managed by claude_desktop addon) -->"
|
||||
} >> "$CLAUDE_MD"
|
||||
fi
|
||||
elif [ -f "$CLAUDE_MD" ] && grep -qF "$begin" "$CLAUDE_MD"; then
|
||||
bashio::log.info "Removing ${name} guidance from CLAUDE.md"
|
||||
CLAUDE_MD="$CLAUDE_MD" BLOCK_NAME="$name" python3 - <<'PY' || bashio::log.warning "Unable to remove the ${name} guidance automatically"
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
path = Path(os.environ["CLAUDE_MD"])
|
||||
name = re.escape(os.environ["BLOCK_NAME"])
|
||||
text = path.read_text(encoding="utf-8")
|
||||
pattern = re.compile(
|
||||
rf"\n*<!-- BEGIN {name} \(managed by claude_desktop addon\) -->.*?"
|
||||
rf"<!-- END {name} \(managed by claude_desktop addon\) -->\n?",
|
||||
re.DOTALL,
|
||||
)
|
||||
new = pattern.sub("", text)
|
||||
if new != text:
|
||||
path.write_text(new, encoding="utf-8")
|
||||
PY
|
||||
fi
|
||||
}
|
||||
|
||||
# Managed hook entry in ~/.claude/settings.json (settings hooks apply to terminal, cowork,
|
||||
# dispatch and cron sessions alike). The managed command is stripped everywhere first and
|
||||
# re-appended when adding, so one pass handles removal, de-duplication, and matcher migration
|
||||
# on upgrades; hooks owned by other tools (e.g. tokensave's own entries) are preserved, and
|
||||
# the final text comparison keeps the write idempotent across boots.
|
||||
manage_settings_hook() {
|
||||
# manage_settings_hook <event> <matcher> <command> <add|remove>
|
||||
HOOK_EVENT="$1" HOOK_MATCHER="$2" HOOK_COMMAND="$3" HOOK_ACTION="$4" \
|
||||
python3 - <<'PY' || bashio::log.warning "Unable to update the $1 hook for '$3'"
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
event = os.environ["HOOK_EVENT"]
|
||||
matcher = os.environ["HOOK_MATCHER"]
|
||||
command = os.environ["HOOK_COMMAND"]
|
||||
action = os.environ["HOOK_ACTION"]
|
||||
|
||||
path = Path.home() / ".claude" / "settings.json"
|
||||
original = path.read_text() if path.exists() else None
|
||||
if original is None and action != "add":
|
||||
raise SystemExit(0)
|
||||
try:
|
||||
data = json.loads(original) if original is not None else {}
|
||||
if not isinstance(data, dict):
|
||||
data = {}
|
||||
except Exception:
|
||||
if action != "add":
|
||||
raise SystemExit(0)
|
||||
path.rename(path.with_suffix(path.suffix + ".bak"))
|
||||
original = None
|
||||
data = {}
|
||||
|
||||
hooks = data.get("hooks") if isinstance(data.get("hooks"), dict) else {}
|
||||
entries = hooks.get(event) if isinstance(hooks.get(event), list) else []
|
||||
|
||||
filtered = []
|
||||
for entry in entries:
|
||||
if not isinstance(entry, dict) or not isinstance(entry.get("hooks"), list):
|
||||
filtered.append(entry)
|
||||
continue
|
||||
kept = [
|
||||
item
|
||||
for item in entry["hooks"]
|
||||
if not (isinstance(item, dict) and item.get("command") == command)
|
||||
]
|
||||
if len(kept) != len(entry["hooks"]):
|
||||
if not kept:
|
||||
continue
|
||||
entry = dict(entry)
|
||||
entry["hooks"] = kept
|
||||
filtered.append(entry)
|
||||
entries = filtered
|
||||
|
||||
if action == "add":
|
||||
entries.append({"matcher": matcher, "hooks": [{"type": "command", "command": command}]})
|
||||
|
||||
if entries:
|
||||
hooks[event] = entries
|
||||
else:
|
||||
hooks.pop(event, None)
|
||||
if hooks:
|
||||
data["hooks"] = hooks
|
||||
else:
|
||||
data.pop("hooks", None)
|
||||
|
||||
serialized = json.dumps(data, indent=2) + "\n"
|
||||
if serialized != original:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(serialized)
|
||||
PY
|
||||
}
|
||||
|
||||
# Headroom's proxy routing works by setting ANTHROPIC_BASE_URL, which the Claude Desktop
|
||||
# Electron app force-overrides to the production endpoint (headroom #869). Desktop therefore
|
||||
@@ -55,6 +163,136 @@ elif command -v tokensave &> /dev/null; then
|
||||
|| bashio::log.warning "tokensave Claude Code integration removal failed"
|
||||
fi
|
||||
|
||||
# Initialize or incrementally sync only explicitly configured repositories. TokenSave deliberately
|
||||
# requires one-time per-project opt-in; an empty list therefore has no startup or storage cost.
|
||||
# Runs before the MCP registration merge below on purpose: a first-time `tokensave init` also
|
||||
# rewrites ~/.claude.json itself (at default permissions), and the merge afterwards reconciles
|
||||
# the managed entries and re-tightens the file mode around the stored HA token.
|
||||
if $TOKENSAVE_ENABLED; then
|
||||
declare -A TOKENSAVE_REPOS_SEEN=()
|
||||
# Capture the list BEFORE looping: bashio::config's internals trip the errexit that
|
||||
# process substitution inherits from the bashio wrapper (a `read -d ''` that always
|
||||
# returns non-zero), so `done < <(bashio::config ...)` silently fed the loop an EMPTY
|
||||
# list — the startup index/sync never ran. Command substitution runs without errexit
|
||||
# (inherit_errexit is off), making this form reliable. bashio::config prints list
|
||||
# entries one per line, without a trailing newline and as "null" when the key is absent
|
||||
# (bashio::config.array only exists in the repo's standalone bashio, not the real one
|
||||
# here); the `|| [ -n ... ]` test keeps the final unterminated record in the loop.
|
||||
TOKENSAVE_PROJECT_PATHS="$(bashio::config 'tokensave_project_paths')"
|
||||
while IFS= read -r configured_path || [ -n "$configured_path" ]; do
|
||||
# Trim surrounding whitespace while preserving spaces inside paths.
|
||||
configured_path="${configured_path#"${configured_path%%[![:space:]]*}"}"
|
||||
configured_path="${configured_path%"${configured_path##*[![:space:]]}"}"
|
||||
if [ -z "$configured_path" ] || [ "$configured_path" = "null" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
case "$configured_path" in
|
||||
/*) ;;
|
||||
*)
|
||||
bashio::log.warning "Skipping non-absolute tokensave_project_paths entry: ${configured_path}"
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
if [ ! -d "$configured_path" ]; then
|
||||
bashio::log.warning "Skipping missing TokenSave project path: ${configured_path}"
|
||||
continue
|
||||
fi
|
||||
|
||||
# The one-shot safe.directory override is used only to discover the repository root.
|
||||
repo_root="$(run_as_runtime_user git -c safe.directory='*' -C "$configured_path" rev-parse --show-toplevel 2> /dev/null || true)"
|
||||
if [ -z "$repo_root" ] || [ "$repo_root" = "/" ]; then
|
||||
bashio::log.warning "Skipping TokenSave path that is not a supported Git repository: ${configured_path}"
|
||||
continue
|
||||
fi
|
||||
if [[ -n "${TOKENSAVE_REPOS_SEEN[$repo_root]:-}" ]]; then
|
||||
continue
|
||||
fi
|
||||
TOKENSAVE_REPOS_SEEN[$repo_root]=1
|
||||
|
||||
# Persist the resolved root in the runtime user's Git config so the sync/init below,
|
||||
# tokensave's git hooks, and Claude sessions all pass Git's dubious-ownership check.
|
||||
if ! run_as_runtime_user git config --global --get-all safe.directory \
|
||||
| grep -Fxq -- "$repo_root"; then
|
||||
run_as_runtime_user git config --global --add safe.directory "$repo_root"
|
||||
bashio::log.info "Marked TokenSave repository as safe for Git: ${repo_root}"
|
||||
fi
|
||||
|
||||
bashio::log.info "Preparing TokenSave index: ${repo_root}"
|
||||
# Prepare the per-repo semantic graph defensively so a hard add-on stop or storage
|
||||
# hiccup can never leave a broken index that fails every subsequent boot:
|
||||
# * a startup-scoped flock serializes against an overlapping restart (and any git
|
||||
# post-commit/checkout sync hook that fires mid-boot); waits up to 60s for the
|
||||
# other writer to finish rather than silently skipping, since a held lock clears
|
||||
# itself the moment its holder exits or dies (the kernel releases flock on exit);
|
||||
# * an existing index is refreshed with a cheap incremental `sync`, retried a few
|
||||
# times because SQLITE_BUSY under lock contention is transient, not corruption;
|
||||
# * quarantine is reserved for sync failures whose stderr actually names database
|
||||
# corruption (SQLite's own "malformed"/"not a database"/"disk image" wording) or
|
||||
# a half-written index from an interrupted `init` (sentinel-flagged). Any other
|
||||
# failure (permissions, disk full, missing binary, ...) leaves the existing index
|
||||
# untouched and simply retries on the next start — corruption should self-heal,
|
||||
# a transient environment problem should not nuke a healthy graph;
|
||||
# * `init` is bracketed by a sentinel file so an interrupted full build is detected
|
||||
# as incomplete on the next start and rebuilt rather than trusted.
|
||||
# All file operations run as the abc runtime user because the repo `.tokensave`
|
||||
# directory is not covered by the startup ownership pass.
|
||||
# shellcheck disable=SC2016 # single-quoted on purpose: $1/$db/etc. expand in the abc shell
|
||||
run_as_runtime_user bash -c '
|
||||
set -o pipefail
|
||||
repo_root="$1"
|
||||
ts_dir="$repo_root/.tokensave"
|
||||
db="$ts_dir/tokensave.db"
|
||||
lock="$ts_dir/.startup.lock"
|
||||
initflag="$ts_dir/.init-incomplete"
|
||||
mkdir -p "$ts_dir"
|
||||
exec 9>"$lock"
|
||||
if ! flock -w 60 9; then
|
||||
echo "TokenSave: index still locked for $repo_root after 60s; skipping startup sync" >&2
|
||||
exit 0
|
||||
fi
|
||||
is_corruption() {
|
||||
printf "%s" "$1" | grep -qiE "malformed|not a database|file is encrypted|disk image|database.*corrupt"
|
||||
}
|
||||
quarantine() {
|
||||
stamp="$(date +%Y%m%d-%H%M%S)"
|
||||
bdir="$ts_dir/corrupt-$stamp"
|
||||
mkdir -p "$bdir"
|
||||
for f in "$db" "$db-wal" "$db-shm"; do
|
||||
[ -e "$f" ] && mv -f "$f" "$bdir/" 2>/dev/null || true
|
||||
done
|
||||
echo "TokenSave: quarantined suspect index to $bdir" >&2
|
||||
}
|
||||
if [ -f "$db" ] && [ ! -f "$initflag" ]; then
|
||||
attempt=1
|
||||
while :; do
|
||||
sync_err="$(tokensave sync "$repo_root" 2>&1 1>/dev/null)" && exit 0
|
||||
[ "$attempt" -ge 3 ] && break
|
||||
echo "TokenSave: sync attempt $attempt failed for $repo_root; retrying" >&2
|
||||
attempt=$((attempt + 1))
|
||||
sleep 2
|
||||
done
|
||||
if is_corruption "$sync_err"; then
|
||||
echo "TokenSave: sync failed after retries for $repo_root (corruption detected); rebuilding index" >&2
|
||||
quarantine
|
||||
else
|
||||
echo "TokenSave: sync failed after retries for $repo_root (no corruption signature); leaving index in place, will retry next start" >&2
|
||||
echo "TokenSave: last sync error: $sync_err" >&2
|
||||
exit 1
|
||||
fi
|
||||
elif [ -f "$db" ]; then
|
||||
echo "TokenSave: previous init did not finish for $repo_root; rebuilding index" >&2
|
||||
quarantine
|
||||
fi
|
||||
: > "$initflag"
|
||||
tokensave init "$repo_root" && { rm -f "$initflag"; exit 0; }
|
||||
echo "TokenSave: init failed for $repo_root; will retry on next start" >&2
|
||||
exit 1
|
||||
' _ "$repo_root" \
|
||||
|| bashio::log.warning "TokenSave preparation failed for ${repo_root}"
|
||||
done <<< "$TOKENSAVE_PROJECT_PATHS"
|
||||
fi
|
||||
|
||||
HA_MCP_ENABLED=false
|
||||
HA_MCP_URL=""
|
||||
HA_MCP_TOKEN=""
|
||||
@@ -74,6 +312,7 @@ if bashio::config.true 'enable_ha_mcp'; then
|
||||
fi
|
||||
|
||||
HEADROOM_ENABLED="$HEADROOM_ENABLED" HEADROOM_BIN="$(command -v headroom || echo headroom)" \
|
||||
HEADROOM_HF_HOME="${HOME}/.headroom/hf" \
|
||||
TOKENSAVE_ENABLED="$TOKENSAVE_ENABLED" TOKENSAVE_BIN="$(command -v tokensave || echo tokensave)" \
|
||||
HA_MCP_ENABLED="$HA_MCP_ENABLED" HA_MCP_URL="$HA_MCP_URL" HA_MCP_TOKEN="$HA_MCP_TOKEN" \
|
||||
MCP_PROXY_BIN="$(command -v mcp-proxy || echo mcp-proxy)" \
|
||||
@@ -94,6 +333,11 @@ if os.environ["HEADROOM_ENABLED"] == "true":
|
||||
desired["headroom"] = {
|
||||
"command": os.environ["HEADROOM_BIN"],
|
||||
"args": ["mcp", "serve", "--proxy-url", "http://127.0.0.1:8787"],
|
||||
# The MCP server is a separate process from the svc-headroom proxy longrun and does
|
||||
# not inherit its HF_HOME export, so Kompress falls back to the default (tmpfs, wiped
|
||||
# every restart) cache dir, never finds the model, and silently no-ops every
|
||||
# compression request. Point it at the same persistent cache the proxy warms.
|
||||
"env": {"HF_HOME": os.environ["HEADROOM_HF_HOME"]},
|
||||
}
|
||||
if os.environ["TOKENSAVE_ENABLED"] == "true":
|
||||
desired["tokensave"] = {"command": os.environ["TOKENSAVE_BIN"], "args": ["serve"]}
|
||||
@@ -149,75 +393,24 @@ for config_var, stdio_type in (("CLAUDE_DESKTOP_CONFIG", False), ("CLAUDE_CODE_C
|
||||
elif existing is not None and is_managed(name, existing):
|
||||
del servers[name]
|
||||
changed = True
|
||||
if not changed:
|
||||
continue
|
||||
if servers:
|
||||
data["mcpServers"] = servers
|
||||
else:
|
||||
data.pop("mcpServers", None)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(data, indent=2) + "\n")
|
||||
# The Home Assistant long-lived access token is stored here in clear text.
|
||||
path.chmod(0o600)
|
||||
if changed:
|
||||
if servers:
|
||||
data["mcpServers"] = servers
|
||||
else:
|
||||
data.pop("mcpServers", None)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(data, indent=2) + "\n")
|
||||
# The Home Assistant long-lived access token is stored here in clear text. Enforced even
|
||||
# on no-change boots because tokensave's own writes can recreate the file with default
|
||||
# permissions between merges.
|
||||
if path.exists():
|
||||
path.chmod(0o600)
|
||||
PY
|
||||
|
||||
# Initialize or incrementally sync only explicitly configured repositories. TokenSave deliberately
|
||||
# requires one-time per-project opt-in; an empty list therefore has no startup or storage cost.
|
||||
if $TOKENSAVE_ENABLED; then
|
||||
declare -A TOKENSAVE_REPOS_SEEN=()
|
||||
while IFS= read -r configured_path; do
|
||||
# Trim surrounding whitespace while preserving spaces inside paths.
|
||||
configured_path="${configured_path#"${configured_path%%[![:space:]]*}"}"
|
||||
configured_path="${configured_path%"${configured_path##*[![:space:]]}"}"
|
||||
[ -n "$configured_path" ] || continue
|
||||
|
||||
case "$configured_path" in
|
||||
/*) ;;
|
||||
*)
|
||||
bashio::log.warning "Skipping non-absolute tokensave_project_paths entry: ${configured_path}"
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
if [ ! -d "$configured_path" ]; then
|
||||
bashio::log.warning "Skipping missing TokenSave project path: ${configured_path}"
|
||||
continue
|
||||
fi
|
||||
|
||||
repo_root="$(git -C "$configured_path" rev-parse --show-toplevel 2> /dev/null || true)"
|
||||
if [ -z "$repo_root" ] || [ "$repo_root" = "/" ]; then
|
||||
bashio::log.warning "Skipping TokenSave path that is not a supported Git repository: ${configured_path}"
|
||||
continue
|
||||
fi
|
||||
if [[ -n "${TOKENSAVE_REPOS_SEEN[$repo_root]:-}" ]]; then
|
||||
continue
|
||||
fi
|
||||
TOKENSAVE_REPOS_SEEN[$repo_root]=1
|
||||
|
||||
if [ -f "$repo_root/.tokensave/tokensave.db" ]; then
|
||||
bashio::log.info "Synchronizing TokenSave index: ${repo_root}"
|
||||
run_as_runtime_user tokensave sync "$repo_root" \
|
||||
|| bashio::log.warning "TokenSave sync failed for ${repo_root}"
|
||||
else
|
||||
bashio::log.info "Initializing TokenSave index: ${repo_root}"
|
||||
run_as_runtime_user tokensave init "$repo_root" \
|
||||
|| bashio::log.warning "TokenSave initialization failed for ${repo_root}"
|
||||
fi
|
||||
done < <(bashio::config.array 'tokensave_project_paths')
|
||||
fi
|
||||
|
||||
# Guide Claude to actually use the Headroom compression tools so the MCP integration produces
|
||||
# real savings when transparent proxying is unavailable. Managed, idempotent block appended to
|
||||
# the user's global CLAUDE.md; removed when Headroom is disabled.
|
||||
CLAUDE_MD="$HOME/.claude/CLAUDE.md"
|
||||
HEADROOM_GUIDE_BEGIN="<!-- BEGIN headroom (managed by claude_desktop addon) -->"
|
||||
# real savings when transparent proxying is unavailable.
|
||||
if $HEADROOM_ENABLED; then
|
||||
mkdir -p "$(dirname "$CLAUDE_MD")"
|
||||
if ! { [ -f "$CLAUDE_MD" ] && grep -qF "$HEADROOM_GUIDE_BEGIN" "$CLAUDE_MD"; }; then
|
||||
bashio::log.info "Adding headroom usage guidance to CLAUDE.md"
|
||||
{
|
||||
[ -s "$CLAUDE_MD" ] && printf '\n'
|
||||
cat <<'MD'
|
||||
<!-- BEGIN headroom (managed by claude_desktop addon) -->
|
||||
manage_claude_md_block headroom add <<'MD'
|
||||
## Headroom context compression
|
||||
|
||||
A local Headroom proxy (127.0.0.1:8787) backs the `headroom` MCP tools. To save context tokens:
|
||||
@@ -227,39 +420,32 @@ search results, JSON/config dumps, big command outputs, roughly >500 tokens —
|
||||
the raw content. Call `mcp__headroom__headroom_retrieve` with that hash when you need the full
|
||||
original back. Skip compression for error/stack-trace output (Headroom deliberately protects it)
|
||||
and for small or one-off content. Use `mcp__headroom__headroom_stats` to check savings.
|
||||
<!-- END headroom (managed by claude_desktop addon) -->
|
||||
MD
|
||||
} >> "$CLAUDE_MD"
|
||||
fi
|
||||
elif [ -f "$CLAUDE_MD" ] && grep -qF "$HEADROOM_GUIDE_BEGIN" "$CLAUDE_MD"; then
|
||||
bashio::log.info "Removing headroom usage guidance from CLAUDE.md"
|
||||
CLAUDE_MD="$CLAUDE_MD" python3 - <<'PY' || bashio::log.warning "Unable to remove headroom guidance automatically"
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
path = Path(os.environ["CLAUDE_MD"])
|
||||
text = path.read_text()
|
||||
pattern = re.compile(
|
||||
r"\n*<!-- BEGIN headroom \(managed by claude_desktop addon\) -->.*?"
|
||||
r"<!-- END headroom \(managed by claude_desktop addon\) -->\n?",
|
||||
re.DOTALL,
|
||||
)
|
||||
new = pattern.sub("", text)
|
||||
if new != text:
|
||||
path.write_text(new)
|
||||
PY
|
||||
else
|
||||
manage_claude_md_block headroom remove
|
||||
fi
|
||||
|
||||
if bashio::config.true 'install_rtk'; then
|
||||
if command -v rtk &> /dev/null; then
|
||||
bashio::log.info "Configuring rtk Claude Code integration"
|
||||
run_as_runtime_user env RTK_NONINTERACTIVE=1 rtk init -g \
|
||||
|| bashio::log.warning "rtk global files configuration failed"
|
||||
python3 - <<'PY' || bashio::log.warning "Unable to configure rtk hook automatically"
|
||||
# Route every Claude Code session through the Headroom proxy via the `env` block in the user's
|
||||
# ~/.claude/settings.json. Claude Code writes settings `env` entries into the process
|
||||
# environment at startup, replacing inherited values — this is the only supported way to reach
|
||||
# Desktop cowork/local-agent-mode sessions, which spawn the bundled CLI at an absolute path
|
||||
# (bypassing the PATH wrapper) with ANTHROPIC_BASE_URL pinned to the production endpoint
|
||||
# (headroom #869). Managed-value semantics: only set or remove the variable when it is absent
|
||||
# or already equals the add-on-managed proxy URL, so a user-customized endpoint is never
|
||||
# clobbered. The svc-headroom longrun is s6-supervised, so a crashed proxy restarts within
|
||||
# seconds; the terminal wrapper's per-launch health check remains as an extra safety net.
|
||||
if $HEADROOM_ENABLED && bashio::config.true 'headroom_wrap_claude_code'; then
|
||||
HEADROOM_ROUTE_ACTION="add"
|
||||
else
|
||||
HEADROOM_ROUTE_ACTION="remove"
|
||||
fi
|
||||
HEADROOM_ROUTE_ACTION="$HEADROOM_ROUTE_ACTION" python3 - <<'PY' || bashio::log.warning "Unable to manage the Claude Code proxy routing env"
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
MANAGED_URL = "http://127.0.0.1:8787"
|
||||
|
||||
path = Path.home() / ".claude" / "settings.json"
|
||||
try:
|
||||
data = json.loads(path.read_text()) if path.exists() else {}
|
||||
@@ -269,78 +455,96 @@ except Exception:
|
||||
if path.exists():
|
||||
path.rename(path.with_suffix(path.suffix + ".bak"))
|
||||
data = {}
|
||||
hooks = data.setdefault("hooks", {})
|
||||
pre = hooks.setdefault("PreToolUse", [])
|
||||
rtk_entry = {"matcher": "Bash", "hooks": [{"type": "command", "command": "rtk hook claude"}]}
|
||||
if not any("rtk hook claude" in json.dumps(entry) for entry in pre if isinstance(entry, dict)):
|
||||
pre.append(rtk_entry)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(data, indent=2) + "\n")
|
||||
|
||||
env = data.get("env")
|
||||
if not isinstance(env, dict):
|
||||
env = {}
|
||||
current = env.get("ANTHROPIC_BASE_URL")
|
||||
changed = False
|
||||
|
||||
if os.environ["HEADROOM_ROUTE_ACTION"] == "add":
|
||||
if current is None or current == MANAGED_URL:
|
||||
if current != MANAGED_URL:
|
||||
env["ANTHROPIC_BASE_URL"] = MANAGED_URL
|
||||
changed = True
|
||||
else:
|
||||
print(f"Claude settings env already sets ANTHROPIC_BASE_URL={current}; leaving it untouched")
|
||||
elif current == MANAGED_URL:
|
||||
del env["ANTHROPIC_BASE_URL"]
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
if env:
|
||||
data["env"] = env
|
||||
elif "env" in data:
|
||||
del data["env"]
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(data, indent=2) + "\n")
|
||||
PY
|
||||
|
||||
# Compress large tool outputs automatically in every Claude Code session via a managed
|
||||
# PostToolUse hook. Desktop-spawned sessions pin ANTHROPIC_BASE_URL to the production endpoint
|
||||
# (headroom #869) so the proxy never sees their traffic, and the CLAUDE.md guidance above only
|
||||
# helps when the model remembers to call the MCP tools. The hook closes that gap: outputs over
|
||||
# ~4000 chars from Bash/Grep/Glob/WebFetch are compressed with Headroom's rule-based pipeline
|
||||
# and swapped in through hookSpecificOutput.updatedToolOutput, with the original kept in the
|
||||
# shared CCR store so the model can fetch it back with mcp__headroom__headroom_retrieve. The
|
||||
# script fails open (any error leaves the tool output untouched) and its --self-test gate
|
||||
# keeps a broken interpreter path from registering a hook that would warn on every tool call.
|
||||
HEADROOM_HOOK_CMD="/usr/local/bin/headroom-posttooluse-compress.py"
|
||||
HEADROOM_HOOK_ACTION="remove"
|
||||
if $HEADROOM_ENABLED && bashio::config.true 'headroom_auto_compress'; then
|
||||
if run_as_runtime_user "$HEADROOM_HOOK_CMD" --self-test; then
|
||||
HEADROOM_HOOK_ACTION="add"
|
||||
bashio::log.info "Registering the Headroom PostToolUse auto-compression hook"
|
||||
else
|
||||
bashio::log.warning "headroom-posttooluse-compress.py --self-test failed; not registering the auto-compression hook"
|
||||
fi
|
||||
fi
|
||||
manage_settings_hook PostToolUse "Bash|Grep|Glob|WebFetch" "$HEADROOM_HOOK_CMD" "$HEADROOM_HOOK_ACTION"
|
||||
|
||||
# Tell Claude Code that it can configure Home Assistant over the Core API via the shipped
|
||||
# `ha-cli` helper (no /config filesystem mount needed).
|
||||
if bashio::config.true 'enable_ha_api_helper'; then
|
||||
manage_claude_md_block ha-api-helper add <<'MD'
|
||||
## Configuring Home Assistant
|
||||
|
||||
You can configure this Home Assistant instance through its Core API using the `ha-cli`
|
||||
command (on `PATH`). It authenticates automatically with the add-on's `$SUPERVISOR_TOKEN`,
|
||||
so no token setup is needed. There is **no `/config` filesystem mount** — work only through
|
||||
`ha-cli`, and never try to read or write Home Assistant YAML files directly.
|
||||
|
||||
What is editable this way: automations, scripts, and scenes
|
||||
(`ha-cli get|post|delete config/automation/config/<id>` and the `script`/`scene` equivalents);
|
||||
service calls (`ha-cli call <domain.service> '<json>'`); state reads (`ha-cli states`); and,
|
||||
over WebSocket, helpers, dashboards, and area/label/floor/entity registries
|
||||
(`ha-cli ws '{"type":"..."}'`). Run `ha-cli --help` for the full reference. Raw YAML
|
||||
(`configuration.yaml`, `secrets.yaml`) is intentionally unreachable — if a change needs it,
|
||||
say so instead of working around it.
|
||||
|
||||
Rules: run `ha-cli config` first to confirm connectivity; **read the current object and show
|
||||
the user the intended change, then wait for confirmation** before any create/update/delete or
|
||||
any state-changing `call`; after writing, read the object back and reload if needed
|
||||
(e.g. `ha-cli call automation.reload`).
|
||||
MD
|
||||
else
|
||||
manage_claude_md_block ha-api-helper remove
|
||||
fi
|
||||
|
||||
if bashio::config.true 'install_rtk'; then
|
||||
if command -v rtk &> /dev/null; then
|
||||
bashio::log.info "Configuring rtk Claude Code integration"
|
||||
# `rtk init -g` writes ~/.claude/RTK.md and its @RTK.md include in CLAUDE.md, but in
|
||||
# non-interactive mode it deliberately refuses to patch settings.json, so the hook
|
||||
# entry that actually rewrites Bash commands is registered here.
|
||||
run_as_runtime_user env RTK_NONINTERACTIVE=1 rtk init -g \
|
||||
|| bashio::log.warning "rtk global files configuration failed"
|
||||
manage_settings_hook PreToolUse Bash "rtk hook claude" add
|
||||
else
|
||||
bashio::log.warning "rtk is not available"
|
||||
fi
|
||||
elif [ -f "$HOME/.claude/settings.json" ]; then
|
||||
bashio::log.info "Removing the add-on-managed rtk Claude Code hook"
|
||||
python3 - <<'PY' || bashio::log.warning "Unable to remove rtk hook automatically"
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
path = Path.home() / ".claude" / "settings.json"
|
||||
data = json.loads(path.read_text())
|
||||
if not isinstance(data, dict):
|
||||
raise TypeError("Claude settings must contain a JSON object")
|
||||
|
||||
hooks = data.get("hooks")
|
||||
if not isinstance(hooks, dict):
|
||||
raise SystemExit(0)
|
||||
|
||||
entries = hooks.get("PreToolUse")
|
||||
if not isinstance(entries, list):
|
||||
raise SystemExit(0)
|
||||
|
||||
changed = False
|
||||
filtered_entries = []
|
||||
for entry in entries:
|
||||
if not isinstance(entry, dict) or entry.get("matcher") != "Bash":
|
||||
filtered_entries.append(entry)
|
||||
continue
|
||||
|
||||
commands = entry.get("hooks")
|
||||
if not isinstance(commands, list):
|
||||
filtered_entries.append(entry)
|
||||
continue
|
||||
|
||||
filtered_commands = [
|
||||
command
|
||||
for command in commands
|
||||
if not (
|
||||
isinstance(command, dict)
|
||||
and command.get("type") == "command"
|
||||
and command.get("command") == "rtk hook claude"
|
||||
)
|
||||
]
|
||||
if len(filtered_commands) == len(commands):
|
||||
filtered_entries.append(entry)
|
||||
continue
|
||||
|
||||
changed = True
|
||||
if filtered_commands:
|
||||
updated_entry = dict(entry)
|
||||
updated_entry["hooks"] = filtered_commands
|
||||
filtered_entries.append(updated_entry)
|
||||
|
||||
if changed:
|
||||
if filtered_entries:
|
||||
hooks["PreToolUse"] = filtered_entries
|
||||
else:
|
||||
hooks.pop("PreToolUse", None)
|
||||
if hooks:
|
||||
data["hooks"] = hooks
|
||||
else:
|
||||
data.pop("hooks", None)
|
||||
path.write_text(json.dumps(data, indent=2) + "\n")
|
||||
PY
|
||||
else
|
||||
manage_settings_hook PreToolUse Bash "rtk hook claude" remove
|
||||
fi
|
||||
|
||||
if bashio::config.true 'install_caveman'; then
|
||||
@@ -356,10 +560,5 @@ else
|
||||
find "$HOME/.claude" -maxdepth 4 -iname '*caveman*' -exec rm -rf {} + 2> /dev/null || true
|
||||
fi
|
||||
|
||||
# Startup configuration runs as root, while Claude Desktop runs as abc. Return managed
|
||||
# persistent files to the configured runtime UID/GID after all writes complete.
|
||||
for managed_path in "$HOME/.claude" "$HOME/.claude.json" "$HOME/.config/Claude"; do
|
||||
if [ -e "$managed_path" ]; then
|
||||
chown -R -- "${PUID}:${PGID}" "$managed_path" || bashio::log.warning "Unable to set ownership on $managed_path"
|
||||
fi
|
||||
done
|
||||
# Ownership of everything written above is reconciled by 84-claude_runtime_ownership.sh after
|
||||
# the remaining Claude configuration scripts have run.
|
||||
|
||||
@@ -3,29 +3,31 @@
|
||||
set -e
|
||||
set -o pipefail
|
||||
|
||||
PUID="$(if bashio::config.has_value 'PUID'; then bashio::config 'PUID'; else echo '0'; fi)"
|
||||
PGID="$(if bashio::config.has_value 'PGID'; then bashio::config 'PGID'; else echo '0'; fi)"
|
||||
PERMISSION_MODE="$(bashio::config 'permission_mode')"
|
||||
SETTINGS_PATH="$HOME/.claude/settings.json"
|
||||
STATE_PATH="$HOME/.claude/.addon-permission-mode.json"
|
||||
|
||||
case "$PERMISSION_MODE" in
|
||||
strict|auto|bypass) ;;
|
||||
strict | auto | bypass) ;;
|
||||
*)
|
||||
bashio::log.warning "Unknown permission_mode '${PERMISSION_MODE}'; falling back to strict"
|
||||
PERMISSION_MODE="strict"
|
||||
;;
|
||||
esac
|
||||
|
||||
# Managed-value semantics, matching the ANTHROPIC_BASE_URL handling in 82-claude_tools.sh:
|
||||
# auto/bypass set permissions.defaultMode to the add-on-managed value, and strict removes it
|
||||
# only while it still holds one of those managed values — a defaultMode the user set by hand
|
||||
# is never deleted. Ownership of the written file is reconciled by 84-claude_runtime_ownership.sh.
|
||||
mkdir -p "$(dirname "$SETTINGS_PATH")"
|
||||
PERMISSION_MODE="$PERMISSION_MODE" SETTINGS_PATH="$SETTINGS_PATH" STATE_PATH="$STATE_PATH" python3 - <<'PY'
|
||||
PERMISSION_MODE="$PERMISSION_MODE" SETTINGS_PATH="$SETTINGS_PATH" python3 - <<'PY'
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
MANAGED_VALUES = {"auto", "bypassPermissions"}
|
||||
|
||||
mode = os.environ["PERMISSION_MODE"]
|
||||
settings_path = Path(os.environ["SETTINGS_PATH"])
|
||||
state_path = Path(os.environ["STATE_PATH"])
|
||||
|
||||
try:
|
||||
settings = json.loads(settings_path.read_text()) if settings_path.exists() else {}
|
||||
@@ -36,33 +38,14 @@ except (OSError, json.JSONDecodeError):
|
||||
if not isinstance(settings, dict):
|
||||
settings = {}
|
||||
|
||||
try:
|
||||
state = json.loads(state_path.read_text()) if state_path.exists() else None
|
||||
except (OSError, json.JSONDecodeError):
|
||||
state = None
|
||||
if not isinstance(state, dict):
|
||||
state = None
|
||||
|
||||
permissions = settings.get("permissions")
|
||||
if not isinstance(permissions, dict):
|
||||
permissions = {}
|
||||
|
||||
if mode == "strict":
|
||||
# Restore the value that existed before the add-on first managed this setting.
|
||||
if state is not None:
|
||||
if state.get("previous_exists"):
|
||||
permissions["defaultMode"] = state.get("previous_value")
|
||||
else:
|
||||
permissions.pop("defaultMode", None)
|
||||
state_path.unlink(missing_ok=True)
|
||||
if permissions.get("defaultMode") in MANAGED_VALUES:
|
||||
permissions.pop("defaultMode")
|
||||
else:
|
||||
if state is None:
|
||||
state = {
|
||||
"previous_exists": "defaultMode" in permissions,
|
||||
"previous_value": permissions.get("defaultMode"),
|
||||
}
|
||||
state_path.write_text(json.dumps(state, indent=2) + "\n")
|
||||
state_path.chmod(0o600)
|
||||
permissions["defaultMode"] = "auto" if mode == "auto" else "bypassPermissions"
|
||||
|
||||
if permissions:
|
||||
@@ -74,6 +57,9 @@ settings_path.write_text(json.dumps(settings, indent=2) + "\n")
|
||||
settings_path.chmod(0o600)
|
||||
PY
|
||||
|
||||
# Drop the state file older add-on versions used to remember the pre-add-on defaultMode.
|
||||
rm -f "$HOME/.claude/.addon-permission-mode.json"
|
||||
|
||||
case "$PERMISSION_MODE" in
|
||||
strict)
|
||||
bashio::log.info "Claude Code permission mode: strict (normal prompts)"
|
||||
@@ -85,8 +71,3 @@ case "$PERMISSION_MODE" in
|
||||
bashio::log.warning "Claude Code permission mode: bypass (permission checks disabled for mounted data and available tools)"
|
||||
;;
|
||||
esac
|
||||
|
||||
chown -- "${PUID}:${PGID}" "$SETTINGS_PATH" 2> /dev/null || true
|
||||
if [ -e "$STATE_PATH" ]; then
|
||||
chown -- "${PUID}:${PGID}" "$STATE_PATH" 2> /dev/null || true
|
||||
fi
|
||||
|
||||
@@ -17,25 +17,54 @@ if ! command -v gh > /dev/null 2>&1; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Everything below writes into the abc runtime user's HOME, so it must run AS abc. cont-init
|
||||
# runs as root with HOME already pointing at the persistent data location, so plain
|
||||
# `git config --global` recreated ~/.gitconfig owned by root:root on every start — and because
|
||||
# that file is rewritten each boot, 20-folders.sh's earlier recursive chown never stuck to it.
|
||||
# The user who actually runs git, gh and Claude was then unable to read its own committer
|
||||
# identity or the gh credential helper, so every commit failed with "Author identity unknown"
|
||||
# and authenticated pushes fell back to prompting. 20-folders.sh already remapped abc to the
|
||||
# effective runtime identity (never root in bypass mode), so follow abc rather than re-reading
|
||||
# the raw PUID/PGID options here.
|
||||
RUNTIME_UID="$(id -u abc)"
|
||||
RUNTIME_GID="$(id -g abc)"
|
||||
|
||||
run_as_runtime_user() {
|
||||
s6-setuidgid abc env HOME="$HOME" "$@"
|
||||
}
|
||||
|
||||
# Reclaim any root-owned copies left by an earlier add-on version before writing as abc:
|
||||
# these paths are not covered by 82-claude_tools.sh's ownership pass, and a root-owned
|
||||
# ~/.gitconfig would make the first `git config` below fail outright under `set -e`.
|
||||
|
||||
mkdir -p "$HOME/.config"
|
||||
chown -- "${RUNTIME_UID}:${RUNTIME_GID}" "$HOME/.config"
|
||||
|
||||
for managed_path in "$HOME/.gitconfig" "$HOME/.config/gh"; do
|
||||
if [ -e "$managed_path" ]; then
|
||||
chown -R -- "${RUNTIME_UID}:${RUNTIME_GID}" "$managed_path" || bashio::log.warning "Unable to set ownership on $managed_path"
|
||||
fi
|
||||
done
|
||||
|
||||
if bashio::config.has_value 'github_username'; then
|
||||
git config --global user.name "$(bashio::config 'github_username')"
|
||||
run_as_runtime_user git config --global user.name "$(bashio::config 'github_username')"
|
||||
fi
|
||||
|
||||
if bashio::config.has_value 'github_email'; then
|
||||
git config --global user.email "$(bashio::config 'github_email')"
|
||||
run_as_runtime_user git config --global user.email "$(bashio::config 'github_email')"
|
||||
fi
|
||||
|
||||
if bashio::config.has_value 'github_token'; then
|
||||
token="$(bashio::config 'github_token')"
|
||||
mkdir -p "$HOME/.config/gh"
|
||||
chmod 700 "$HOME/.config/gh"
|
||||
if env -u GH_TOKEN -u GITHUB_TOKEN gh auth status --hostname github.com > /dev/null 2>&1; then
|
||||
run_as_runtime_user mkdir -p "$HOME/.config/gh"
|
||||
run_as_runtime_user chmod 700 "$HOME/.config/gh"
|
||||
if run_as_runtime_user env -u GH_TOKEN -u GITHUB_TOKEN gh auth status --hostname github.com > /dev/null 2>&1; then
|
||||
bashio::log.info "GitHub CLI already authenticated for github.com"
|
||||
else
|
||||
bashio::log.info "Configuring GitHub CLI authentication for github.com"
|
||||
printf '%s\n' "$token" | env -u GH_TOKEN -u GITHUB_TOKEN gh auth login --hostname github.com --with-token || bashio::log.warning "GitHub CLI authentication failed"
|
||||
printf '%s\n' "$token" | run_as_runtime_user env -u GH_TOKEN -u GITHUB_TOKEN gh auth login --hostname github.com --with-token || bashio::log.warning "GitHub CLI authentication failed"
|
||||
fi
|
||||
env -u GH_TOKEN -u GITHUB_TOKEN gh auth setup-git --hostname github.com || bashio::log.warning "GitHub CLI git credential setup failed"
|
||||
run_as_runtime_user env -u GH_TOKEN -u GITHUB_TOKEN gh auth setup-git --hostname github.com || bashio::log.warning "GitHub CLI git credential setup failed"
|
||||
else
|
||||
bashio::log.info "GitHub CLI available. Set github_token to authenticate gh and git operations."
|
||||
fi
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
# shellcheck shell=bash
|
||||
set -e
|
||||
|
||||
# Earlier configuration scripts intentionally run as root and may use the configured PUID/PGID
|
||||
# values when returning files to the runtime user. In bypass mode PUID can still be configured as
|
||||
# 0 even though 19-claude_bypass_runtime.sh remapped abc to a non-root UID. Reconcile ownership
|
||||
# with the effective desktop identity after all Claude configuration writes are complete.
|
||||
# Earlier configuration scripts intentionally run as root. 20-folders.sh remapped abc to the
|
||||
# effective runtime identity (never root in bypass mode, where Claude Code refuses to run as
|
||||
# root). Reconcile ownership with that identity after all Claude configuration writes are
|
||||
# complete, as a safety net in case any intermediate step re-owned a managed path.
|
||||
RUNTIME_UID="$(id -u abc)"
|
||||
RUNTIME_GID="$(id -g abc)"
|
||||
|
||||
|
||||
@@ -10,6 +10,21 @@ if bashio::config.true 'expose_headroom_dashboard'; then
|
||||
fi
|
||||
|
||||
if bashio::config.true 'install_headroom' && command -v headroom > /dev/null 2>&1; then
|
||||
# Kompress (the ONNX compression engine) needs its model in the local HF cache: the
|
||||
# proxy's startup preload is deliberately cache-only, and the default HF cache lands
|
||||
# under ~/.cache, which the add-on points at tmpfs (/tmp/cache) — wiped on every
|
||||
# restart. Without a warm persistent cache the proxy ran forever in "deferred" mode
|
||||
# and recorded zero compression savings. Point the cache at persistent storage;
|
||||
# nothing else is needed here — the proxy's own request path already downloads a
|
||||
# missing model in the background on first use (ensure_background_load) and passes
|
||||
# requests through uncompressed until it lands, so this self-heals within a couple of
|
||||
# requests on the first boot and loads instantly (eager preload) on every boot after.
|
||||
# A synchronous pre-warm was tried here and removed: it blocked the port bind for up
|
||||
# to the download's duration, which left the settings-managed ANTHROPIC_BASE_URL
|
||||
# (see 82-claude_tools.sh) pointing at a proxy that wasn't listening yet.
|
||||
export HF_HOME="${HOME}/.headroom/hf"
|
||||
mkdir -p "$HF_HOME"
|
||||
chown abc:abc "$HF_HOME" 2> /dev/null || true
|
||||
bashio::log.info "svc-headroom: starting local Headroom proxy on ${host}:${port}"
|
||||
exec s6-setuidgid abc headroom proxy --host "${host}" --port "${port}" --code-aware
|
||||
fi
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
set -o pipefail
|
||||
|
||||
REAL_CLAUDE="/usr/bin/claude"
|
||||
HEADROOM_BIN="/usr/local/bin/headroom"
|
||||
HEADROOM_BIN="$(command -v headroom || true)"
|
||||
HEADROOM_URL="http://127.0.0.1:8787"
|
||||
PERMISSION_MODE="$(bashio::config 'permission_mode')"
|
||||
declare -a CLAUDE_PERMISSION_ARGS=()
|
||||
|
||||
@@ -56,7 +56,6 @@ else:
|
||||
print(f"permissions.defaultMode: {permissions.get('defaultMode', '<upstream default>')}")
|
||||
else:
|
||||
print("permissions: INVALID")
|
||||
print(f"managed-state marker: {(Path.home() / '.claude/.addon-permission-mode.json').exists()}")
|
||||
PY
|
||||
|
||||
section "MCP registrations (environment values redacted)"
|
||||
@@ -149,8 +148,13 @@ section "TokenSave"
|
||||
if bashio::config.true 'install_tokensave'; then
|
||||
tokensave doctor --agent claude || true
|
||||
tokensave gain --all --range 30d || true
|
||||
while IFS= read -r configured_path; do
|
||||
[ -n "$configured_path" ] || continue
|
||||
# Capture before looping — see the matching comment in 82-claude_tools.sh: feeding the
|
||||
# loop straight from `< <(bashio::config ...)` yields an empty list under errexit.
|
||||
TOKENSAVE_PROJECT_PATHS="$(bashio::config 'tokensave_project_paths')"
|
||||
while IFS= read -r configured_path || [ -n "$configured_path" ]; do
|
||||
if [ -z "$configured_path" ] || [ "$configured_path" = "null" ]; then
|
||||
continue
|
||||
fi
|
||||
repo_root="$(s6-setuidgid abc env HOME="$HOME" git -c safe.directory='*' -C "$configured_path" rev-parse --show-toplevel 2> /dev/null || true)"
|
||||
if [ -z "$repo_root" ]; then
|
||||
echo "${configured_path}: not a Git repository"
|
||||
@@ -159,7 +163,7 @@ if bashio::config.true 'install_tokensave'; then
|
||||
else
|
||||
echo "${repo_root}: NOT INITIALIZED"
|
||||
fi
|
||||
done < <(bashio::config.array 'tokensave_project_paths')
|
||||
done <<< "$TOKENSAVE_PROJECT_PATHS"
|
||||
else
|
||||
echo "disabled"
|
||||
fi
|
||||
|
||||
236
claude_desktop/rootfs/usr/local/bin/ha-cli
Executable file
236
claude_desktop/rootfs/usr/local/bin/ha-cli
Executable file
@@ -0,0 +1,236 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ha-cli — talk to the local Home Assistant Core API from inside the add-on.
|
||||
|
||||
Lets Claude Code configure Home Assistant (automations, scripts, scenes,
|
||||
helpers, dashboards, areas/labels, service calls) through the API, without any
|
||||
`/config` filesystem mount. `secrets.yaml` and other add-ons' credentials are
|
||||
therefore never reachable.
|
||||
|
||||
Authentication and the base URL are resolved automatically, in this order:
|
||||
|
||||
1. $HA_BASE_URL + $HA_TOKEN explicit override (advanced/scoped)
|
||||
2. `ha_mcp_token` in /data/options.json scoped user long-lived token -> :8123
|
||||
3. $SUPERVISOR_TOKEN Supervisor Core-API proxy fallback
|
||||
(admin-equivalent; needs
|
||||
homeassistant_api: true, which this
|
||||
add-on sets — zero setup)
|
||||
|
||||
The scoped token is checked before the Supervisor fallback so setting
|
||||
`ha_mcp_token` actually narrows access instead of being shadowed by the
|
||||
always-present admin-equivalent Supervisor token.
|
||||
|
||||
Subcommands:
|
||||
ha-cli get <path> GET e.g. get config/automation/config/1700000000
|
||||
ha-cli post <path> [BODY] POST BODY = inline JSON, @file, or - (stdin)
|
||||
ha-cli delete <path> DELETE
|
||||
ha-cli call <domain.service> [BODY] call a service (BODY = JSON service data)
|
||||
ha-cli states [entity_id] all states, or one entity
|
||||
ha-cli config GET /config (sanity check / core info)
|
||||
ha-cli ws <BODY> one WebSocket command (BODY = JSON, @file, or -)
|
||||
|
||||
`<path>` is relative to the REST API root; a leading slash and/or `api/` prefix
|
||||
are optional. Responses are printed as formatted JSON. Exit code is non-zero on
|
||||
HTTP or API errors.
|
||||
|
||||
Use the WebSocket subcommand for things the REST API does not expose:
|
||||
ha-cli ws '{"type":"config/area_registry/list"}'
|
||||
ha-cli ws '{"type":"input_boolean/create","name":"Guest mode","icon":"mdi:account"}'
|
||||
ha-cli ws '{"type":"lovelace/config","url_path":null}'
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
|
||||
def _load_option(name):
|
||||
"""Read a single option from the add-on's /data/options.json, if present."""
|
||||
try:
|
||||
with open("/data/options.json", encoding="utf-8") as handle:
|
||||
return json.load(handle).get(name)
|
||||
except (OSError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def _helper_enabled():
|
||||
"""Mirror config.yaml's enable_ha_api_helper default (true) when unset."""
|
||||
value = _load_option("enable_ha_api_helper")
|
||||
return value is not False
|
||||
|
||||
|
||||
def resolve_endpoint():
|
||||
"""Return (rest_base, ws_url, token) for the best available auth path."""
|
||||
base = os.environ.get("HA_BASE_URL")
|
||||
token = os.environ.get("HA_TOKEN")
|
||||
if base and token:
|
||||
rest = base.rstrip("/")
|
||||
if not rest.endswith("/api"):
|
||||
rest += "/api"
|
||||
ws = rest.replace("http", "ws", 1).rsplit("/api", 1)[0] + "/api/websocket"
|
||||
return rest, ws, token
|
||||
|
||||
# Checked before SUPERVISOR_TOKEN: this add-on always sets homeassistant_api,
|
||||
# so the admin-equivalent Supervisor token is otherwise always present and
|
||||
# would shadow a user's deliberately scoped-down ha_mcp_token.
|
||||
token = _load_option("ha_mcp_token")
|
||||
if token:
|
||||
return (
|
||||
"http://homeassistant:8123/api",
|
||||
"ws://homeassistant:8123/api/websocket",
|
||||
token,
|
||||
)
|
||||
|
||||
token = os.environ.get("SUPERVISOR_TOKEN")
|
||||
if token:
|
||||
return "http://supervisor/core/api", "ws://supervisor/core/websocket", token
|
||||
|
||||
sys.exit(
|
||||
"ha-cli: no credentials. Expected ha_mcp_token in the add-on options "
|
||||
"(scoped user), $SUPERVISOR_TOKEN (admin-equivalent fallback, default "
|
||||
"inside the add-on), or an explicit $HA_BASE_URL+$HA_TOKEN override."
|
||||
)
|
||||
|
||||
|
||||
def _url(base, path):
|
||||
path = path.lstrip("/")
|
||||
if path.startswith("api/"):
|
||||
path = path[len("api/"):]
|
||||
return base.rstrip("/") + "/" + path
|
||||
|
||||
|
||||
def _read_body(arg):
|
||||
"""Resolve an inline-JSON / @file / - (stdin) body argument to a dict/list."""
|
||||
if arg is None:
|
||||
return None
|
||||
if arg == "-":
|
||||
raw = sys.stdin.read()
|
||||
elif arg.startswith("@"):
|
||||
with open(arg[1:], encoding="utf-8") as handle:
|
||||
raw = handle.read()
|
||||
else:
|
||||
raw = arg
|
||||
raw = raw.strip()
|
||||
if not raw:
|
||||
return None
|
||||
try:
|
||||
return json.loads(raw)
|
||||
except ValueError as exc:
|
||||
sys.exit(f"ha-cli: body is not valid JSON: {exc}")
|
||||
|
||||
|
||||
def _print(obj):
|
||||
if isinstance(obj, (dict, list)):
|
||||
print(json.dumps(obj, indent=2, ensure_ascii=False))
|
||||
elif obj not in (None, ""):
|
||||
print(obj)
|
||||
|
||||
|
||||
def rest(method, base, token, path, body=None):
|
||||
data = None
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
if body is not None:
|
||||
data = json.dumps(body).encode("utf-8")
|
||||
headers["Content-Type"] = "application/json"
|
||||
req = urllib.request.Request(_url(base, path), data=data, method=method, headers=headers)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
text = resp.read().decode("utf-8")
|
||||
except urllib.error.HTTPError as exc:
|
||||
detail = exc.read().decode("utf-8", "replace").strip()
|
||||
sys.exit(f"ha-cli: HTTP {exc.code} {exc.reason} on {method} {path}\n{detail}")
|
||||
except urllib.error.URLError as exc:
|
||||
sys.exit(f"ha-cli: cannot reach Home Assistant ({exc.reason}) on {method} {path}")
|
||||
try:
|
||||
return json.loads(text) if text.strip() else None
|
||||
except ValueError:
|
||||
return text
|
||||
|
||||
|
||||
def ws_command(ws_url, token, command):
|
||||
try:
|
||||
import asyncio
|
||||
|
||||
import websockets
|
||||
except ImportError:
|
||||
sys.exit(
|
||||
"ha-cli: the 'websockets' Python package is required for the ws "
|
||||
"subcommand. REST subcommands work without it."
|
||||
)
|
||||
|
||||
async def run():
|
||||
async with websockets.connect(ws_url, max_size=None) as sock:
|
||||
hello = json.loads(await sock.recv())
|
||||
if hello.get("type") != "auth_required":
|
||||
raise SystemExit(f"ha-cli: unexpected WS greeting: {hello}")
|
||||
await sock.send(json.dumps({"type": "auth", "access_token": token}))
|
||||
if json.loads(await sock.recv()).get("type") != "auth_ok":
|
||||
raise SystemExit("ha-cli: WebSocket authentication failed")
|
||||
payload = dict(command)
|
||||
payload["id"] = 1
|
||||
await sock.send(json.dumps(payload))
|
||||
while True:
|
||||
msg = json.loads(await sock.recv())
|
||||
if msg.get("id") == 1 and msg.get("type") == "result":
|
||||
return msg
|
||||
|
||||
result = asyncio.run(run())
|
||||
if not result.get("success", True):
|
||||
_print(result.get("error", result))
|
||||
sys.exit(1)
|
||||
return result.get("result", result)
|
||||
|
||||
|
||||
def main(argv):
|
||||
if not argv or argv[0] in ("-h", "--help", "help"):
|
||||
print(__doc__)
|
||||
return 0
|
||||
|
||||
if not _helper_enabled():
|
||||
sys.exit(
|
||||
"ha-cli: disabled (enable_ha_api_helper is false in the add-on "
|
||||
"options). Enable it there to let Claude configure Home Assistant."
|
||||
)
|
||||
|
||||
rest_base, ws_url, token = resolve_endpoint()
|
||||
cmd, args = argv[0], argv[1:]
|
||||
|
||||
if cmd == "get":
|
||||
if len(args) != 1:
|
||||
sys.exit("usage: ha-cli get <path>")
|
||||
_print(rest("GET", rest_base, token, args[0]))
|
||||
elif cmd == "post":
|
||||
if not args:
|
||||
sys.exit("usage: ha-cli post <path> [BODY]")
|
||||
body = _read_body(args[1]) if len(args) > 1 else None
|
||||
_print(rest("POST", rest_base, token, args[0], body))
|
||||
elif cmd == "delete":
|
||||
if len(args) != 1:
|
||||
sys.exit("usage: ha-cli delete <path>")
|
||||
_print(rest("DELETE", rest_base, token, args[0]))
|
||||
elif cmd == "call":
|
||||
if not args or "." not in args[0]:
|
||||
sys.exit("usage: ha-cli call <domain.service> [BODY]")
|
||||
domain, service = args[0].split(".", 1)
|
||||
body = _read_body(args[1]) if len(args) > 1 else None
|
||||
_print(rest("POST", rest_base, token, f"services/{domain}/{service}", body or {}))
|
||||
elif cmd == "states":
|
||||
path = f"states/{args[0]}" if args else "states"
|
||||
_print(rest("GET", rest_base, token, path))
|
||||
elif cmd == "config":
|
||||
_print(rest("GET", rest_base, token, "config"))
|
||||
elif cmd == "ws":
|
||||
if len(args) != 1:
|
||||
sys.exit("usage: ha-cli ws <BODY>")
|
||||
command = _read_body(args[0])
|
||||
if not isinstance(command, dict) or "type" not in command:
|
||||
sys.exit('ha-cli: ws BODY must be a JSON object with a "type" field')
|
||||
_print(ws_command(ws_url, token, command))
|
||||
else:
|
||||
sys.exit(f"ha-cli: unknown subcommand '{cmd}' (try: ha-cli --help)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv[1:]))
|
||||
218
claude_desktop/rootfs/usr/local/bin/headroom-posttooluse-compress.py
Executable file
218
claude_desktop/rootfs/usr/local/bin/headroom-posttooluse-compress.py
Executable file
@@ -0,0 +1,218 @@
|
||||
#!/lsiopy/bin/python3
|
||||
"""Claude Code PostToolUse hook: auto-compress large tool outputs through Headroom.
|
||||
|
||||
Registered in ~/.claude/settings.json by 82-claude_tools.sh (managed entry, matcher
|
||||
"Bash|Grep|Glob|WebFetch"). Desktop-spawned Claude Code sessions cannot be routed
|
||||
through the Headroom proxy (the Electron app pins ANTHROPIC_BASE_URL to the
|
||||
production endpoint, headroom #869), so compression there used to depend on the
|
||||
model voluntarily calling the headroom MCP tools. This hook makes it automatic for
|
||||
every session type: when a matched tool returns a large output, the hook compresses
|
||||
it with Headroom's rule-based pipeline and replaces the tool output via
|
||||
hookSpecificOutput.updatedToolOutput, appending a retrieval marker. The original is
|
||||
stored in Headroom's shared CCR store (SQLite at ~/.headroom/ccr_store.db — the
|
||||
same store the headroom MCP server reads), so the model can always get the full
|
||||
output back with mcp__headroom__headroom_retrieve.
|
||||
|
||||
Design constraints:
|
||||
- Fail open: any error or non-compressible payload exits 0 with no output, leaving
|
||||
the tool result untouched. A hook crash must never break a session.
|
||||
- Fast path first: the payload is inspected before importing headroom (~0.6 s);
|
||||
small outputs never pay the import cost.
|
||||
- ML text compression (Kompress) is disabled: its model loads in the background,
|
||||
which never completes inside a short-lived hook process. The rule-based
|
||||
transforms (SmartCrusher for JSON, search/log/diff/tabular compressors) carry
|
||||
the savings on tool output anyway; plain prose passes through unchanged.
|
||||
- stderr fields are never compressed — error text must reach the model verbatim
|
||||
(matching Headroom's own error-protection policy).
|
||||
- File-list arrays (Glob's `filenames`, Grep's `filenames` in files_with_matches
|
||||
mode — both typed `string[]` by the CLI's own output schema) are handled
|
||||
separately from prose/JSON-blob fields: Headroom's SmartCrusher subsamples
|
||||
JSON arrays for informational dumps, which is fine for e.g. a list of sensor
|
||||
states but silently drops most paths from a file listing the model needs to
|
||||
act on. Those fields are truncated deterministically instead (keep the first
|
||||
N entries, append one marker string) so the model always sees a labeled cut
|
||||
point rather than a shorter list it might mistake for the complete result.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def _int_env(name: str, default: str) -> int:
|
||||
try:
|
||||
return int(os.environ.get(name, default))
|
||||
except (TypeError, ValueError):
|
||||
return int(default)
|
||||
|
||||
|
||||
MIN_CHARS = _int_env("HEADROOM_HOOK_MIN_CHARS", "4000")
|
||||
MIN_SAVED_TOKENS = _int_env("HEADROOM_HOOK_MIN_SAVED_TOKENS", "50")
|
||||
ARRAY_KEEP = _int_env("HEADROOM_HOOK_ARRAY_KEEP", "40")
|
||||
TTL_SECONDS = 3600 # matches the headroom MCP server's session TTL
|
||||
SKIP_KEYS = {"stderr"}
|
||||
|
||||
|
||||
def self_test() -> int:
|
||||
"""Exit 0 when the interpreter can import headroom (used at registration time)."""
|
||||
try:
|
||||
import headroom # noqa: F401
|
||||
|
||||
return 0
|
||||
except Exception:
|
||||
return 1
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if os.environ.get("HEADROOM_HOOK_DISABLE"):
|
||||
return 0
|
||||
try:
|
||||
payload = json.load(sys.stdin)
|
||||
except Exception:
|
||||
return 0
|
||||
if not isinstance(payload, dict):
|
||||
return 0
|
||||
response = payload.get("tool_response")
|
||||
|
||||
# Find big string/array fields before paying the headroom import cost.
|
||||
def is_string_array(value):
|
||||
return isinstance(value, list) and len(value) > ARRAY_KEEP and all(isinstance(v, str) for v in value)
|
||||
|
||||
if isinstance(response, str):
|
||||
string_candidates = ["__whole__"] if len(response) >= MIN_CHARS else []
|
||||
array_candidates = []
|
||||
elif isinstance(response, dict):
|
||||
string_candidates = [
|
||||
key
|
||||
for key, value in response.items()
|
||||
if key not in SKIP_KEYS and isinstance(value, str) and len(value) >= MIN_CHARS
|
||||
]
|
||||
array_candidates = [
|
||||
key for key, value in response.items() if key not in SKIP_KEYS and is_string_array(value)
|
||||
]
|
||||
else:
|
||||
string_candidates = []
|
||||
array_candidates = []
|
||||
if not string_candidates and not array_candidates:
|
||||
return 0
|
||||
|
||||
# Keep Kompress's cache probe away from the tmpfs-backed ~/.cache default.
|
||||
os.environ.setdefault("HF_HOME", os.path.expanduser("~/.headroom/hf"))
|
||||
from headroom import savings_ledger
|
||||
from headroom.cache.compression_store import get_compression_store
|
||||
from headroom.compress import compress
|
||||
|
||||
store = None
|
||||
totals = [0, 0] # tokens before, tokens after (only for rewritten fields)
|
||||
|
||||
def shrink(text):
|
||||
nonlocal store
|
||||
result = compress(
|
||||
[{"role": "tool", "content": text}],
|
||||
protect_recent=0,
|
||||
kompress_model="disabled",
|
||||
)
|
||||
compressed = result.messages[0].get("content")
|
||||
if not isinstance(compressed, str):
|
||||
compressed = json.dumps(compressed)
|
||||
saved = result.tokens_before - result.tokens_after
|
||||
if saved < MIN_SAVED_TOKENS:
|
||||
return None
|
||||
if store is None:
|
||||
store = get_compression_store()
|
||||
hash_key = store.store(
|
||||
original=text,
|
||||
compressed=compressed,
|
||||
original_tokens=result.tokens_before,
|
||||
compressed_tokens=result.tokens_after,
|
||||
compression_strategy="posttooluse_hook",
|
||||
ttl=TTL_SECONDS,
|
||||
)
|
||||
totals[0] += result.tokens_before
|
||||
totals[1] += result.tokens_after
|
||||
return (
|
||||
f"{compressed}\n"
|
||||
f"[headroom: output compressed {result.tokens_before}->{result.tokens_after} tokens; "
|
||||
f"call mcp__headroom__headroom_retrieve with hash={hash_key} if you need the full original]"
|
||||
)
|
||||
|
||||
def shrink_array(items):
|
||||
nonlocal store
|
||||
original_json = json.dumps(items)
|
||||
if len(original_json) < MIN_CHARS:
|
||||
return None
|
||||
kept = items[:ARRAY_KEEP]
|
||||
truncated_json = json.dumps(kept)
|
||||
# No ML/token-counter call needed for a plain truncation decision; a char/4
|
||||
# estimate is the same fallback Headroom's own cost estimator uses and is
|
||||
# only used here to decide eligibility and annotate the marker.
|
||||
tokens_before = max(1, len(original_json) // 4)
|
||||
tokens_after = max(1, len(truncated_json) // 4)
|
||||
if tokens_before - tokens_after < MIN_SAVED_TOKENS:
|
||||
return None
|
||||
if store is None:
|
||||
store = get_compression_store()
|
||||
hash_key = store.store(
|
||||
original=original_json,
|
||||
compressed=truncated_json,
|
||||
original_tokens=tokens_before,
|
||||
compressed_tokens=tokens_after,
|
||||
compression_strategy="posttooluse_hook_array_truncate",
|
||||
ttl=TTL_SECONDS,
|
||||
)
|
||||
totals[0] += tokens_before
|
||||
totals[1] += tokens_after
|
||||
remaining = len(items) - len(kept)
|
||||
marker = (
|
||||
f"[headroom: {remaining} more of {len(items)} entries omitted "
|
||||
f"({tokens_before}->{tokens_after} tokens); call mcp__headroom__headroom_retrieve "
|
||||
f"with hash={hash_key} for the complete list]"
|
||||
)
|
||||
return kept + [marker]
|
||||
|
||||
updated = None
|
||||
if isinstance(response, str):
|
||||
updated = shrink(response)
|
||||
else:
|
||||
rewritten = dict(response)
|
||||
changed = False
|
||||
for key in string_candidates:
|
||||
new_value = shrink(rewritten[key])
|
||||
if new_value is not None:
|
||||
rewritten[key] = new_value
|
||||
changed = True
|
||||
for key in array_candidates:
|
||||
new_value = shrink_array(rewritten[key])
|
||||
if new_value is not None:
|
||||
rewritten[key] = new_value
|
||||
changed = True
|
||||
if changed:
|
||||
updated = rewritten
|
||||
if updated is None:
|
||||
return 0
|
||||
|
||||
savings_ledger.record_savings_event(
|
||||
tokens_before=totals[0],
|
||||
tokens_after=totals[1],
|
||||
client="posttooluse-hook",
|
||||
source="hook",
|
||||
)
|
||||
json.dump(
|
||||
{
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "PostToolUse",
|
||||
"updatedToolOutput": updated,
|
||||
}
|
||||
},
|
||||
sys.stdout,
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if "--self-test" in sys.argv:
|
||||
sys.exit(self_test())
|
||||
try:
|
||||
sys.exit(main())
|
||||
except Exception:
|
||||
sys.exit(0)
|
||||
@@ -1,4 +1,7 @@
|
||||
|
||||
## 2.2.0 (2026-07-16)
|
||||
- Update to latest version from ajslater/codex (changelog : https://github.com/ajslater/codex/releases)
|
||||
|
||||
## 2.1.2 (2026-07-11)
|
||||
- Update to latest version from ajslater/codex (changelog : https://github.com/ajslater/codex/releases)
|
||||
|
||||
|
||||
@@ -101,4 +101,4 @@ schema:
|
||||
slug: codex
|
||||
udev: true
|
||||
url: https://github.com/alexbelgium/hassio-addons
|
||||
version: "2.1.2"
|
||||
version: "2.2.0"
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"github_beta": "true",
|
||||
"last_update": "2026-07-11",
|
||||
"last_update": "2026-07-16",
|
||||
"repository": "alexbelgium/hassio-addons",
|
||||
"slug": "codex",
|
||||
"source": "github",
|
||||
"upstream_repo": "ajslater/codex",
|
||||
"upstream_version": "2.1.2"
|
||||
"upstream_version": "2.2.0"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
|
||||
## 1.3.0 (2026-07-16)
|
||||
- Update to latest version from CollaboraOnline/online (changelog : https://github.com/CollaboraOnline/online/releases)
|
||||
|
||||
## 1.2.2 (2026-07-04)
|
||||
- Update to latest version from CollaboraOnline/online (changelog : https://github.com/CollaboraOnline/online/releases)
|
||||
|
||||
|
||||
@@ -45,5 +45,5 @@ schema:
|
||||
username: str
|
||||
slug: collabora
|
||||
url: https://github.com/alexbelgium/hassio-addons
|
||||
version: "1.2.2"
|
||||
version: "1.3.0"
|
||||
webui: "[PROTO:ssl]://[HOST]:[PORT:9980]/browser/dist/admin/admin.html"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"last_update": "2026-07-04",
|
||||
"last_update": "2026-07-16",
|
||||
"repository": "alexbelgium/hassio-addons",
|
||||
"slug": "collabora",
|
||||
"source": "github",
|
||||
"upstream_repo": "CollaboraOnline/online",
|
||||
"upstream_version": "1.2.2"
|
||||
"upstream_version": "1.3.0"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
## 1.4.0-2 (16-07-2026)
|
||||
- Minor bugs fixed
|
||||
|
||||
## 1.4.0 (2026-06-20)
|
||||
- Update to latest version from gtsteffaniak/filebrowser (changelog : https://github.com/gtsteffaniak/filebrowser/releases)
|
||||
|
||||
@@ -115,4 +115,5 @@ LABEL \
|
||||
# 6 Healthcheck #
|
||||
#################
|
||||
|
||||
# Upstream
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
|
||||
CMD curl -f http://localhost:3001/health || exit 1
|
||||
|
||||
@@ -114,4 +114,4 @@ schema:
|
||||
slug: filebrowser_quantum
|
||||
udev: true
|
||||
url: https://github.com/alexbelgium/hassio-addons
|
||||
version: "1.4.0"
|
||||
version: "1.4.0.2"
|
||||
|
||||
@@ -1,20 +1,60 @@
|
||||
- Added support for configuring extra environment variables via the `env_vars` add-on option alongside config.yaml. See https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2 for details.
|
||||
## 2.0.1 (2026-07-17)
|
||||
|
||||
- Aligned the pull-request build context with the production builder by copying
|
||||
the shared Home Assistant helper scripts before building the add-on.
|
||||
- Fixed aarch64 and amd64 PR validation failing on unresolved Dockerfile
|
||||
`COPY` instructions. Runtime and migration behavior are unchanged.
|
||||
|
||||
## 2.0.0 (2026-07-17)
|
||||
|
||||
- Replaced the abandoned `vogler/free-games-claimer` upstream with
|
||||
`P-Adamiec/Free-Games-Claimer-Remaster`.
|
||||
- Reworked the image build for the remaster's Python, Chromium, TurboVNC, and
|
||||
noVNC runtime on both amd64 and aarch64.
|
||||
- Preserved the previous default one-shot behavior and Epic, Prime Gaming, and
|
||||
GOG store selection.
|
||||
- Kept noVNC on port 6080 for upgrade compatibility.
|
||||
- Added `RUN_ONCE` and `STORES` options while retaining `CMD_ARGUMENTS` as a
|
||||
deprecated compatibility input.
|
||||
- Added an automatic, idempotent migration of legacy Epic, Prime Gaming, and
|
||||
GOG JSON claim history into the remaster SQLite database.
|
||||
- Preserved legacy files for rollback and documented the required one-time
|
||||
Chromium login when a Firefox session cannot be migrated.
|
||||
- Pinned the reviewed upstream source commit and paused generic automatic
|
||||
updates so the add-on's independent `2.x` version cannot regress to `1.x`.
|
||||
- Updated the configuration template, upstream metadata, and documentation.
|
||||
- Added support for configuring extra environment variables via the `env_vars`
|
||||
add-on option.
|
||||
|
||||
## 1.8 (2025-05-17)
|
||||
- Update to latest version from vogler/free-games-claimer (changelog : https://github.com/vogler/free-games-claimer/releases)
|
||||
|
||||
- Update to latest version from vogler/free-games-claimer (changelog: https://github.com/vogler/free-games-claimer/releases)
|
||||
|
||||
## 1.7 (2025-03-08)
|
||||
- Update to latest version from vogler/free-games-claimer (changelog : https://github.com/vogler/free-games-claimer/releases)
|
||||
|
||||
- Update to latest version from vogler/free-games-claimer (changelog: https://github.com/vogler/free-games-claimer/releases)
|
||||
|
||||
## 1.6-6 (2024-12-29)
|
||||
|
||||
- Minor bugs fixed
|
||||
|
||||
## 1.6-5 (2024-12-13)
|
||||
|
||||
- Minor bugs fixed
|
||||
|
||||
## 1.6-4 (2024-12-07)
|
||||
- ⚠ MAJOR CHANGE : switch to the new config logic from homeassistant. Your configuration files will have migrated from /config/hassio_addons/free_games_claimer in a folder only accessible from my Filebrowser addon called /addon_configs/db21ed7f_free_games_claimer. This avoids the addon to mess with your homeassistant configuration folder, and allows to backup the options. For more information, see here : https://developers.home-assistant.io/blog/2023/11/06/public-addon-config/
|
||||
|
||||
- Major change: switch to the new Home Assistant add-on configuration logic.
|
||||
Configuration files were migrated from
|
||||
`/config/hassio_addons/free_games_claimer` to the private add-on configuration
|
||||
directory available through compatible file browser add-ons.
|
||||
|
||||
## 1.6-3 (2024-12-05)
|
||||
|
||||
- Minor bugs fixed
|
||||
|
||||
## 1.6-2 (2024-12-05)
|
||||
|
||||
- Minor bugs fixed
|
||||
|
||||
## 1.6 (2023-12-30)
|
||||
@@ -28,18 +68,23 @@
|
||||
## 1.4 (2023-05-27)
|
||||
|
||||
- Update to latest version from vogler/free-games-claimer
|
||||
|
||||
## 1.4-5 (2023-05-26)
|
||||
|
||||
- Minor bugs fixed
|
||||
|
||||
## 1.4-4 (2023-05-26)
|
||||
|
||||
- Minor bugs fixed
|
||||
|
||||
## 1.4-3 (2023-05-26)
|
||||
|
||||
- Minor bugs fixed
|
||||
|
||||
## 1.4-2 (2023-05-25)
|
||||
|
||||
- Minor bugs fixed
|
||||
|
||||
## NOT_WORKING (2023-05-22)
|
||||
|
||||
- Minor bugs fixed
|
||||
|
||||
@@ -1,91 +1,14 @@
|
||||
#============================#
|
||||
# ALEXBELGIUM'S DOCKERFILE #
|
||||
#============================#
|
||||
# _.------.
|
||||
# _.-` ('>.-`"""-.
|
||||
# '.--'` _'` _ .--.)
|
||||
# -' '-.-';` `
|
||||
# ' - _.' ``'--.
|
||||
# '---` .-'""`
|
||||
# /`
|
||||
#=== Home Assistant Addon ===#
|
||||
#=== Home Assistant Add-on ===#
|
||||
|
||||
#################
|
||||
# 1 Build Image #
|
||||
#################
|
||||
|
||||
ARG BUILD_FROM
|
||||
ARG BUILD_VERSION
|
||||
ARG BUILD_FROM="debian:bookworm-slim"
|
||||
FROM ${BUILD_FROM}
|
||||
|
||||
##################
|
||||
# 2 Modify Image #
|
||||
##################
|
||||
|
||||
# Set S6 wait time
|
||||
ENV S6_CMD_WAIT_FOR_SERVICES=1 \
|
||||
S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0 \
|
||||
S6_SERVICES_GRACETIME=0
|
||||
|
||||
##################
|
||||
# 3 Install apps #
|
||||
##################
|
||||
|
||||
# Add rootfs
|
||||
COPY rootfs/ /
|
||||
RUN find . -type f \( -name "*.sh" -o -name "run" -o -name "finish" \) -print -exec chmod +x {} \;
|
||||
|
||||
# Uses /bin for compatibility purposes
|
||||
# hadolint ignore=DL4005
|
||||
RUN if [ ! -f /bin/sh ] && [ -f /usr/bin/sh ]; then ln -s /usr/bin/sh /bin/sh; fi && \
|
||||
if [ ! -f /bin/bash ] && [ -f /usr/bin/bash ]; then ln -s /usr/bin/bash /bin/bash; fi
|
||||
|
||||
# Modules
|
||||
ARG MODULES="00-banner.sh 01-custom_script.sh"
|
||||
|
||||
# Automatic modules download
|
||||
COPY ha_automodules.sh /ha_automodules.sh
|
||||
RUN chmod 744 /ha_automodules.sh && /ha_automodules.sh "$MODULES" && rm /ha_automodules.sh
|
||||
|
||||
# Manual apps
|
||||
ENV PACKAGES=""
|
||||
|
||||
# Automatic apps & bashio
|
||||
COPY ha_autoapps.sh /ha_autoapps.sh
|
||||
RUN chmod 744 /ha_autoapps.sh && /ha_autoapps.sh "$PACKAGES" && rm /ha_autoapps.sh
|
||||
|
||||
################
|
||||
# 4 Entrypoint #
|
||||
################
|
||||
|
||||
# Add entrypoint
|
||||
ENV S6_STAGE2_HOOK=/ha_entrypoint.sh
|
||||
COPY ha_entrypoint.sh /ha_entrypoint.sh
|
||||
RUN chmod 777 /ha_entrypoint.sh
|
||||
|
||||
# Install bashio
|
||||
COPY bashio-standalone.sh /usr/local/lib/bashio-standalone.sh
|
||||
RUN chmod 0755 /usr/local/lib/bashio-standalone.sh
|
||||
|
||||
RUN \
|
||||
# Change data folder
|
||||
sed -i "s|/fgc|/data|g" /usr/local/bin/docker-entrypoint.sh && \
|
||||
# Run scripts only once
|
||||
sed -i "1a if [ -f /done ]; then exit 0; fi && touch /done" /ha_entrypoint.sh && \
|
||||
sed -i "s=x11vnc=[[ \"\$(ps aux | grep \"x11vnc\" | grep -v grep)\" ]] || x11vnc=g" /usr/local/bin/docker-entrypoint.sh && \
|
||||
# Update playwright
|
||||
npx playwright install-deps && \
|
||||
npx playwright install
|
||||
|
||||
WORKDIR /data
|
||||
ENTRYPOINT [ "/usr/bin/env" ]
|
||||
CMD [ "/ha_entrypoint.sh" ]
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
############
|
||||
# 5 Labels #
|
||||
############
|
||||
|
||||
ARG DEBIAN_FRONTEND="noninteractive"
|
||||
ARG BUILD_ARCH
|
||||
ARG BUILD_DATE
|
||||
ARG BUILD_DESCRIPTION
|
||||
@@ -93,28 +16,143 @@ ARG BUILD_NAME
|
||||
ARG BUILD_REF
|
||||
ARG BUILD_REPOSITORY
|
||||
ARG BUILD_VERSION
|
||||
ENV BUILD_VERSION="${BUILD_VERSION}"
|
||||
ARG UPSTREAM_REPOSITORY="P-Adamiec/Free-Games-Claimer-Remaster"
|
||||
ARG UPSTREAM_REF="cca4992354215cef8807b748575af797606627f4"
|
||||
|
||||
ENV S6_CMD_WAIT_FOR_SERVICES="1" \
|
||||
S6_CMD_WAIT_FOR_SERVICES_MAXTIME="0" \
|
||||
S6_SERVICES_GRACETIME="0" \
|
||||
VNC_PORT="5900" \
|
||||
NOVNC_PORT="6080" \
|
||||
WIDTH="1280" \
|
||||
HEIGHT="720" \
|
||||
DEPTH="24" \
|
||||
SHOW="1" \
|
||||
COMMIT="${UPSTREAM_REF}" \
|
||||
BRANCH="main" \
|
||||
NOW="${BUILD_DATE}"
|
||||
|
||||
# Install the dependencies used by Free Games Claimer Remaster. The upstream
|
||||
# Dockerfile supports both amd64 (Google Chrome) and arm64 (Chromium), so the
|
||||
# add-on keeps its existing multi-architecture support.
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
curl \
|
||||
dos2unix \
|
||||
gnupg \
|
||||
gzip \
|
||||
jq \
|
||||
python3 \
|
||||
python3-pip \
|
||||
tar \
|
||||
tini \
|
||||
&& mkdir -p /etc/apt/keyrings \
|
||||
&& curl --proto "=https" --tlsv1.2 -fsSL https://packagecloud.io/dcommander/virtualgl/gpgkey \
|
||||
| gpg --dearmor -o /etc/apt/trusted.gpg.d/VirtualGL.gpg \
|
||||
&& curl --proto "=https" --tlsv1.2 -fsSL https://packagecloud.io/dcommander/turbovnc/gpgkey \
|
||||
| gpg --dearmor -o /etc/apt/trusted.gpg.d/TurboVNC.gpg \
|
||||
&& curl --proto "=https" --tlsv1.2 -fsSL https://raw.githubusercontent.com/VirtualGL/repo/main/VirtualGL.list \
|
||||
> /etc/apt/sources.list.d/VirtualGL.list \
|
||||
&& curl --proto "=https" --tlsv1.2 -fsSL https://raw.githubusercontent.com/TurboVNC/repo/main/TurboVNC.list \
|
||||
> /etc/apt/sources.list.d/TurboVNC.list \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
novnc \
|
||||
ratpoison \
|
||||
turbovnc \
|
||||
virtualgl \
|
||||
websockify \
|
||||
libasound2 \
|
||||
libatk-bridge2.0-0 \
|
||||
libatk1.0-0 \
|
||||
libatspi2.0-0 \
|
||||
libcairo2 \
|
||||
libcups2 \
|
||||
libgbm1 \
|
||||
libnspr4 \
|
||||
libnss3 \
|
||||
libpango-1.0-0 \
|
||||
libxcomposite1 \
|
||||
libxdamage1 \
|
||||
libxfixes3 \
|
||||
libxkbcommon0 \
|
||||
&& if [ "$(dpkg --print-architecture)" = "amd64" ]; then \
|
||||
curl -fsSL https://dl.google.com/linux/linux_signing_key.pub \
|
||||
| gpg --dearmor -o /etc/apt/keyrings/google-chrome.gpg; \
|
||||
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/google-chrome.gpg] https://dl.google.com/linux/chrome/deb/ stable main" \
|
||||
> /etc/apt/sources.list.d/google-chrome.list; \
|
||||
apt-get update; \
|
||||
apt-get install -y --no-install-recommends google-chrome-stable; \
|
||||
else \
|
||||
apt-get install -y --no-install-recommends chromium; \
|
||||
fi \
|
||||
&& ln -sf /usr/share/novnc/vnc_auto.html /usr/share/novnc/index.html \
|
||||
&& ln -sf /usr/bin/python3 /usr/bin/python \
|
||||
&& apt-get purge -y gnupg \
|
||||
&& apt-get autoremove -y \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/* /var/cache/* /var/tmp/* /tmp/* /usr/share/doc/*
|
||||
|
||||
# Install a deterministic snapshot of the replacement upstream. Updating the
|
||||
# upstream reference is intentionally explicit so image contents cannot change
|
||||
# without an add-on version bump.
|
||||
WORKDIR /fgc
|
||||
RUN curl --proto "=https" --tlsv1.2 -fsSL \
|
||||
"https://github.com/${UPSTREAM_REPOSITORY}/archive/${UPSTREAM_REF}.tar.gz" \
|
||||
-o /tmp/free-games-claimer-remaster.tar.gz \
|
||||
&& tar -xzf /tmp/free-games-claimer-remaster.tar.gz --strip-components=1 -C /fgc \
|
||||
&& python3 -m pip install --no-cache-dir --break-system-packages -r requirements.txt \
|
||||
&& dos2unix ./*.sh \
|
||||
&& chmod +x ./*.sh \
|
||||
&& cp docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh \
|
||||
&& chmod 0755 /usr/local/bin/docker-entrypoint.sh \
|
||||
&& rm -f /tmp/free-games-claimer-remaster.tar.gz \
|
||||
&& rm -rf /fgc/data \
|
||||
&& ln -s /data /fgc/data
|
||||
|
||||
# Add Home Assistant integration files and shared helper modules.
|
||||
COPY rootfs/ /
|
||||
RUN find /etc/cont-init.d /usr/local/bin -type f -name "*.sh" -exec chmod 0755 {} + \
|
||||
&& chmod 0755 /usr/local/bin/migrate_vogler_data.py
|
||||
|
||||
ARG MODULES="00-banner.sh 01-custom_script.sh"
|
||||
COPY ha_automodules.sh /ha_automodules.sh
|
||||
RUN chmod 0755 /ha_automodules.sh \
|
||||
&& /ha_automodules.sh "${MODULES}" \
|
||||
&& rm /ha_automodules.sh
|
||||
|
||||
ENV PACKAGES=""
|
||||
COPY ha_autoapps.sh /ha_autoapps.sh
|
||||
RUN chmod 0755 /ha_autoapps.sh \
|
||||
&& /ha_autoapps.sh "${PACKAGES}" \
|
||||
&& rm /ha_autoapps.sh
|
||||
|
||||
ENV S6_STAGE2_HOOK="/ha_entrypoint.sh"
|
||||
COPY ha_entrypoint.sh /ha_entrypoint.sh
|
||||
COPY bashio-standalone.sh /usr/local/lib/bashio-standalone.sh
|
||||
RUN chmod 0755 /ha_entrypoint.sh /usr/local/lib/bashio-standalone.sh
|
||||
|
||||
EXPOSE 5900 6080
|
||||
ENTRYPOINT ["/usr/bin/env"]
|
||||
CMD ["/ha_entrypoint.sh"]
|
||||
|
||||
LABEL \
|
||||
io.hass.name="${BUILD_NAME}" \
|
||||
io.hass.description="${BUILD_DESCRIPTION}" \
|
||||
io.hass.arch="${BUILD_ARCH}" \
|
||||
io.hass.type="addon" \
|
||||
io.hass.version=${BUILD_VERSION} \
|
||||
io.hass.version="${BUILD_VERSION}" \
|
||||
maintainer="alexbelgium (https://github.com/alexbelgium)" \
|
||||
org.opencontainers.image.title="${BUILD_NAME}" \
|
||||
org.opencontainers.image.description="${BUILD_DESCRIPTION}" \
|
||||
org.opencontainers.image.vendor="Home Assistant Add-ons" \
|
||||
org.opencontainers.image.authors="alexbelgium (https://github.com/alexbelgium)" \
|
||||
org.opencontainers.image.licenses="MIT" \
|
||||
org.opencontainers.image.url="https://github.com/alexbelgium" \
|
||||
org.opencontainers.image.licenses="AGPL-3.0" \
|
||||
org.opencontainers.image.url="https://github.com/${BUILD_REPOSITORY}" \
|
||||
org.opencontainers.image.source="https://github.com/${BUILD_REPOSITORY}" \
|
||||
org.opencontainers.image.documentation="https://github.com/${BUILD_REPOSITORY}/blob/main/README.md" \
|
||||
org.opencontainers.image.created=${BUILD_DATE} \
|
||||
org.opencontainers.image.revision=${BUILD_REF} \
|
||||
org.opencontainers.image.version=${BUILD_VERSION}
|
||||
|
||||
####################
|
||||
# 6 HealthcheckNOT #
|
||||
####################
|
||||
|
||||
# Can't be implemented as container is optimized for memory usage, so the webserver and Node are spun down during idle
|
||||
org.opencontainers.image.documentation="https://github.com/${BUILD_REPOSITORY}/blob/master/free_games_claimer/README.md" \
|
||||
org.opencontainers.image.created="${BUILD_DATE}" \
|
||||
org.opencontainers.image.revision="${BUILD_REF}" \
|
||||
org.opencontainers.image.version="${BUILD_VERSION}" \
|
||||
io.hass.upstream="https://github.com/${UPSTREAM_REPOSITORY}/tree/${UPSTREAM_REF}"
|
||||
|
||||
@@ -1,128 +1,164 @@
|
||||
## ⚠ VNC not working on several machines. Please use config.env to execute the script
|
||||
# Home Assistant add-on: Free Games Claimer
|
||||
|
||||
# Home assistant add-on: Free Games Claimer
|
||||
|
||||
|
||||
I maintain this and other Home Assistant add-ons in my free time: keeping up with upstream changes, HA changes, and testing on real hardware takes a lot of time (and some money). I use around 5-10 of my >110 addons so regularly I install test machines (and purchase some test services such as vpn) that I don't use myself to troubleshoot and improve the addons
|
||||
|
||||
If this add-on saves you time or makes your setup easier, I would be very grateful for your support!
|
||||
I maintain this and other Home Assistant add-ons in my free time. Keeping up
|
||||
with upstream changes, Home Assistant changes, and testing on real hardware
|
||||
takes a significant amount of time.
|
||||
|
||||
[![Buy me a coffee][donation-badge]](https://www.buymeacoffee.com/alexbelgium)
|
||||
[![Donate via PayPal][paypal-badge]](https://www.paypal.com/donate/?hosted_button_id=DZFULJZTP3UQA)
|
||||
|
||||
## Addon informations
|
||||
## Add-on information
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
[](https://www.codacy.com/gh/alexbelgium/hassio-addons/dashboard?utm_source=github.com&utm_medium=referral&utm_content=alexbelgium/hassio-addons&utm_campaign=Badge_Grade)
|
||||
[](https://github.com/alexbelgium/hassio-addons/actions/workflows/weekly-supelinter.yaml)
|
||||
[](https://github.com/alexbelgium/hassio-addons/actions/workflows/onpush_builder.yaml)
|
||||
|
||||
[donation-badge]: https://img.shields.io/badge/Buy%20me%20a%20coffee-%23d32f2f?logo=buy-me-a-coffee&style=flat&logoColor=white
|
||||
[paypal-badge]: https://img.shields.io/badge/Donate%20via%20PayPal-0070BA?logo=paypal&style=flat&logoColor=white
|
||||
|
||||
_Thanks to everyone having starred my repo! To star it click on the image below, then it will be on top right. Thanks!_
|
||||
|
||||
[](https://github.com/alexbelgium/hassio-addons/stargazers)
|
||||
|
||||

|
||||
|
||||
## About
|
||||
|
||||
[Free Games Claimer](https://github.com/vogler/free-games-claimer) : Claims free games periodically on
|
||||
This add-on is based on
|
||||
[Free Games Claimer Remaster](https://github.com/P-Adamiec/Free-Games-Claimer-Remaster).
|
||||
It can claim free games from:
|
||||
|
||||
- Epic Games Store
|
||||
- Amazon Prime Gaming
|
||||
- GOG
|
||||
- Live Games with Gold - planned
|
||||
- Steam
|
||||
- GamerPower-supported stores, when explicitly enabled
|
||||
|
||||
This addon is based on the docker image https://github.com/vogler/free-games-claimer
|
||||
For compatibility with previous add-on releases, the default store selection
|
||||
remains Epic Games, Prime Gaming, and GOG.
|
||||
|
||||
## Configuration
|
||||
## Web interface
|
||||
|
||||
Webui can be found at <http://homeassistant:6080> (NoVNC interface - currently has issues on some machines).
|
||||
The noVNC interface remains available on port `6080`:
|
||||
|
||||
### Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `CMD_ARGUMENTS` | str | `node epic-games ; node prime-gaming ; node gog` | Commands to run for claiming games |
|
||||
| `CONFIG_LOCATION` | str | `/config/config.env` | Location of the configuration file |
|
||||
|
||||
### Example Configuration
|
||||
|
||||
```yaml
|
||||
CMD_ARGUMENTS: "node epic-games ; node prime-gaming ; node gog"
|
||||
CONFIG_LOCATION: "/config/config.env"
|
||||
```text
|
||||
http://homeassistant:6080
|
||||
```
|
||||
|
||||
### Environment Configuration
|
||||
It can be used for initial sign-in, CAPTCHA handling, or other manual browser
|
||||
interaction. Set `VNC_PASSWORD` in `config.env` to protect the VNC session.
|
||||
|
||||
All main configuration is done via the `config.env` file located at `/config/addons_config/free_games_claimer/config.env`.
|
||||
## Add-on options
|
||||
|
||||
If this file doesn't exist, it will be created at first boot with default settings.
|
||||
| Option | Default | Description |
|
||||
|--------|---------|-------------|
|
||||
| `CONFIG_LOCATION` | `/config/config.env` | Persistent environment configuration file |
|
||||
| `RUN_ONCE` | `true` | Run all selected claimers once, then stop the add-on as previous releases did |
|
||||
| `STORES` | empty | Optional comma-separated override, such as `epic,prime,gog,steam` |
|
||||
| `CMD_ARGUMENTS` | `node epic-games ; node prime-gaming ; node gog` | Deprecated compatibility option; recognized legacy command names are converted to `STORES` |
|
||||
| `env_vars` | `[]` | Additional environment variables passed to the add-on |
|
||||
|
||||
### Required Environment Variables
|
||||
### Run modes
|
||||
|
||||
Add these to your `config.env` file:
|
||||
With `RUN_ONCE: true`, the add-on performs one claiming pass and stops. This is
|
||||
the default and preserves the behavior of the former vogler-based add-on.
|
||||
|
||||
With `RUN_ONCE: false`, the remaster remains running and uses its internal
|
||||
scheduler. Set `SCHEDULER_HOURS` in `config.env` to control the interval.
|
||||
|
||||
## Environment configuration
|
||||
|
||||
The add-on keeps its configuration in `CONFIG_LOCATION`, which defaults to
|
||||
`/config/config.env`. From Home Assistant this is stored in the add-on's
|
||||
private `addon_configs` directory and can be edited with a compatible file
|
||||
browser add-on.
|
||||
|
||||
A template is created on first start. Common examples are:
|
||||
|
||||
```env
|
||||
# Epic Games Store
|
||||
# Preserve the former default selection
|
||||
STORES=epic,prime,gog
|
||||
|
||||
# Epic Games
|
||||
EG_EMAIL=your-email@example.com
|
||||
EG_PASSWORD=your-password
|
||||
EG_OTPKEY=
|
||||
|
||||
# Amazon Prime Gaming
|
||||
PG_EMAIL=your-amazon-email@example.com
|
||||
PG_PASSWORD=your-amazon-password
|
||||
PG_PASSWORD=your-password
|
||||
PG_OTPKEY=
|
||||
|
||||
# GOG (optional)
|
||||
# GOG
|
||||
GOG_EMAIL=your-gog-email@example.com
|
||||
GOG_PASSWORD=your-gog-password
|
||||
GOG_PASSWORD=your-password
|
||||
|
||||
# Notifications (optional)
|
||||
EMAIL_SMTP_HOST=smtp.gmail.com
|
||||
EMAIL_SMTP_PORT=587
|
||||
EMAIL_USER=notifications@example.com
|
||||
EMAIL_PASS=your-app-password
|
||||
EMAIL_TO=recipient@example.com
|
||||
# Optional Steam support
|
||||
STEAM_USERNAME=your-steam-username
|
||||
STEAM_PASSWORD=your-password
|
||||
|
||||
# Optional notifications
|
||||
NOTIFY=tgram://bot-token/chat-id
|
||||
# DISCORD_WEBHOOK=https://discord.com/api/webhooks/...
|
||||
```
|
||||
|
||||
### Additional Options
|
||||
Existing variables such as `EG_EMAIL`, `EG_PASSWORD`, `PG_EMAIL`,
|
||||
`PG_PASSWORD`, `PG_OTPKEY`, `GOG_EMAIL`, `GOG_PASSWORD`, `SHOW`, `WIDTH`,
|
||||
`HEIGHT`, `TIMEOUT`, `LOGIN_TIMEOUT`, `DRYRUN`, and `NOTIFY` remain compatible.
|
||||
See the
|
||||
[upstream configuration reference](https://github.com/P-Adamiec/Free-Games-Claimer-Remaster#configuration)
|
||||
for all available settings.
|
||||
|
||||
For complete configuration options and advanced settings, see: https://github.com/vogler/free-games-claimer#configuration--options
|
||||
## Upgrade from version 1.8
|
||||
|
||||
### Important Notes
|
||||
Version 2.0 changes the application engine from
|
||||
`vogler/free-games-claimer` (Node.js, Playwright, and Firefox) to
|
||||
`P-Adamiec/Free-Games-Claimer-Remaster` (Python, nodriver, and Chromium).
|
||||
The add-on performs the following migration automatically on first start:
|
||||
|
||||
- **VNC Issues**: The NoVNC web interface is currently not working reliably on several machines
|
||||
- **Recommended**: Use the `config.env` file for configuration instead of the web interface
|
||||
- **Security**: Store credentials securely and consider using app-specific passwords where available
|
||||
1. The existing `config.env` remains at the same configured location.
|
||||
2. Legacy `epic-games.json`, `prime-gaming.json`, and `gog.json` claim history
|
||||
is imported into the remaster SQLite database at `/data/fgc.db`.
|
||||
3. Existing database rows are detected and are not duplicated if migration is
|
||||
retried.
|
||||
4. A pre-migration database backup is created when an existing `fgc.db` is
|
||||
present.
|
||||
5. All old files remain under `/data/data` for rollback or manual recovery.
|
||||
|
||||
### Custom Scripts and Environment Variables
|
||||
Browser sessions cannot be converted because the old add-on used a shared
|
||||
Firefox profile while the remaster uses separate Chromium profiles per store.
|
||||
Credentials remain available through `config.env`, but accounts that require
|
||||
interactive authentication may need a one-time login through noVNC after the
|
||||
upgrade. The old Firefox profile is retained and is never deleted.
|
||||
|
||||
This addon supports custom scripts and environment variables through the `addon_config` mapping:
|
||||
The external noVNC port remains `6080`, although the standalone remaster image
|
||||
normally uses port `7080`.
|
||||
|
||||
- **Custom scripts**: See [Running Custom Scripts in Addons](https://github.com/alexbelgium/hassio-addons/wiki/Running-custom-scripts-in-Addons)
|
||||
- **env_vars option**: Use the add-on `env_vars` option to pass extra environment variables (uppercase or lowercase names). See https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2 for details.
|
||||
## Upstream update policy
|
||||
|
||||
The image is built from an explicit upstream commit in the Dockerfile. This
|
||||
keeps amd64 and aarch64 images reproducible and prevents an upstream branch or
|
||||
container tag from changing without an add-on review and version bump.
|
||||
|
||||
The repository updater is intentionally paused for this add-on because the
|
||||
add-on uses its own `2.x` version series while the replacement upstream uses a
|
||||
`1.x` version series. An automatic replacement would risk a Home Assistant
|
||||
version regression and would not safely update the pinned commit. A maintainer
|
||||
upstream update must therefore update `UPSTREAM_REF`, `upstream_version`, the
|
||||
add-on version, and `CHANGELOG.md` together.
|
||||
|
||||
## Installation
|
||||
|
||||
The installation of this add-on is pretty straightforward and not different in comparison to installing any other add-on.
|
||||
1. Add this add-on repository to the Home Assistant add-on store.
|
||||
2. Install **Free Games Claimer**.
|
||||
3. Configure the add-on options as needed.
|
||||
4. Start the add-on and review its log.
|
||||
5. Open noVNC if an account needs manual authentication.
|
||||
|
||||
1. Add my add-ons repository to your home assistant instance (in supervisor addons store at top right, or click button below if you have configured my HA)
|
||||
[](https://my.home-assistant.io/redirect/supervisor_add_addon_repository/?repository_url=https%3A%2F%2Fgithub.com%2Falexbelgium%2Fhassio-addons)
|
||||
1. Install this add-on.
|
||||
1. Click the `Save` button to store your configuration.
|
||||
1. Set the add-on options to your preferences
|
||||
1. Start the add-on.
|
||||
1. Check the logs of the add-on to see if everything went well.
|
||||
1. Open the webUI and adapt the software options
|
||||
[](https://my.home-assistant.io/redirect/supervisor_add_addon_repository/?repository_url=https%3A%2F%2Fgithub.com%2Falexbelgium%2Fhassio-addons)
|
||||
|
||||
## Custom scripts and environment variables
|
||||
|
||||
- [Running custom scripts in add-ons](https://github.com/alexbelgium/hassio-addons/wiki/Running-custom-scripts-in-Addons)
|
||||
- [Passing environment variables to an add-on](https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2)
|
||||
|
||||
## Support
|
||||
|
||||
Create an issue on github
|
||||
|
||||
[repository]: https://github.com/alexbelgium/hassio-addons
|
||||
|
||||
|
||||
Open an issue in the
|
||||
[add-on repository](https://github.com/alexbelgium/hassio-addons/issues).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"build_from": {
|
||||
"aarch64": "ghcr.io/vogler/free-games-claimer:latest",
|
||||
"amd64": "ghcr.io/vogler/free-games-claimer:latest"
|
||||
"aarch64": "debian:bookworm-slim",
|
||||
"amd64": "debian:bookworm-slim"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
# Free Games Claimer Remaster
|
||||
arch:
|
||||
- aarch64
|
||||
- amd64
|
||||
description:
|
||||
automatically claims free games on the Epic Games Store, Amazon Prime
|
||||
Gaming and GOG
|
||||
description: "Automatically claims free games from Epic Games Store, Amazon Prime Gaming, GOG, Steam, and optional GamerPower-supported stores"
|
||||
devices:
|
||||
- /dev/dri
|
||||
- /dev/dri/card0
|
||||
@@ -73,25 +72,29 @@ init: false
|
||||
map:
|
||||
- addon_config:rw
|
||||
- homeassistant_config:rw
|
||||
name: Free Games Claimer (NoVNC not working)
|
||||
name: Free Games Claimer
|
||||
options:
|
||||
env_vars: []
|
||||
CMD_ARGUMENTS: node epic-games ; node prime-gaming ; node gog
|
||||
STORES: ""
|
||||
RUN_ONCE: true
|
||||
CONFIG_LOCATION: /config/config.env
|
||||
ports:
|
||||
5900/tcp: null
|
||||
6080/tcp: 6080
|
||||
ports_description:
|
||||
5900/tcp: VNC port
|
||||
6080/tcp: NOVNC port
|
||||
6080/tcp: noVNC web interface
|
||||
schema:
|
||||
env_vars:
|
||||
- name: match(^[A-Za-z0-9_]+$)
|
||||
value: str?
|
||||
CMD_ARGUMENTS: str
|
||||
CMD_ARGUMENTS: str?
|
||||
STORES: str?
|
||||
RUN_ONCE: bool
|
||||
CONFIG_LOCATION: str
|
||||
slug: free_games_claimer
|
||||
udev: true
|
||||
url: https://github.com/alexbelgium/hassio-addons
|
||||
version: "1.8"
|
||||
version: "2.0.1"
|
||||
webui: "[PROTO:ssl]://[HOST]:[PORT:6080]"
|
||||
|
||||
@@ -1,85 +1,152 @@
|
||||
#!/usr/bin/env bashio
|
||||
# shellcheck shell=bash
|
||||
set -e
|
||||
set -eo pipefail
|
||||
|
||||
##############
|
||||
# Initialize #
|
||||
##############
|
||||
CONFIG_FILE="/config/config.env"
|
||||
if bashio::config.has_value 'CONFIG_LOCATION'; then
|
||||
CONFIG_FILE="$(bashio::config 'CONFIG_LOCATION')"
|
||||
fi
|
||||
CONFIG_DIR="$(dirname "${CONFIG_FILE}")"
|
||||
RUNTIME_CONFIG="/data/config.env"
|
||||
|
||||
CONFIG_HOME="$(bashio::config "CONFIG_LOCATION")"
|
||||
CONFIG_HOME="$(dirname "$CONFIG_HOME")"
|
||||
mkdir -p "${CONFIG_DIR}" /data
|
||||
|
||||
# Use new config file
|
||||
if [ ! -f "$CONFIG_HOME/config.env" ]; then
|
||||
# Copy default config.env
|
||||
cp /templates/config.env "$CONFIG_HOME/"
|
||||
chmod 755 "$CONFIG_HOME/config.env"
|
||||
bashio::log.warning "A default config.env file was copied to $CONFIG_HOME. Please customize according to https://github.com/vogler/free-games-claimer/tree/main#configuration--options and restart the add-on"
|
||||
# Recover from an old add-on bug that could create config.env as a directory.
|
||||
if [ -d "${CONFIG_FILE}" ]; then
|
||||
bashio::log.warning "Found a directory at ${CONFIG_FILE}; replacing it with a configuration file"
|
||||
rm -rf "${CONFIG_FILE}"
|
||||
fi
|
||||
|
||||
if [ ! -f "${CONFIG_FILE}" ]; then
|
||||
install -m 0600 /templates/config.env "${CONFIG_FILE}"
|
||||
bashio::log.warning \
|
||||
"Created ${CONFIG_FILE}. Add account credentials there and restart the add-on if automatic login is required."
|
||||
else
|
||||
bashio::log.info "Using existing config.env file in $CONFIG_HOME. Please customize according to https://github.com/vogler/free-games-claimer/tree/main#configuration--options and restart the add-on"
|
||||
bashio::log.info "Using configuration from ${CONFIG_FILE}"
|
||||
fi
|
||||
|
||||
# Remove erroneous folder named config.env (bug fix for looping issue)
|
||||
if [ -d "$CONFIG_HOME/config.env" ]; then
|
||||
bashio::log.warning "Found directory named config.env, deleting it..."
|
||||
rm -rf "$CONFIG_HOME/config.env" # Fix: Ensures directory removal even if it exists
|
||||
cp /templates/config.env "$CONFIG_HOME/config.env" # Recreate as a valid file
|
||||
chmod 755 "$CONFIG_HOME/config.env"
|
||||
fi
|
||||
# The remaster reads /fgc/data/config.env. /fgc/data is linked to Home
|
||||
# Assistant's persistent /data volume by the Dockerfile.
|
||||
install -m 0600 "${CONFIG_FILE}" "${RUNTIME_CONFIG}"
|
||||
sed -i 's/\r$//' "${RUNTIME_CONFIG}"
|
||||
|
||||
# Copy new file
|
||||
mkdir -p /data/data
|
||||
cp "$CONFIG_HOME/config.env" /data/data/
|
||||
|
||||
# Permissions
|
||||
chmod -R 755 "$CONFIG_HOME"
|
||||
|
||||
# Export variables
|
||||
# Export values needed by the VNC entrypoint as well as by the Python app.
|
||||
set -a
|
||||
echo ""
|
||||
bashio::log.info "Sourcing variables from $CONFIG_HOME/config.env"
|
||||
cp "$CONFIG_HOME"/config.env /config.env
|
||||
# Remove previous instance
|
||||
sed -i "s|export ||g" /config.env
|
||||
# Add export for non empty lines
|
||||
sed -i '/\S/s/^/export /' /config.env
|
||||
# Delete lines starting with #
|
||||
sed -i '/export #/d' /config.env
|
||||
# Get variables
|
||||
# shellcheck source=/dev/null
|
||||
source /config.env
|
||||
rm /config.env
|
||||
source "${RUNTIME_CONFIG}"
|
||||
set +a
|
||||
|
||||
##############
|
||||
# Launch App #
|
||||
##############
|
||||
# The Home Assistant port mapping is intentionally kept at 6080 for a seamless
|
||||
# upgrade from the previous add-on, even though the new upstream defaults to 7080.
|
||||
if [ -n "${NOVNC_PORT:-}" ] && [ "${NOVNC_PORT}" != "6080" ]; then
|
||||
bashio::log.warning "NOVNC_PORT=${NOVNC_PORT} is not supported by the add-on port mapping; using 6080"
|
||||
fi
|
||||
export NOVNC_PORT="6080"
|
||||
export VNC_PORT="5900"
|
||||
|
||||
# Go to folder
|
||||
cd /data || true
|
||||
# Absolute paths from the former image pointed to its Firefox profile. The
|
||||
# replacement uses Chromium profiles and must start with a separate directory.
|
||||
if [ "${BROWSER_DIR:-}" = "/data/data/browser" ]; then
|
||||
bashio::log.warning "Remapping legacy Firefox BROWSER_DIR to the remaster Chromium profile directory"
|
||||
export BROWSER_DIR="data/browser"
|
||||
fi
|
||||
if [ "${SCREENSHOTS_DIR:-}" = "/data/data/screenshots" ]; then
|
||||
export SCREENSHOTS_DIR="/fgc/data/screenshots"
|
||||
fi
|
||||
|
||||
# Fetch commands
|
||||
CMD_ARGUMENTS="$(bashio::config "CMD_ARGUMENTS")"
|
||||
IFS=';'
|
||||
read -ar strarr <<< "$CMD_ARGUMENTS"
|
||||
append_store() {
|
||||
local store="${1}"
|
||||
local current="${2}"
|
||||
|
||||
# Sanitizes commands
|
||||
trim() {
|
||||
local var="$*"
|
||||
var="${var#"${var%%[![:space:]]*}"}"
|
||||
var="${var%"${var##*[![:space:]]}"}"
|
||||
printf '%s' "$var"
|
||||
if [[ ",${current}," == *",${store},"* ]]; then
|
||||
printf '%s' "${current}"
|
||||
elif [ -n "${current}" ]; then
|
||||
printf '%s,%s' "${current}" "${store}"
|
||||
else
|
||||
printf '%s' "${store}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Add docker-entrypoint command
|
||||
for val in "${strarr[@]}"; do
|
||||
val="$(trim "$val")"
|
||||
echo " "
|
||||
bashio::log.info "Starting the app with arguments \"$val\""
|
||||
echo " "
|
||||
echo "$val" | xargs docker-entrypoint.sh || true
|
||||
done
|
||||
legacy_commands_to_stores() {
|
||||
local commands="${1}"
|
||||
local selected=""
|
||||
local command=""
|
||||
local normalized=""
|
||||
local command_list=()
|
||||
|
||||
bashio::log.info "All actions concluded. Stopping in 10 seconds."
|
||||
sleep 10
|
||||
bashio::addon.stop
|
||||
IFS=';' read -ra command_list <<< "${commands}"
|
||||
for command in "${command_list[@]}"; do
|
||||
normalized="${command,,}"
|
||||
case "${normalized}" in
|
||||
*epic-games*|*epicgames*|*" epic"*|epic*)
|
||||
selected="$(append_store "epic" "${selected}")"
|
||||
;;
|
||||
*prime-gaming*|*primegaming*|*" prime"*|prime*|*" amazon"*|amazon*)
|
||||
selected="$(append_store "prime" "${selected}")"
|
||||
;;
|
||||
*steam-games*|*" steam"*|steam*)
|
||||
selected="$(append_store "steam" "${selected}")"
|
||||
;;
|
||||
*gamerpower*)
|
||||
selected="$(append_store "gamerpower" "${selected}")"
|
||||
;;
|
||||
*" gog"*|gog*)
|
||||
selected="$(append_store "gog" "${selected}")"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
printf '%s' "${selected}"
|
||||
}
|
||||
|
||||
# A non-empty STORES add-on option takes priority. Otherwise retain a STORES
|
||||
# value from config.env, then fall back to translating the legacy commands.
|
||||
STORES_OPTION=""
|
||||
if bashio::config.has_value 'STORES'; then
|
||||
STORES_OPTION="$(bashio::config 'STORES')"
|
||||
fi
|
||||
if [ -n "${STORES_OPTION}" ]; then
|
||||
export STORES="${STORES_OPTION}"
|
||||
elif [ -z "${STORES:-}" ]; then
|
||||
CMD_ARGUMENTS=""
|
||||
if bashio::config.has_value 'CMD_ARGUMENTS'; then
|
||||
CMD_ARGUMENTS="$(bashio::config 'CMD_ARGUMENTS')"
|
||||
fi
|
||||
STORES="$(legacy_commands_to_stores "${CMD_ARGUMENTS}")"
|
||||
export STORES="${STORES:-epic,prime,gog}"
|
||||
fi
|
||||
|
||||
bashio::log.info "Enabled stores: ${STORES:-epic,prime,gog}"
|
||||
|
||||
# Import claim history from vogler/free-games-claimer once. Legacy files and
|
||||
# browser data are retained under /data/data for rollback and manual recovery.
|
||||
/usr/local/bin/migrate_vogler_data.py
|
||||
|
||||
APP_COMMAND=(python3 /fgc/main.py)
|
||||
RUN_ONCE="true"
|
||||
if bashio::config.has_value 'RUN_ONCE' && ! bashio::config.true 'RUN_ONCE'; then
|
||||
RUN_ONCE="false"
|
||||
fi
|
||||
if [ "${RUN_ONCE}" = "true" ]; then
|
||||
APP_COMMAND+=(--once)
|
||||
bashio::log.info "Starting a single claiming run (legacy-compatible mode)"
|
||||
|
||||
set +e
|
||||
/usr/local/bin/docker-entrypoint.sh "${APP_COMMAND[@]}"
|
||||
exit_code=$?
|
||||
set -e
|
||||
|
||||
if [ "${exit_code}" -ne 0 ]; then
|
||||
bashio::log.error "Free Games Claimer exited with status ${exit_code}"
|
||||
else
|
||||
bashio::log.info "Claiming run completed"
|
||||
fi
|
||||
|
||||
bashio::log.info "Stopping the add-on"
|
||||
sleep 2
|
||||
bashio::addon.stop
|
||||
exit "${exit_code}"
|
||||
fi
|
||||
|
||||
bashio::log.info "Starting the built-in scheduler"
|
||||
exec /usr/local/bin/docker-entrypoint.sh "${APP_COMMAND[@]}"
|
||||
|
||||
@@ -1,7 +1,39 @@
|
||||
HEIGHT=1280
|
||||
LOGIN_TIMEOUT=180
|
||||
NOTIFY_TITLE='Free Games Claimer'
|
||||
# List of environment variables, see : https://github.com/vogler/free-games-claimer#configuration--options
|
||||
# Free Games Claimer Remaster configuration
|
||||
# Complete option reference:
|
||||
# https://github.com/P-Adamiec/Free-Games-Claimer-Remaster#configuration
|
||||
|
||||
# Browser and noVNC
|
||||
SHOW=1
|
||||
TIMEOUT=60
|
||||
WIDTH=1280
|
||||
HEIGHT=720
|
||||
TIMEOUT=60
|
||||
LOGIN_TIMEOUT=180
|
||||
VNC_LOGIN_TIMEOUT=180
|
||||
NOVNC_PORT=6080
|
||||
|
||||
# Keep the previous add-on's default stores. Add steam or gamerpower if wanted.
|
||||
STORES=epic,prime,gog
|
||||
|
||||
# Used only when RUN_ONCE is disabled in the add-on options.
|
||||
SCHEDULER_HOURS=12
|
||||
|
||||
# Common credentials can be used as fallbacks for all stores.
|
||||
# EMAIL=
|
||||
# PASSWORD=
|
||||
|
||||
# Store-specific credentials take priority over EMAIL/PASSWORD.
|
||||
# EG_EMAIL=
|
||||
# EG_PASSWORD=
|
||||
# EG_OTPKEY=
|
||||
# EG_PARENTALPIN=
|
||||
# PG_EMAIL=
|
||||
# PG_PASSWORD=
|
||||
# PG_OTPKEY=
|
||||
# GOG_EMAIL=
|
||||
# GOG_PASSWORD=
|
||||
# STEAM_USERNAME=
|
||||
# STEAM_PASSWORD=
|
||||
|
||||
# Notifications (Apprise or Discord)
|
||||
# NOTIFY=
|
||||
# DISCORD_WEBHOOK=
|
||||
|
||||
248
free_games_claimer/rootfs/usr/local/bin/migrate_vogler_data.py
Normal file
248
free_games_claimer/rootfs/usr/local/bin/migrate_vogler_data.py
Normal file
@@ -0,0 +1,248 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Migrate legacy vogler/free-games-claimer JSON history to the remaster DB."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sqlite3
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterator
|
||||
|
||||
DATA_DIR = Path(os.environ.get("FGC_DATA_DIR", "/data"))
|
||||
LEGACY_DIR = DATA_DIR / "data"
|
||||
DATABASE = DATA_DIR / "fgc.db"
|
||||
MARKER = DATA_DIR / ".vogler-remaster-migrated-v1.json"
|
||||
BACKUP = DATA_DIR / "fgc.db.pre-vogler-migration"
|
||||
|
||||
SOURCES = {
|
||||
"epic": "epic-games.json",
|
||||
"prime": "prime-gaming.json",
|
||||
"gog": "gog.json",
|
||||
}
|
||||
|
||||
|
||||
def log(message: str) -> None:
|
||||
print(f"[legacy migration] {message}", flush=True)
|
||||
|
||||
|
||||
def locate_source(filename: str) -> Path | None:
|
||||
"""Prefer the old nested data directory, with a root fallback."""
|
||||
for candidate in (LEGACY_DIR / filename, DATA_DIR / filename):
|
||||
if candidate.is_file():
|
||||
return candidate
|
||||
return None
|
||||
|
||||
|
||||
def normalize_timestamp(value: Any) -> str:
|
||||
if isinstance(value, str) and value.strip():
|
||||
raw = value.strip().replace("Z", "+00:00")
|
||||
try:
|
||||
parsed = datetime.fromisoformat(raw)
|
||||
if parsed.tzinfo is not None:
|
||||
parsed = parsed.astimezone(timezone.utc).replace(tzinfo=None)
|
||||
return parsed.isoformat(sep=" ", timespec="seconds")
|
||||
except ValueError:
|
||||
pass
|
||||
return datetime.now(timezone.utc).replace(tzinfo=None).isoformat(
|
||||
sep=" ", timespec="seconds"
|
||||
)
|
||||
|
||||
|
||||
def clean(value: Any, *, default: str = "", limit: int | None = None) -> str:
|
||||
result = default if value is None else str(value)
|
||||
if limit is not None:
|
||||
result = result[:limit]
|
||||
return result
|
||||
|
||||
|
||||
def iter_records(store: str, payload: Any, source: Path) -> Iterator[dict[str, Any]]:
|
||||
if not isinstance(payload, dict):
|
||||
raise ValueError(f"{source} does not contain a JSON object")
|
||||
|
||||
for user, games in payload.items():
|
||||
if not isinstance(games, dict):
|
||||
continue
|
||||
|
||||
for legacy_id, value in games.items():
|
||||
record = value if isinstance(value, dict) else {}
|
||||
title = clean(record.get("title"), default=clean(legacy_id), limit=512)
|
||||
game_id = clean(legacy_id, default=title, limit=256)
|
||||
if not game_id:
|
||||
game_id = title[:256] or "unknown"
|
||||
|
||||
legacy_store = record.get("store")
|
||||
extra = {
|
||||
"migration_source": str(source),
|
||||
"legacy_store": legacy_store,
|
||||
"legacy_time": record.get("time") or record.get("timestamp"),
|
||||
}
|
||||
extra = {key: val for key, val in extra.items() if val not in (None, "")}
|
||||
|
||||
yield {
|
||||
"store": store,
|
||||
"user": clean(user, default="unknown", limit=128) or "unknown",
|
||||
"game_id": game_id,
|
||||
"title": title or game_id,
|
||||
"url": clean(record.get("url"), limit=None) or None,
|
||||
"status": clean(record.get("status"), default="unknown", limit=64)
|
||||
or "unknown",
|
||||
"code": clean(record.get("code"), limit=128) or None,
|
||||
"extra": json.dumps(extra, ensure_ascii=False) if extra else None,
|
||||
"created_at": normalize_timestamp(
|
||||
record.get("time") or record.get("timestamp")
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def ensure_schema(connection: sqlite3.Connection) -> None:
|
||||
connection.executescript(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS claimed_games (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
store VARCHAR(32) NOT NULL,
|
||||
user VARCHAR(128) NOT NULL,
|
||||
game_id VARCHAR(256) NOT NULL,
|
||||
title VARCHAR(512) NOT NULL,
|
||||
url TEXT,
|
||||
status VARCHAR(64) NOT NULL DEFAULT 'unknown',
|
||||
code VARCHAR(128),
|
||||
extra TEXT,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS ix_claimed_games_store ON claimed_games (store);
|
||||
CREATE INDEX IF NOT EXISTS ix_claimed_games_user ON claimed_games (user);
|
||||
CREATE INDEX IF NOT EXISTS ix_claimed_games_game_id ON claimed_games (game_id);
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def migrate() -> int:
|
||||
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if MARKER.exists():
|
||||
log("Legacy claim history was already migrated")
|
||||
return 0
|
||||
|
||||
sources = {
|
||||
store: source
|
||||
for store, filename in SOURCES.items()
|
||||
if (source := locate_source(filename)) is not None
|
||||
}
|
||||
legacy_browser = LEGACY_DIR / "browser"
|
||||
|
||||
if not sources:
|
||||
if legacy_browser.exists():
|
||||
log(
|
||||
"Legacy Firefox browser data was found but cannot be converted to "
|
||||
"the remaster Chromium profile; a one-time login may be required"
|
||||
)
|
||||
MARKER.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"migrated_at": datetime.now(timezone.utc).isoformat(),
|
||||
"imported": 0,
|
||||
"sources": [],
|
||||
},
|
||||
indent=2,
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
log("No legacy claim-history files were found")
|
||||
return 0
|
||||
|
||||
if DATABASE.exists() and not BACKUP.exists():
|
||||
shutil.copy2(DATABASE, BACKUP)
|
||||
log(f"Backed up the existing database to {BACKUP}")
|
||||
|
||||
imported = 0
|
||||
skipped = 0
|
||||
errors: list[str] = []
|
||||
|
||||
try:
|
||||
with sqlite3.connect(DATABASE) as connection:
|
||||
ensure_schema(connection)
|
||||
|
||||
for store, source in sources.items():
|
||||
try:
|
||||
payload = json.loads(source.read_text(encoding="utf-8"))
|
||||
source_imported = 0
|
||||
for record in iter_records(store, payload, source):
|
||||
exists = connection.execute(
|
||||
"""
|
||||
SELECT 1 FROM claimed_games
|
||||
WHERE store = ? AND user = ? AND game_id = ?
|
||||
LIMIT 1
|
||||
""",
|
||||
(record["store"], record["user"], record["game_id"]),
|
||||
).fetchone()
|
||||
if exists:
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
connection.execute(
|
||||
"""
|
||||
INSERT INTO claimed_games (
|
||||
store, user, game_id, title, url, status, code,
|
||||
extra, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
record["store"],
|
||||
record["user"],
|
||||
record["game_id"],
|
||||
record["title"],
|
||||
record["url"],
|
||||
record["status"],
|
||||
record["code"],
|
||||
record["extra"],
|
||||
record["created_at"],
|
||||
record["created_at"],
|
||||
),
|
||||
)
|
||||
imported += 1
|
||||
source_imported += 1
|
||||
|
||||
connection.commit()
|
||||
log(f"Imported {source_imported} record(s) from {source}")
|
||||
except (OSError, ValueError, json.JSONDecodeError, sqlite3.Error) as err:
|
||||
connection.rollback()
|
||||
message = f"Failed to import {source}: {err}"
|
||||
errors.append(message)
|
||||
log(message)
|
||||
except sqlite3.Error as err:
|
||||
log(f"Database migration failed: {err}")
|
||||
return 1
|
||||
|
||||
if legacy_browser.exists():
|
||||
log(
|
||||
"Legacy Firefox browser data remains in /data/data/browser. It is "
|
||||
"not compatible with Chromium, so use noVNC for a one-time login if needed."
|
||||
)
|
||||
|
||||
if errors:
|
||||
log("Migration was incomplete and will be retried on the next start")
|
||||
return 0
|
||||
|
||||
MARKER.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"migrated_at": datetime.now(timezone.utc).isoformat(),
|
||||
"imported": imported,
|
||||
"skipped_existing": skipped,
|
||||
"sources": [str(path) for path in sources.values()],
|
||||
},
|
||||
indent=2,
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
log(f"Migration complete: {imported} imported, {skipped} already present")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(migrate())
|
||||
@@ -1,10 +1,11 @@
|
||||
{
|
||||
"dockerhub_by_date": true,
|
||||
"dockerhub_by_date": false,
|
||||
"dockerhub_list_size": 2,
|
||||
"last_update": "17-05-2025",
|
||||
"last_update": "17-07-2026",
|
||||
"paused": true,
|
||||
"repository": "alexbelgium/hassio-addons",
|
||||
"slug": "free_games_claimer",
|
||||
"source": "github",
|
||||
"upstream_repo": "vogler/free-games-claimer",
|
||||
"upstream_version": "1.8"
|
||||
"upstream_repo": "P-Adamiec/Free-Games-Claimer-Remaster",
|
||||
"upstream_version": "1.1"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
## 1.27 (15-07-2026)
|
||||
- Fix the Gitea add-on HEALTHCHECK to work correctly when SSL is enabled, so Home Assistant can accurately report the add-on's health status regardless of whether the instance uses HTTP or HTTPS.
|
||||
|
||||
## 1.26.4 (2026-06-23)
|
||||
- Update to latest version from go-gitea/gitea (changelog : https://github.com/go-gitea/gitea/releases)
|
||||
|
||||
@@ -133,4 +133,4 @@ HEALTHCHECK \
|
||||
--retries=5 \
|
||||
--start-period=30s \
|
||||
--timeout=25s \
|
||||
CMD curl -A "HealthCheck: Docker/1.0" -s -f "http://127.0.0.1:${HEALTH_PORT}${HEALTH_URL}" &>/dev/null || exit 1
|
||||
CMD curl -A "HealthCheck: Docker/1.0" -s -f -k --http1.1 "$(cat /run/health_protocol 2>/dev/null || echo http)://127.0.0.1:${HEALTH_PORT}${HEALTH_URL}" &>/dev/null || exit 1
|
||||
|
||||
@@ -97,5 +97,5 @@ schema:
|
||||
slug: gitea
|
||||
udev: true
|
||||
url: https://github.com/alexbelgium/hassio-addons/tree/master/gitea
|
||||
version: "1.26.4"
|
||||
version: "1.27"
|
||||
webui: "[PROTO:ssl]://[HOST]:[PORT:3000]"
|
||||
|
||||
@@ -55,6 +55,7 @@ for file in /config/app.ini /etc/templates/app.ini; do
|
||||
PROTOCOL=http
|
||||
sed -i "/server/a PROTOCOL=http" "$file"
|
||||
fi
|
||||
echo -n "${PROTOCOL}" > /run/health_protocol
|
||||
|
||||
##################
|
||||
# ADAPT ROOT_URL #
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
## ⚠ Open Issue : [🐛 [qBittorrent] Cant update to 5.2.3 (opened 2026-07-09)](https://github.com/alexbelgium/hassio-addons/issues/2836) by [@tschoehuijs](https://github.com/tschoehuijs)
|
||||
## ⚠ Open Request : [✨ [REQUEST] qbittorrent (opened 2025-10-11)](https://github.com/alexbelgium/hassio-addons/issues/2139) by [@nyok92](https://github.com/nyok92)
|
||||
# Hass.io Add-ons: Tor with bridges
|
||||
|
||||
[![Donate][donation-badge]](https://www.buymeacoffee.com/alexbelgium)
|
||||
|
||||
Reference in New Issue
Block a user