feat: add aichat kimi and spotify mcp services

Add Home Manager module for `aichat` CLI, integrating Moonshot/Kimi API.
Uses sops-nix for API key management.

Add `spotify-mcp-server` as a system service, integrated with Hermes.
Manages Spotify credentials via sops-nix.

Enable `nextcloud` OIDC app.
Configure `nextcloud-mcp` to restart on sops secret changes.

Enable `zitadel` service.
This commit is contained in:
2026-09-21 21:42:43 -04:00
parent 2ba5a019d7
commit da78aea121
5 changed files with 187 additions and 3 deletions
+72
View File
@@ -0,0 +1,72 @@
# kimi-aichat.nix
# Module Home Manager — importez-le dans votre home.nix
{
config,
lib,
pkgs,
...
}:
with lib; let
cfg = config.programs.aichat-moonshot;
in {
options.programs.aichat-moonshot = {
enable = mkEnableOption "aichat CLI configuré pour Moonshot/Kimi via sops-nix";
sopsSecretKey = mkOption {
type = types.str;
default = "kimi/api_key";
description = ''
Nom de la clé dans votre fichier de secrets sops.
Exemple: <literal>kimi/api_key</literal> ou <literal>aichat/moonshot_api_key</literal>.
La valeur déchiffrée doit être la clé brute (ex: <literal>sk-xxx</literal>).
'';
};
model = mkOption {
type = types.str;
default = "moonshot:kimi-latest";
example = "moonshot:kimi-k1";
description = "Modèle par défaut utilisé par aichat.";
};
extraConfig = mkOption {
type = types.attrs;
default = {};
example = literalExpression ''
{
save = true;
keybindings = "vi";
highlight = true;
}
'';
description = ''
Attributs Nix supplémentaires mergés dans <filename>~/.config/aichat/config.yaml</filename>.
Ne mettez JAMAIS de secrets ici.
'';
};
};
config = mkIf cfg.enable {
# 1. Déclare le secret pour sops-nix
# (suppose que vous avez déjà importé le module sops-nix dans votre config HM)
sops.secrets.${cfg.sopsSecretKey} = {};
# 2. Fichier de config publique (sans clé API)
home.file.".config/aichat/config.yaml".text = lib.generators.toYAML {} ({
model = cfg.model;
}
// cfg.extraConfig);
# 3. Wrapper "kimi" qui lit le secret à l'exécution
home.packages = [
(pkgs.writeShellApplication {
name = "kimi";
runtimeInputs = [pkgs.aichat];
text = ''
export AICHAT_MOONSHOT_API_KEY=$(cat "${config.sops.secrets.${cfg.sopsSecretKey}.path}")
exec aichat "$@"
'';
})
];
};
}
+2 -1
View File
@@ -26,8 +26,9 @@
./hermes
./discord-bot
./eternal-terminal
# ./nextcloud-mcp
# ./gotify
# ./zitadel
./zitadel
# ./syncserver
# ./newt # attendre maj flakes
];
+4 -2
View File
@@ -1,4 +1,4 @@
{...}: {
{config, ...}: {
virtualisation.oci-containers.containers.nextcloud-mcp-claude = {
image = "ghcr.io/cbcoutinho/nextcloud-mcp-server:latest";
cmd = ["--transport" "streamable-http" "--oauth" "--port" "8004" "--enable-app" "calendar"];
@@ -13,5 +13,7 @@
environmentFiles = [config.sops.secrets."nextcloud_mcp_oauth_env".path];
volumes = ["nextcloud-mcp-claude:/app/data"];
};
sops.secrets."nextcloud_mcp_oauth_env" = {};
sops.secrets."nextcloud_mcp_oauth_env".restartUnits = [
"${config.virtualisation.oci-containers.backend}-nextcloud-mcp-claude.service"
];
}
+8
View File
@@ -41,6 +41,9 @@
{
addr = "0.0.0.0";
port = 8083;
# locations."= /.well-known/openid-configuration".extraConfig = ''
# rewrite ^ /index.php$request_uri last;
# '';
}
];
};
@@ -99,6 +102,11 @@
memories
notes
;
oidc = pkgs.fetchNextcloudApp {
url = "https://github.com/H2CK/oidc/releases/download/2.3.1/oidc-2.3.1.tar.gz";
hash = "sha256-zfdZSUwV8VZ5qq7rwN/G4cSfTbuutaK+yJ6g8556yOQ=";
license = "agpl3Plus";
};
};
extraAppsEnable = true;
};
+101
View File
@@ -0,0 +1,101 @@
# services/spotify-mcp/default.nix
# Serveur MCP Spotify (marcelmarais/spotify-mcp-server) lancé en stdio par Hermes sur tower.
#
# flake.nix, dans inputs (remplace l'ancien input varunneal) :
# spotify-mcp = {
# url = "github:marcelmarais/spotify-mcp-server/108fd6b26db9bd9627cd4e4a128841a0f8933756";
# flake = false;
# };
#
# secrets sops (fichier par défaut) :
# spotify_mcp_env: |
# SPOTIFY_CLIENT_ID=...
# SPOTIFY_CLIENT_SECRET=...
#
# App Spotify Developer : redirect URI = http://127.0.0.1:8888/callback
{
config,
pkgs,
lib,
inputs,
...
}: let
stateDir = "/var/lib/hermes/spotify-mcp";
configFile = "${stateDir}/spotify-config.json";
redirectUri = "http://127.0.0.1:8888/callback";
# Le projet exige node >= 26.8.1 (package.json, engines).
nodejs = pkgs.nodejs_26;
spotify-mcp = pkgs.buildNpmPackage {
pname = "spotify-mcp-server";
version = "1.0.0-unstable-2026-09-21";
src = inputs.spotify-mcp;
inherit nodejs;
# 1er build : laisser fakeHash, copier le hash donné par l'erreur, rebuild.
npmDepsHash = lib.fakeHash;
# Le chemin de config est codé en dur à côté du build, donc dans le store
# en lecture seule. On le rend pilotable par variable d'environnement,
# sinon le refresh du token échoue au bout d'une heure.
postPatch = ''
substituteInPlace src/utils.ts \
--replace-fail \
"path.join(__dirname, '../spotify-config.json')" \
"(process.env.SPOTIFY_MCP_CONFIG ?? path.join(__dirname, '../spotify-config.json'))"
'';
meta.mainProgram = "spotify-mcp";
};
authScript = "${spotify-mcp}/lib/node_modules/spotify-mcp-server/build/auth.js";
# Le fichier de config contient aussi les tokens, réécrits par le serveur.
# À chaque lancement on y réinjecte client id / secret depuis sops,
# en conservant accessToken, refreshToken et expiresAt.
prepareConfig = ''
set -euo pipefail
umask 077
set -a
. ${config.sops.secrets.spotify_mcp_env.path}
set +a
[ -f ${configFile} ] || echo '{}' > ${configFile}
${lib.getExe pkgs.jq} \
--arg id "$SPOTIFY_CLIENT_ID" \
--arg secret "$SPOTIFY_CLIENT_SECRET" \
--arg uri ${redirectUri} \
'. + {clientId: $id, clientSecret: $secret, redirectUri: $uri}' \
${configFile} > ${configFile}.tmp
mv ${configFile}.tmp ${configFile}
export SPOTIFY_MCP_CONFIG=${configFile}
'';
wrapper = pkgs.writeShellScript "spotify-mcp-hermes" ''
${prepareConfig}
exec ${lib.getExe spotify-mcp}
'';
# Login OAuth unique, à lancer à la main en tant que hermes.
login = pkgs.writeShellScriptBin "spotify-mcp-login" ''
${prepareConfig}
exec ${lib.getExe nodejs} ${authScript}
'';
in {
sops.secrets.spotify_mcp_env = {
owner = "hermes";
mode = "0400";
};
systemd.tmpfiles.rules = [
"d ${stateDir} 0700 hermes hermes -"
];
environment.systemPackages = [login];
services.hermes-agent.mcpServers.spotify = {
command = "${wrapper}";
# Optionnel, pour démarrer sans les actions destructives :
# tools.exclude = [ "removeUsersSavedTracks" "unfollowPlaylist" "removeTracksFromPlaylist" ];
};
}