feat(rhema-code): add the colors setting with four palettes

The stored `colors` key names a palette — rhema (the default,
Jadeite), aquamarine, black-and-white, or dracula — and the
Appearance page of /settings cycles it live, like the theme and
icon toggles beside it. Each palette carries a dark and a light
appearance under the existing `theme` key; dracula's light side is
derived, the official palette ships only dark.

The appearance decision moves off token comparison: Theme now
answers is_dark() from the ground/text luminance, so palettes
beyond the original two get the right per-profile fallback tones.
This commit is contained in:
vhaudiquet committed 2026-10-17 04:28:31 +08:00
commit 81da26b955
159 files changed
+50628

No files matched your search

+11
View File
@@ -0,0 +1,11 @@
# Patterns to ignore when building the package.
.DS_Store
.git/
.gitignore
.nojekyll
*.swp
*.tmp
*.orig
*.rej
*.tgz
vault
+7
View File
@@ -0,0 +1,7 @@
apiVersion: v2
name: rhema-claw
description: "The resident forge-watching agent daemon: forge watches and a
chat HTTP surface over one SQLite store."
type: application
version: 0.1.0
appVersion: "0.1.0"
@@ -0,0 +1,31 @@
rhema-claw {{ .Chart.AppVersion }} deployed as {{ include "rhema-claw.fullname" . }}.
The daemon is single-node by design: one SQLite store, one dispatcher
(specs/rhema-framework-v1.md §7) — do not scale the Deployment.
1. Talk to it (port-forward; the surface speaks OpenAI chat-completions too):
kubectl port-forward svc/{{ include "rhema-claw.fullname" . }} {{ .Values.service.port }}:{{ .Values.service.port }}
curl http://127.0.0.1:{{ .Values.service.port }}/healthz
2. Config: values `config` renders into ConfigMap {{ include "rhema-claw.fullname" . }}
(claw.yaml, mounted at /config/claw.yaml). db and workspace_root are pinned
under /data; edit them through values, not the ConfigMap.
{{- if not (or .Values.secret.existingSecret .Values.secret.stringData) }}
3. No credentials Secret is configured — set values secret.stringData
(e.g. ANTHROPIC_API_KEY, GITHUB_TOKEN) or secret.existingSecret, or the
daemon refuses to start when a watch or dispatch needs the key.
{{- else }}
3. Credentials: injected from the Secret into the environment.
{{- end }}
4. Token: /v1 routes are protected by the token set in values; /healthz
stays open either way. The render refuses an empty token — the daemon
binds all interfaces in-cluster and will not serve unguarded.
{{- if not .Values.persistence.enabled }}
WARNING: persistence.enabled=false — the SQLite store and workspaces live in
an emptyDir and are lost when the pod restarts.
{{- end }}
@@ -0,0 +1,49 @@
{{/* Chart-wide names and labels. */}}
{{- define "rhema-claw.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- define "rhema-claw.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{- define "rhema-claw.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | quote }}
{{- end }}
{{- define "rhema-claw.labels" -}}
helm.sh/chart: {{ include "rhema-claw.chart" . }}
{{ include "rhema-claw.selectorLabels" . }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{- define "rhema-claw.selectorLabels" -}}
app.kubernetes.io/name: {{ include "rhema-claw.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{- define "rhema-claw.imageTag" -}}
{{- default .Chart.AppVersion .Values.image.tag }}
{{- end }}
{{/*
The single-node invariant, enforced at render time: one daemon owns every
session in one SQLite store, with one dispatcher — scaling past one replica
would corrupt the store, not share the load (specs/rhema-framework-v1.md §7).
*/}}
{{- define "rhema-claw.assertSingleReplica" -}}
{{- if ne (int .Values.replicaCount) 1 }}
{{- fail (printf "replicaCount must be 1, got %v: rhema-claw is single-node by design (one SQLite store, one dispatcher; specs/rhema-framework-v1.md §7)" .Values.replicaCount) }}
{{- end }}
{{- end }}
@@ -0,0 +1,20 @@
{{- $cfg := deepCopy (.Values.config | default dict) }}
{{- /* The state volume is fixed at /data and the Service needs the process
bound to all interfaces, so these three win over whatever values say. */ -}}
{{- $_ := set $cfg "db" "/data/claw.db" }}
{{- $_ := set $cfg "workspace_root" "/data/workspaces" }}
{{- $_ := set $cfg "listen" (printf "0.0.0.0:%v" .Values.service.port) }}
{{- if .Values.token }}
{{- $_ := set $cfg "token" .Values.token }}
{{- else }}
{{- $_ := unset $cfg "token" }}
{{- end }}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "rhema-claw.fullname" . }}
labels:
{{- include "rhema-claw.labels" . | nindent 4 }}
data:
claw.yaml: |
{{ toYaml $cfg | indent 4 }}
@@ -0,0 +1,99 @@
{{- include "rhema-claw.assertSingleReplica" . }}
{{- /* The daemon binds 0.0.0.0 in-cluster (the Service needs every
interface) and refuses to serve a non-loopback surface without a
bearer token — fail the render here, before the pod does. */ -}}
{{- if not .Values.token }}
{{- fail "token is required: the daemon binds all interfaces in-cluster, and it refuses to serve a non-loopback surface without a bearer token (set `token` in values)" }}
{{- end }}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "rhema-claw.fullname" . }}
labels:
{{- include "rhema-claw.labels" . | nindent 4 }}
spec:
replicas: 1
selector:
matchLabels:
{{- include "rhema-claw.selectorLabels" . | nindent 6 }}
app.kubernetes.io/component: server
template:
metadata:
labels:
{{- include "rhema-claw.selectorLabels" . | nindent 8 }}
app.kubernetes.io/component: server
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
securityContext:
# The image runs as uid 1000; fsGroup makes the PVC writable by it.
fsGroup: 1000
containers:
- name: claw
image: "{{ .Values.image.repository }}:{{ include "rhema-claw.imageTag" . }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
args:
- --config
- /config/claw.yaml
ports:
- name: http
containerPort: {{ .Values.service.port }}
protocol: TCP
livenessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 2
periodSeconds: 5
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
{{- if or .Values.secret.existingSecret .Values.secret.stringData }}
envFrom:
- secretRef:
name: {{ .Values.secret.existingSecret | default (include "rhema-claw.fullname" .) }}
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
volumeMounts:
- name: config
mountPath: /config
readOnly: true
- name: data
mountPath: /data
volumes:
- name: config
configMap:
name: {{ include "rhema-claw.fullname" . }}
- name: data
{{- if .Values.persistence.enabled }}
persistentVolumeClaim:
claimName: {{ include "rhema-claw.fullname" . }}
{{- else }}
emptyDir: {}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
@@ -0,0 +1,68 @@
{{- if .Values.ingress.enabled }}
{{- if not .Values.ingress.host }}
{{- fail "ingress.host is required when ingress.enabled: the claw is single-user and bearer-gated, and an unnamed host has no TLS story" }}
{{- end }}
{{- if and .Values.ingress.tls.enabled (not .Values.ingress.tls.secretName) }}
{{- fail "ingress.tls.secretName is required when ingress.tls.enabled: bearer tokens cross this ingress in the clear without TLS" }}
{{- end }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "rhema-claw.fullname" . }}
labels:
{{- include "rhema-claw.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- with .Values.ingress.className }}
ingressClassName: {{ . }}
{{- end }}
{{- if .Values.ingress.tls.enabled }}
tls:
- hosts:
- {{ .Values.ingress.host | quote }}
secretName: {{ .Values.ingress.tls.secretName }}
{{- end }}
rules:
- host: {{ .Values.ingress.host | quote }}
http:
paths:
{{- if .Values.ui.enabled }}
# One address, two services: the browser gets the UI on /,
# the daemon answers /v1 and /healthz (longest prefix wins),
# and the phone app points at the same host. One origin, so
# no CORS anywhere; the bearer token is the auth.
- path: /v1
pathType: Prefix
backend:
service:
name: {{ include "rhema-claw.fullname" . }}
port:
name: http
- path: /healthz
pathType: Prefix
backend:
service:
name: {{ include "rhema-claw.fullname" . }}
port:
name: http
- path: /
pathType: Prefix
backend:
service:
name: {{ include "rhema-claw.fullname" . }}-ui
port:
name: http
{{- else }}
# No UI in the release: the daemon owns the whole host.
- path: /
pathType: Prefix
backend:
service:
name: {{ include "rhema-claw.fullname" . }}
port:
name: http
{{- end }}
{{- end }}
+17
View File
@@ -0,0 +1,17 @@
{{- if .Values.persistence.enabled }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "rhema-claw.fullname" . }}
labels:
{{- include "rhema-claw.labels" . | nindent 4 }}
spec:
accessModes:
- ReadWriteOnce
{{- with .Values.persistence.storageClass }}
storageClassName: {{ . | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.persistence.size }}
{{- end }}
@@ -0,0 +1,10 @@
{{- if and (not .Values.secret.existingSecret) .Values.secret.stringData }}
apiVersion: v1
kind: Secret
metadata:
name: {{ include "rhema-claw.fullname" . }}
labels:
{{- include "rhema-claw.labels" . | nindent 4 }}
stringData:
{{- toYaml .Values.secret.stringData | nindent 2 }}
{{- end }}
@@ -0,0 +1,18 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "rhema-claw.fullname" . }}
labels:
{{- include "rhema-claw.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- name: http
port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
selector:
{{- include "rhema-claw.selectorLabels" . | nindent 4 }}
# The UI pods share the name/instance labels; without this the
# daemon's service load-balances chat traffic to nginx.
app.kubernetes.io/component: server
@@ -0,0 +1,45 @@
{{- if .Values.ui.enabled }}
{{- if not .Values.ui.image.repository }}
{{- fail "ui.image.repository is required when ui.enabled: the UI image is built from deploy/docker/ui.Dockerfile" }}
{{- end }}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "rhema-claw.fullname" . }}-ui
labels:
{{- include "rhema-claw.labels" . | nindent 4 }}
app.kubernetes.io/component: ui
spec:
# Stateless static files: scaling the UI is safe. The single-node
# invariant belongs to the daemon, not to nginx.
replicas: {{ .Values.ui.replicas | default 1 }}
selector:
matchLabels:
{{- include "rhema-claw.selectorLabels" . | nindent 6 }}
app.kubernetes.io/component: ui
template:
metadata:
labels:
{{- include "rhema-claw.labels" . | nindent 8 }}
app.kubernetes.io/component: ui
spec:
containers:
- name: ui
image: "{{ .Values.ui.image.repository }}:{{ default .Chart.AppVersion .Values.ui.image.tag }}"
imagePullPolicy: {{ .Values.ui.image.pullPolicy }}
ports:
- name: http
containerPort: 80
# The shell answering is the whole contract: the bundle is
# static, the API calls happen browser-side against /v1.
livenessProbe:
httpGet:
path: /
port: http
readinessProbe:
httpGet:
path: /
port: http
resources:
{{- toYaml .Values.ui.resources | nindent 12 }}
{{- end }}
@@ -0,0 +1,17 @@
{{- if .Values.ui.enabled }}
apiVersion: v1
kind: Service
metadata:
name: {{ include "rhema-claw.fullname" . }}-ui
labels:
{{- include "rhema-claw.labels" . | nindent 4 }}
app.kubernetes.io/component: ui
spec:
selector:
{{- include "rhema-claw.selectorLabels" . | nindent 4 }}
app.kubernetes.io/component: ui
ports:
- name: http
port: 80
targetPort: http
{{- end }}
+93
View File
@@ -0,0 +1,93 @@
# rhema-claw is single-node by design: one daemon owns every session in one
# SQLite store, with one dispatcher (specs/rhema-framework-v1.md §7). The
# templates fail the render when this is anything but 1 — it is an
# invariant, not a starting point.
replicaCount: 1
image:
repository: rhema-claw
# Defaults to Chart appVersion when empty.
tag: ""
pullPolicy: IfNotPresent
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
# Bearer token for the /v1 routes, rendered into claw.yaml. Required:
# the daemon binds all interfaces in-cluster (the Service needs it) and
# refuses to serve a non-loopback surface without a token, so the render
# fails when this is empty. /healthz stays open either way.
token: ""
# The claw.yaml content. db, workspace_root, and listen are pinned by the
# templates (the PVC is fixed at /data and the container must bind all
# interfaces for the Service); whatever these say here is overridden.
config:
poll_interval_secs: 60
# Seed watches: imported once, while the watch table is still empty.
# Example:
# watches:
# - forge: github
# repo: acme/widgets
watches: []
service:
type: ClusterIP
port: 7799
ingress:
# Off by default: the claw is single-user and bearer-gated, and bearer
# tokens must not cross the public internet in the clear. When enabled,
# a host is required (the render refuses without one), TLS is strongly
# recommended, and the whole surface — chat, /v1, /healthz — is one
# origin. A UI on the same host's / path composes: put the UI's
# ingress first, or route /v1 here from its own host.
enabled: false
# Required when enabled; the render refuses an ingress without one.
host: ""
# The ingress controller's class (nginx, traefik, ...); empty means
# the cluster default.
className: ""
annotations: {}
tls:
enabled: false
# The cert-manager (or manually provisioned) secret carrying the
# certificate; required when tls.enabled.
secretName: ""
ui:
# The web UI on the same host as the daemon: the ingress routes /
# here and /v1 (+ /healthz) to the daemon, so one address serves the
# browser and the phone app alike, with no CORS anywhere. Stateless
# static files — replicas may scale freely.
enabled: true
image:
# Built by deploy/docker/ui.Dockerfile; published by the ui job in
# the publish workflow.
repository: git.vhaudiquet.fr/vhaudiquet/rhema-claw-ui
# Defaults to the chart's appVersion when empty.
tag: ""
pullPolicy: IfNotPresent
resources: {}
persistence:
enabled: true
storageClass: ""
size: 5Gi
# Provider and forge credentials reach the daemon as env vars (the
# provider keys and GITHUB_TOKEN/GITEA_TOKEN/GITLAB_TOKEN fallbacks).
# One mechanism: either name an existingSecret, or list key/value pairs
# in stringData and the chart creates the Secret.
secret:
existingSecret: ""
stringData: {}
# stringData:
# ANTHROPIC_API_KEY: sk-ant-...
# GITHUB_TOKEN: ghp_...
resources: {}
nodeSelector: {}
tolerations: []
affinity: {}