feat: pi-agent wrapper
This commit is contained in:
@@ -36,6 +36,44 @@ in {
|
||||
'';
|
||||
};
|
||||
|
||||
externalSkills = mkOption {
|
||||
type = types.listOf (types.submodule {
|
||||
options = {
|
||||
src = mkOption {
|
||||
type = types.anything;
|
||||
description = "Flake input pointing to a skills repository root.";
|
||||
};
|
||||
skillsDir = mkOption {
|
||||
type = types.str;
|
||||
default = "skills";
|
||||
description = ''
|
||||
Subdirectory inside src that contains skill folders.
|
||||
'';
|
||||
};
|
||||
selectSkills = mkOption {
|
||||
type = types.nullOr (types.listOf types.str);
|
||||
default = null;
|
||||
description = ''
|
||||
List of skill names to cherry-pick from this source.
|
||||
null means include every skill found in skillsDir.
|
||||
'';
|
||||
};
|
||||
};
|
||||
});
|
||||
default = [];
|
||||
description = ''
|
||||
External skill sources passed to mkOpencodeSkills.
|
||||
Each entry maps directly to an element of the externalSkills
|
||||
list accepted by the AGENTS flake's lib.mkOpencodeSkills.
|
||||
'';
|
||||
example = literalExpression ''
|
||||
[
|
||||
{ src = inputs.skills-anthropic; selectSkills = [ "claude-api" ]; }
|
||||
{ src = inputs.skills-vercel; }
|
||||
]
|
||||
'';
|
||||
};
|
||||
|
||||
mcpServers = mkOption {
|
||||
type = types.attrsOf types.anything;
|
||||
default = if mcpCfg != null then mcpCfg.servers else {};
|
||||
@@ -82,6 +120,21 @@ in {
|
||||
source = "${rendered}/.claude/agents";
|
||||
};
|
||||
|
||||
# Skills (merged from personal AGENTS repo + optional external skills)
|
||||
home.file.".claude/skills" = mkIf (cfg.agentsInput != null) {
|
||||
source = cfg.agentsInput.lib.mkOpencodeSkills {
|
||||
inherit pkgs;
|
||||
customSkills = "${cfg.agentsInput}/skills";
|
||||
externalSkills =
|
||||
map (
|
||||
entry:
|
||||
{inherit (entry) src skillsDir;}
|
||||
// optionalAttrs (entry.selectSkills != null) {inherit (entry) selectSkills;}
|
||||
)
|
||||
cfg.externalSkills;
|
||||
};
|
||||
};
|
||||
|
||||
# Rendered settings.json with permissions + MCP servers
|
||||
home.file.".claude/settings.json" = mkIf (settingsJson != null) {
|
||||
source = "${settingsJson}";
|
||||
|
||||
@@ -7,124 +7,32 @@
|
||||
with lib; let
|
||||
cfg = config.coding.agents.pi;
|
||||
mcpCfg = config.programs.mcp or null;
|
||||
|
||||
hasPiPackage = pkgs ? pi;
|
||||
|
||||
defaultPiImageArchive =
|
||||
if hasPiPackage
|
||||
then
|
||||
pkgs.dockerTools.buildLayeredImage {
|
||||
name = "pi-agent";
|
||||
tag = "latest";
|
||||
contents = with pkgs; [
|
||||
bashInteractive
|
||||
bun
|
||||
cacert
|
||||
coreutils
|
||||
findutils
|
||||
git
|
||||
gnugrep
|
||||
gnused
|
||||
nix
|
||||
nodejs
|
||||
pi
|
||||
];
|
||||
config = {
|
||||
Env = [
|
||||
"PATH=/bin:/usr/bin"
|
||||
"NIX_REMOTE=daemon"
|
||||
"SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"
|
||||
];
|
||||
WorkingDir = "/tmp";
|
||||
Cmd = ["${pkgs.coreutils}/bin/sleep" "infinity"];
|
||||
};
|
||||
}
|
||||
else null;
|
||||
in {
|
||||
options.coding.agents.pi = {
|
||||
enable = mkEnableOption "Pi agent management via canonical agent.toml definitions";
|
||||
|
||||
container = mkOption {
|
||||
description = "Run Pi through a rootless Podman container while keeping a native host UX.";
|
||||
default = {};
|
||||
type = types.submodule {
|
||||
options = {
|
||||
enable = mkEnableOption "Containerized Pi wrapper";
|
||||
path = mkOption {
|
||||
type = types.str;
|
||||
default = ".pi/agent";
|
||||
description = ''
|
||||
Relative path (inside the Home Manager user's home) where Pi agent
|
||||
config should be materialized.
|
||||
|
||||
name = mkOption {
|
||||
type = types.str;
|
||||
default = "pi-agent";
|
||||
description = "Container name used by the Pi wrapper.";
|
||||
};
|
||||
|
||||
image = mkOption {
|
||||
type = types.str;
|
||||
default = if hasPiPackage then "pi-agent:latest" else "docker.io/nixos/nix:latest";
|
||||
description = ''
|
||||
Podman image to run for Pi.
|
||||
Defaults to a local declarative Pi-ready image when `pkgs.pi` exists,
|
||||
otherwise falls back to docker.io/nixos/nix:latest.
|
||||
'';
|
||||
};
|
||||
|
||||
imageArchive = mkOption {
|
||||
type = types.nullOr types.path;
|
||||
default = defaultPiImageArchive;
|
||||
description = ''
|
||||
Optional OCI/Docker archive path to load into Podman when `image`
|
||||
is missing locally. By default, a Pi-ready local image archive is
|
||||
generated when `pkgs.pi` is available.
|
||||
'';
|
||||
};
|
||||
|
||||
projectRoots = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [];
|
||||
description = ''
|
||||
Allowlisted absolute host roots that may be mounted into the container.
|
||||
Wrapper exits with a clear error when cwd is outside these roots.
|
||||
'';
|
||||
example = ["/home/m3tam3re/p"];
|
||||
};
|
||||
|
||||
autoStart = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = "Automatically start container when wrapper is invoked and it is not running.";
|
||||
};
|
||||
|
||||
autoNixDevelop = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
If true and cwd contains flake.nix, run Pi as:
|
||||
nix develop -c pi ...
|
||||
inside the container.
|
||||
'';
|
||||
};
|
||||
|
||||
extraRunArgs = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [];
|
||||
description = "Additional Podman create args appended after safe defaults.";
|
||||
};
|
||||
|
||||
extraEnv = mkOption {
|
||||
type = types.attrsOf types.str;
|
||||
default = {};
|
||||
description = "Extra environment variables passed to the container.";
|
||||
};
|
||||
};
|
||||
};
|
||||
Defaults to `.pi/agent`, i.e. `~/.pi/agent`.
|
||||
'';
|
||||
example = ".config/pi/agent";
|
||||
};
|
||||
|
||||
mcpServers = mkOption {
|
||||
type = types.attrsOf types.anything;
|
||||
default = if mcpCfg != null then mcpCfg.servers else {};
|
||||
default =
|
||||
if mcpCfg != null
|
||||
then mcpCfg.servers
|
||||
else {};
|
||||
defaultText = literalExpression "config.programs.mcp.servers";
|
||||
description = ''
|
||||
MCP server configurations for Pi (pi-mcp-adapter).
|
||||
Written to ~/.pi/agent/mcp.json.
|
||||
Written to `${cfg.path}/mcp.json`.
|
||||
Automatically inherits from config.programs.mcp.servers.
|
||||
'';
|
||||
};
|
||||
@@ -149,6 +57,44 @@ in {
|
||||
'';
|
||||
};
|
||||
|
||||
externalSkills = mkOption {
|
||||
type = types.listOf (types.submodule {
|
||||
options = {
|
||||
src = mkOption {
|
||||
type = types.anything;
|
||||
description = "Flake input pointing to a skills repository root.";
|
||||
};
|
||||
skillsDir = mkOption {
|
||||
type = types.str;
|
||||
default = "skills";
|
||||
description = ''
|
||||
Subdirectory inside src that contains skill folders.
|
||||
'';
|
||||
};
|
||||
selectSkills = mkOption {
|
||||
type = types.nullOr (types.listOf types.str);
|
||||
default = null;
|
||||
description = ''
|
||||
List of skill names to cherry-pick from this source.
|
||||
null means include every skill found in skillsDir.
|
||||
'';
|
||||
};
|
||||
};
|
||||
});
|
||||
default = [];
|
||||
description = ''
|
||||
External skill sources passed to mkOpencodeSkills.
|
||||
Each entry maps directly to an element of the externalSkills
|
||||
list accepted by the AGENTS flake's lib.mkOpencodeSkills.
|
||||
'';
|
||||
example = literalExpression ''
|
||||
[
|
||||
{ src = inputs.skills-anthropic; selectSkills = [ "claude-api" ]; }
|
||||
{ src = inputs.skills-vercel; }
|
||||
]
|
||||
'';
|
||||
};
|
||||
|
||||
primaryAgent = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
@@ -167,7 +113,7 @@ in {
|
||||
default = [];
|
||||
description = ''
|
||||
Pi packages to install (npm:, git:, or local paths).
|
||||
These are written to ~/.pi/agent/settings.json.
|
||||
These are written to `${cfg.path}/settings.json`.
|
||||
'';
|
||||
};
|
||||
|
||||
@@ -255,7 +201,7 @@ in {
|
||||
};
|
||||
default = {};
|
||||
description = ''
|
||||
Pi settings written to ~/.pi/agent/settings.json.
|
||||
Pi settings written to `${cfg.path}/settings.json`.
|
||||
Only non-null values are included in the generated JSON.
|
||||
See pi docs/settings.md for all options.
|
||||
'';
|
||||
@@ -263,6 +209,8 @@ in {
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable (let
|
||||
basePath = lib.removeSuffix "/" cfg.path;
|
||||
|
||||
# Build settings.json by filtering out null values recursively
|
||||
filterNulls = attrs:
|
||||
lib.filterAttrs (_: v: v != null) (
|
||||
@@ -271,182 +219,15 @@ in {
|
||||
then let
|
||||
filtered = filterNulls v;
|
||||
in
|
||||
if filtered == {} then null else filtered
|
||||
else v) attrs
|
||||
if filtered == {}
|
||||
then null
|
||||
else filtered
|
||||
else v)
|
||||
attrs
|
||||
);
|
||||
|
||||
piSettings = filterNulls cfg.settings;
|
||||
|
||||
projectRoots = map toString cfg.container.projectRoots;
|
||||
projectRootsShell = concatStringsSep " " (map escapeShellArg projectRoots);
|
||||
extraRunArgsShell = concatStringsSep " " (map escapeShellArg cfg.container.extraRunArgs);
|
||||
extraEnvPairs = map (k: "${k}=${cfg.container.extraEnv.${k}}") (builtins.attrNames cfg.container.extraEnv);
|
||||
extraEnvShell = concatStringsSep " " (map escapeShellArg extraEnvPairs);
|
||||
hostPiDir = "${config.home.homeDirectory}/.pi";
|
||||
hostPiDirShell = escapeShellArg hostPiDir;
|
||||
imageArchiveShell =
|
||||
if cfg.container.imageArchive != null
|
||||
then escapeShellArg (toString cfg.container.imageArchive)
|
||||
else "";
|
||||
|
||||
piWrapper = pkgs.writeShellScriptBin "pi" ''
|
||||
set -euo pipefail
|
||||
|
||||
PODMAN="${pkgs.podman}/bin/podman"
|
||||
REALPATH="${pkgs.coreutils}/bin/realpath"
|
||||
|
||||
CONTAINER_NAME=${escapeShellArg cfg.container.name}
|
||||
IMAGE=${escapeShellArg cfg.container.image}
|
||||
IMAGE_ARCHIVE=${imageArchiveShell}
|
||||
AUTO_START=${if cfg.container.autoStart then "1" else "0"}
|
||||
AUTO_NIX_DEVELOP=${if cfg.container.autoNixDevelop then "1" else "0"}
|
||||
HOST_PI_DIR=${hostPiDirShell}
|
||||
|
||||
PROJECT_ROOTS=(${projectRootsShell})
|
||||
EXTRA_RUN_ARGS=(${extraRunArgsShell})
|
||||
EXTRA_ENV_VARS=(${extraEnvShell})
|
||||
|
||||
err() {
|
||||
printf "pi-wrapper: %s\n" "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [ "''${#PROJECT_ROOTS[@]}" -eq 0 ]; then
|
||||
err "No allowed projectRoots configured. Set coding.agents.pi.container.projectRoots."
|
||||
fi
|
||||
|
||||
if ! command -v "$PODMAN" >/dev/null 2>&1; then
|
||||
err "podman binary not found at $PODMAN"
|
||||
fi
|
||||
|
||||
CWD="$($REALPATH -m "$PWD")"
|
||||
|
||||
cwd_allowed=0
|
||||
NORMALIZED_ROOTS=()
|
||||
for root in "''${PROJECT_ROOTS[@]}"; do
|
||||
norm_root="$($REALPATH -m "$root")"
|
||||
NORMALIZED_ROOTS+=("$norm_root")
|
||||
case "$CWD/" in
|
||||
"$norm_root/"*)
|
||||
cwd_allowed=1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "$cwd_allowed" -ne 1 ]; then
|
||||
{
|
||||
printf "pi-wrapper: cwd '%s' is outside allowed projectRoots.\n" "$CWD"
|
||||
printf "Allowed roots:\n"
|
||||
for root in "''${NORMALIZED_ROOTS[@]}"; do
|
||||
printf " - %s\n" "$root"
|
||||
done
|
||||
} >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
tty_args=()
|
||||
if [ -t 0 ] && [ -t 1 ]; then
|
||||
tty_args=(-it)
|
||||
fi
|
||||
|
||||
ensure_image_available() {
|
||||
if [ -n "$IMAGE_ARCHIVE" ] && [ -f "$IMAGE_ARCHIVE" ]; then
|
||||
"$PODMAN" load -i "$IMAGE_ARCHIVE" >/dev/null
|
||||
fi
|
||||
|
||||
if ! "$PODMAN" image exists "$IMAGE"; then
|
||||
err "Container image '$IMAGE' is not available and no valid imageArchive was provided."
|
||||
fi
|
||||
}
|
||||
|
||||
create_container() {
|
||||
mount_args=()
|
||||
|
||||
for root in "''${NORMALIZED_ROOTS[@]}"; do
|
||||
mount_args+=("-v" "$root:$root:rw")
|
||||
done
|
||||
|
||||
if [ ! -S /nix/var/nix/daemon-socket/socket ]; then
|
||||
err "Host Nix daemon socket not found at /nix/var/nix/daemon-socket/socket"
|
||||
fi
|
||||
|
||||
mount_args+=("-v" "/nix/var/nix/daemon-socket/socket:/nix/var/nix/daemon-socket/socket:rw")
|
||||
|
||||
mkdir -p "$HOST_PI_DIR"
|
||||
mount_args+=("-v" "$HOST_PI_DIR:/tmp/.pi:rw")
|
||||
|
||||
if [ -d /nix/store ]; then
|
||||
mount_args+=("-v" "/nix/store:/nix/store:ro")
|
||||
fi
|
||||
|
||||
if [ -e /etc/nix/nix.conf ]; then
|
||||
mount_args+=("-v" "/etc/nix/nix.conf:/etc/nix/nix.conf:ro")
|
||||
fi
|
||||
|
||||
if [ -d /etc/ssl/certs ]; then
|
||||
mount_args+=("-v" "/etc/ssl/certs:/etc/ssl/certs:ro")
|
||||
fi
|
||||
|
||||
if [ -d /etc/pki ]; then
|
||||
mount_args+=("-v" "/etc/pki:/etc/pki:ro")
|
||||
fi
|
||||
|
||||
env_args=()
|
||||
for kv in "''${EXTRA_ENV_VARS[@]}"; do
|
||||
env_args+=("--env" "$kv")
|
||||
done
|
||||
|
||||
"$PODMAN" create \
|
||||
--name "$CONTAINER_NAME" \
|
||||
--hostname "$CONTAINER_NAME" \
|
||||
--userns keep-id \
|
||||
--user "$(${pkgs.coreutils}/bin/id -u):$(${pkgs.coreutils}/bin/id -g)" \
|
||||
--security-opt no-new-privileges \
|
||||
--workdir /tmp \
|
||||
--tmpfs /tmp:rw,nodev,nosuid \
|
||||
--env HOME=/tmp \
|
||||
--env NIX_REMOTE=daemon \
|
||||
--env NPM_CONFIG_PREFIX=/tmp/.npm-global \
|
||||
--env npm_config_prefix=/tmp/.npm-global \
|
||||
--env NPM_CONFIG_CACHE=/tmp/.npm \
|
||||
--env npm_config_cache=/tmp/.npm \
|
||||
--env PATH=/tmp/.npm-global/bin:/bin:/usr/bin \
|
||||
"''${mount_args[@]}" \
|
||||
"''${env_args[@]}" \
|
||||
"''${EXTRA_RUN_ARGS[@]}" \
|
||||
"$IMAGE" \
|
||||
sleep infinity >/dev/null
|
||||
}
|
||||
|
||||
ensure_container_running() {
|
||||
if ! "$PODMAN" container exists "$CONTAINER_NAME"; then
|
||||
ensure_image_available
|
||||
create_container
|
||||
fi
|
||||
|
||||
running="$($PODMAN inspect -f '{{.State.Running}}' "$CONTAINER_NAME" 2>/dev/null || true)"
|
||||
if [ "$running" != "true" ]; then
|
||||
if [ "$AUTO_START" = "1" ]; then
|
||||
"$PODMAN" start "$CONTAINER_NAME" >/dev/null
|
||||
else
|
||||
err "Container '$CONTAINER_NAME' is not running and autoStart=false. Start it manually with: podman start $CONTAINER_NAME"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_container_running
|
||||
|
||||
if [ "$AUTO_NIX_DEVELOP" = "1" ] && [ -f "$CWD/flake.nix" ]; then
|
||||
exec "$PODMAN" exec "''${tty_args[@]}" --workdir "$CWD" "$CONTAINER_NAME" nix develop -c pi "$@"
|
||||
fi
|
||||
|
||||
if "$PODMAN" exec --workdir "$CWD" "$CONTAINER_NAME" sh -lc 'command -v pi >/dev/null 2>&1'; then
|
||||
exec "$PODMAN" exec "''${tty_args[@]}" --workdir "$CWD" "$CONTAINER_NAME" pi "$@"
|
||||
fi
|
||||
|
||||
err "Container '$CONTAINER_NAME' does not have 'pi' in PATH (image: $IMAGE). Use a Pi-ready image or run from a flake project with autoNixDevelop=true."
|
||||
'';
|
||||
|
||||
# Rendered agents (only computed when agentsInput is set)
|
||||
rendered =
|
||||
if cfg.agentsInput != null
|
||||
@@ -462,87 +243,56 @@ in {
|
||||
# Dynamic home.file entries for agent .md files
|
||||
agentFiles =
|
||||
if cfg.agentsInput != null
|
||||
then
|
||||
let
|
||||
agentNames = builtins.attrNames cfg.agentsInput.lib.loadAgents;
|
||||
in
|
||||
builtins.listToAttrs (
|
||||
map (name: {
|
||||
name = ".pi/agent/agents/${name}.md";
|
||||
value = {text = builtins.readFile "${rendered}/agents/${name}.md";};
|
||||
})
|
||||
agentNames
|
||||
)
|
||||
then let
|
||||
agentNames = builtins.attrNames cfg.agentsInput.lib.loadAgents;
|
||||
in
|
||||
builtins.listToAttrs (
|
||||
map (name: {
|
||||
name = "${basePath}/agents/${name}.md";
|
||||
value = {source = "${rendered}/agents/${name}.md";};
|
||||
})
|
||||
agentNames
|
||||
)
|
||||
else {};
|
||||
|
||||
skillsSource =
|
||||
if cfg.agentsInput != null
|
||||
then
|
||||
cfg.agentsInput.lib.mkOpencodeSkills {
|
||||
inherit pkgs;
|
||||
customSkills = "${cfg.agentsInput}/skills";
|
||||
}
|
||||
else null;
|
||||
in {
|
||||
assertions =
|
||||
[
|
||||
{
|
||||
assertion = cfg.container.enable || hasPiPackage;
|
||||
message = "coding.agents.pi.enable requires pkgs.pi when container mode is disabled.";
|
||||
}
|
||||
]
|
||||
++ optional cfg.container.enable {
|
||||
assertion = cfg.container.projectRoots != [];
|
||||
message = "coding.agents.pi.container.projectRoots must contain at least one absolute path when container mode is enabled.";
|
||||
}
|
||||
++ optional cfg.container.enable {
|
||||
assertion = all (path: hasPrefix "/" (toString path)) cfg.container.projectRoots;
|
||||
message = "coding.agents.pi.container.projectRoots entries must be absolute paths.";
|
||||
};
|
||||
|
||||
home.packages =
|
||||
(optional cfg.container.enable piWrapper)
|
||||
++ (optional (!cfg.container.enable && hasPiPackage) pkgs.pi);
|
||||
|
||||
home.file = mkMerge [
|
||||
# ── MCP servers from programs.mcp → ~/.pi/agent/mcp.json ───────
|
||||
# ── MCP servers from programs.mcp → ${cfg.path}/mcp.json ───────
|
||||
(mkIf (cfg.mcpServers != {}) {
|
||||
".pi/agent/mcp.json".text = builtins.toJSON {mcpServers = cfg.mcpServers;};
|
||||
"${basePath}/mcp.json".text = builtins.toJSON {mcpServers = cfg.mcpServers;};
|
||||
})
|
||||
|
||||
# ── ~/.pi/agent/settings.json ──────────────────────────────────
|
||||
# ── ${cfg.path}/settings.json ──────────────────────────────────
|
||||
{
|
||||
".pi/agent/settings.json".text = builtins.toJSON piSettings;
|
||||
"${basePath}/settings.json".text = builtins.toJSON piSettings;
|
||||
}
|
||||
|
||||
# ── AGENTS.md — agent descriptions and specialist listing ──────
|
||||
(mkIf (cfg.agentsInput != null) {
|
||||
".pi/agent/AGENTS.md".text = builtins.readFile "${rendered}/AGENTS.md";
|
||||
"${basePath}/AGENTS.md".source = "${rendered}/AGENTS.md";
|
||||
})
|
||||
|
||||
# ── SYSTEM.md — primary agent's system prompt ──────────────────
|
||||
(mkIf (cfg.agentsInput != null) {
|
||||
".pi/agent/SYSTEM.md".text = builtins.readFile "${rendered}/SYSTEM.md";
|
||||
"${basePath}/SYSTEM.md".source = "${rendered}/SYSTEM.md";
|
||||
})
|
||||
|
||||
# ── Agents — pi-subagents .md files ────────────────────────────
|
||||
agentFiles
|
||||
|
||||
# ── Skills symlinked from AGENTS repo (non-container mode) ─────
|
||||
(mkIf (cfg.agentsInput != null && !cfg.container.enable) {
|
||||
".pi/agent/skills".source = skillsSource;
|
||||
# ── Skills symlinked from AGENTS repo ──────────────────────────
|
||||
(mkIf (cfg.agentsInput != null) {
|
||||
"${basePath}/skills".source = cfg.agentsInput.lib.mkOpencodeSkills {
|
||||
inherit pkgs;
|
||||
customSkills = "${cfg.agentsInput}/skills";
|
||||
externalSkills =
|
||||
map (
|
||||
entry:
|
||||
{inherit (entry) src skillsDir;}
|
||||
// optionalAttrs (entry.selectSkills != null) {inherit (entry) selectSkills;}
|
||||
)
|
||||
cfg.externalSkills;
|
||||
};
|
||||
})
|
||||
];
|
||||
|
||||
home.activation.piMaterializeSkills = mkIf (cfg.container.enable && cfg.agentsInput != null) (
|
||||
lib.hm.dag.entryAfter ["writeBoundary"] ''
|
||||
skillsSrc=${escapeShellArg "${skillsSource}"}
|
||||
skillsDst=${escapeShellArg "${config.home.homeDirectory}/.pi/agent/skills"}
|
||||
|
||||
${pkgs.coreutils}/bin/rm -rf "$skillsDst"
|
||||
${pkgs.coreutils}/bin/mkdir -p "$skillsDst"
|
||||
${pkgs.coreutils}/bin/cp -aL "$skillsSrc"/. "$skillsDst"/
|
||||
''
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user