claude-code: migrate memory to context & skills{,Dir} to skills

This commit is contained in:
Vinicius Deolindo 2026-04-08 00:38:02 -03:00 committed by Austin Horstman
parent ffa740c0df
commit 13df3b4e44
8 changed files with 152 additions and 83 deletions

View file

@ -7,6 +7,7 @@
let
inherit (lib)
literalExpression
mkChangedOptionModule
mkOption
nameValuePair
optionalAttrs
@ -55,6 +56,24 @@ in
{
meta.maintainers = [ lib.maintainers.khaneliman ];
imports = [
(mkChangedOptionModule
[ "programs" "claude-code" "memory" "text" ]
[ "programs" "claude-code" "context" ]
(config: lib.getAttrFromPath [ "programs" "claude-code" "memory" "text" ] config)
)
(mkChangedOptionModule
[ "programs" "claude-code" "memory" "source" ]
[ "programs" "claude-code" "context" ]
(config: lib.getAttrFromPath [ "programs" "claude-code" "memory" "source" ] config)
)
(mkChangedOptionModule
[ "programs" "claude-code" "skillsDir" ]
[ "programs" "claude-code" "skills" ]
(config: lib.getAttrFromPath [ "programs" "claude-code" "skillsDir" ] config)
)
];
options.programs.claude-code = {
enable = lib.mkEnableOption "Claude Code, Anthropic's official CLI";
@ -137,6 +156,22 @@ in
description = "JSON configuration for Claude Code settings.json";
};
context = mkOption {
type = lib.types.either lib.types.lines lib.types.path;
default = "";
description = ''
Global context for Claude Code.
The value is either:
- Inline content as a string
- A path to a file containing the content
The configured content is written to
{file}`~/.claude/CLAUDE.md`.
'';
example = literalExpression "./claude-memory.md";
};
plugins = lib.mkOption {
type = with lib.types; listOf (either package path);
default = [ ];
@ -266,36 +301,6 @@ in
};
};
memory = {
text = mkOption {
type = lib.types.nullOr lib.types.lines;
default = null;
description = ''
Inline memory content for CLAUDE.md.
This option is mutually exclusive with memory.source.
'';
example = ''
# Project Memory
## Current Task
Implementing enhanced claude-code module for home-manager.
## Key Files
- claude-code.nix: Main module implementation
'';
};
source = mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = ''
Path to a file containing memory content for CLAUDE.md.
This option is mutually exclusive with memory.text.
'';
example = literalExpression "./claude-memory.md";
};
};
rules = mkContentOption {
description = ''
Modular rule files for Claude Code.
@ -378,13 +383,34 @@ in
'';
};
skills = mkContentOption {
skills = mkOption {
type = lib.types.either (lib.types.attrsOf (
lib.types.oneOf [
lib.types.lines
lib.types.path
lib.types.str
]
)) lib.types.path;
default = { };
description = ''
Custom skills for Claude Code.
The attribute name becomes the skill directory name, and the value is either:
This option can be either:
- An attribute set defining skills
- A path to a directory containing skill folders
If an attribute set is used, the attribute name becomes the
skill directory name, and the value is either:
- Inline content as a string (creates .claude/skills/<name>/SKILL.md)
- A path to a file (creates .claude/skills/<name>/SKILL.md)
- A path to a directory (creates .claude/skills/<name>/ with all files)
This also accepts Nix store paths, for example a skill directory
from a package.
If a path is used, it is expected to contain one folder per
skill name, each containing a {file}`SKILL.md`. The directory is
symlinked to {file}`~/.claude/skills/`.
'';
example = literalExpression ''
{
@ -409,19 +435,13 @@ in
text = pdf.pages[0].extract_text()
```
''';
# A skill can also be a subdirectory within a package source (store path)
beads = "''${pkgs.beads.src}/claude-plugin/skills/beads";
}
'';
};
skillsDir = mkDirOption {
description = ''
Path to a directory containing skill directories for Claude Code.
Each skill directory should contain a SKILL.md entrypoint file.
Skill directories from this path will be symlinked to .claude/skills/.
'';
example = literalExpression "./skills";
};
lspServers = mkOption {
type = lib.types.attrsOf jsonFormat.type;
default = { };
@ -494,6 +514,9 @@ in
let
mkSourceEntry = content: if lib.isPath content then { source = content; } else { text = content; };
isStorePathString = content: builtins.isString content && lib.hasPrefix builtins.storeDir content;
isPathLikeContent = content: lib.isPath content || isStorePathString content;
mkMarkdownEntries =
subdir: attrs:
lib.mapAttrs' (
@ -515,13 +538,15 @@ in
mkSkillEntry =
name: content:
if lib.isPath content && lib.pathIsDirectory content then
if isPathLikeContent content && lib.pathIsDirectory content then
nameValuePair ".claude/skills/${name}" {
source = content;
recursive = true;
}
else
nameValuePair ".claude/skills/${name}/SKILL.md" (mkSourceEntry content);
nameValuePair ".claude/skills/${name}/SKILL.md" (
if isPathLikeContent content then { source = content; } else { text = content; }
);
mkMarketplaceEntry = name: content: {
source = {
@ -547,7 +572,6 @@ in
"agents"
"commands"
"hooks"
"skills"
];
mkExclusiveAssertion = inline: {
@ -563,8 +587,8 @@ in
message = "`programs.claude-code.package` cannot be null when `mcpServers`, `lspServers`, `enableMcpIntegration`, or `plugins` is configured";
}
{
assertion = !(cfg.memory.text != null && cfg.memory.source != null);
message = "Cannot specify both `programs.claude-code.memory.text` and `programs.claude-code.memory.source`";
assertion = !lib.isPath cfg.skills || lib.pathIsDirectory cfg.skills;
message = "`programs.claude-code.skills` must be a directory when set to a path";
}
]
++ map mkExclusiveAssertion exclusiveInlineDirNames;
@ -633,12 +657,16 @@ in
}
);
})
(lib.mkIf (cfg.memory.text != null) {
".claude/CLAUDE.md".text = cfg.memory.text;
})
(lib.mkIf (cfg.memory.source != null) {
".claude/CLAUDE.md".source = cfg.memory.source;
})
(
if lib.isPath cfg.context then
{
".claude/CLAUDE.md".source = cfg.context;
}
else
(lib.mkIf (cfg.context != "") {
".claude/CLAUDE.md".text = cfg.context;
})
)
(lib.mkIf (cfg.marketplaces != { }) {
".claude/plugins/known_marketplaces.json".source =
jsonFormat.generate "claude-code-known-marketplaces.json" (
@ -652,9 +680,14 @@ in
(mkRecursiveDirAttrs "commands" cfg.commandsDir)
(mkRecursiveDirAttrs "hooks" cfg.hooksDir)
(mkRecursiveDirAttrs "rules" cfg.rulesDir)
(mkRecursiveDirAttrs "skills" cfg.skillsDir)
(lib.mkIf (lib.isPath cfg.skills) {
".claude/skills" = {
source = cfg.skills;
recursive = true;
};
})
(mkTextEntries "hooks" cfg.hooks)
(lib.mapAttrs' mkSkillEntry cfg.skills)
(lib.optionalAttrs (builtins.isAttrs cfg.skills) (lib.mapAttrs' mkSkillEntry cfg.skills))
(mkMarkdownEntries "output-styles" cfg.outputStyles)
];
};

View file

@ -15,12 +15,6 @@
};
};
# assert fail: cannot set text and source at the same time.
memory = {
text = "Some text content";
source = ./expected-memory.md;
};
# assert fail: cannot set agents and agentsDir at the same time.
agents = {
test-agent = "test content";
@ -39,19 +33,12 @@
};
hooksDir = ./hooks;
# assert fail: cannot set skills and skillsDir at the same time.
skills = {
test-skill = "test content";
};
skillsDir = ./skills;
};
test.asserts.assertions.expected = [
"`programs.claude-code.package` cannot be null when `mcpServers`, `lspServers`, `enableMcpIntegration`, or `plugins` is configured"
"Cannot specify both `programs.claude-code.memory.text` and `programs.claude-code.memory.source`"
"Cannot specify both `programs.claude-code.agents` and `programs.claude-code.agentsDir`"
"Cannot specify both `programs.claude-code.commands` and `programs.claude-code.commandsDir`"
"Cannot specify both `programs.claude-code.hooks` and `programs.claude-code.hooksDir`"
"Cannot specify both `programs.claude-code.skills` and `programs.claude-code.skillsDir`"
];
}

View file

@ -6,8 +6,8 @@
claude-code-mcp-lsp = ./mcp-lsp.nix;
claude-code-mcp-integration = ./mcp-integration.nix;
claude-code-assertion = ./assertion.nix;
claude-code-memory-management = ./memory-management.nix;
claude-code-memory-from-source = ./memory-from-source.nix;
claude-code-context-inline = ./memory-management.nix;
claude-code-context-path = ./memory-from-source.nix;
claude-code-rules-dir = ./rules-dir.nix;
claude-code-rules-path = ./rules-path.nix;
claude-code-agents-dir = ./agents-dir.nix;
@ -18,6 +18,8 @@
claude-code-agents-path = ./agents-path.nix;
claude-code-commands-path = ./commands-path.nix;
claude-code-skills-path = ./skills-path.nix;
claude-code-legacy-memory-text = ./legacy-memory-text.nix;
claude-code-legacy-memory-source-and-skills-dir = ./legacy-memory-source-and-skills-dir.nix;
claude-code-mixed-content = ./mixed-content.nix;
claude-code-output-styles = ./output-styles.nix;
claude-code-output-styles-not-set = ./output-styles-not-set.nix;

View file

@ -0,0 +1,25 @@
{ lib, options, ... }:
{
programs.claude-code = {
enable = true;
memory.source = ./expected-memory.md;
skillsDir = ./skills;
};
test.asserts.warnings.expected = [
"The option `programs.claude-code.skillsDir' defined in ${lib.showFiles options.programs.claude-code.skillsDir.files} has been changed to `programs.claude-code.skills' that has a different type. Please read `programs.claude-code.skills' documentation and update your configuration accordingly."
"The option `programs.claude-code.memory.source' defined in ${lib.showFiles options.programs.claude-code.memory.source.files} has been changed to `programs.claude-code.context' that has a different type. Please read `programs.claude-code.context' documentation and update your configuration accordingly."
];
nmt.script = ''
assertFileExists home-files/.claude/CLAUDE.md
assertFileContent home-files/.claude/CLAUDE.md ${./expected-memory.md}
assertFileExists home-files/.claude/skills/test-skill/SKILL.md
assertLinkExists home-files/.claude/skills/test-skill/SKILL.md
assertFileContent \
home-files/.claude/skills/test-skill/SKILL.md \
${./skills/test-skill/SKILL.md}
'';
}

View file

@ -0,0 +1,26 @@
{ lib, options, ... }:
{
programs.claude-code = {
enable = true;
memory.text = ''
# Project Memory
This configuration still uses the legacy memory.text option.
'';
};
test.asserts.warnings.expected = [
"The option `programs.claude-code.memory.text' defined in ${lib.showFiles options.programs.claude-code.memory.text.files} has been changed to `programs.claude-code.context' that has a different type. Please read `programs.claude-code.context' documentation and update your configuration accordingly."
];
nmt.script = ''
assertFileExists home-files/.claude/CLAUDE.md
assertFileContent home-files/.claude/CLAUDE.md \
${builtins.toFile "expected-legacy-memory-text.md" ''
# Project Memory
This configuration still uses the legacy memory.text option.
''}
'';
}

View file

@ -1,9 +1,7 @@
{
programs.claude-code = {
enable = true;
memory = {
source = ./expected-memory.md;
};
context = ./expected-memory.md;
};
nmt.script = ''

View file

@ -1,18 +1,16 @@
{
programs.claude-code = {
enable = true;
memory = {
text = ''
# Project Memory
context = ''
# Project Memory
## Current Task
Test implementation of memory management.
## Current Task
Test implementation of memory management.
## Key Context
- This is a test configuration
- Memory should be created at ~/.claude/CLAUDE.md
'';
};
## Key Context
- This is a test configuration
- Memory should be created at ~/.claude/CLAUDE.md
'';
};
nmt.script = ''

View file

@ -1,7 +1,7 @@
{
programs.claude-code = {
enable = true;
skillsDir = ./skills;
skills = ./skills;
};
nmt.script = ''