Update README.MD and add nano-claude-code v3.0 + original-source-code/src

- README.MD: add original-source-code and nano-claude-code sections, update
  overview table (4 subprojects), add v3.0 news entry, expand comparison table
  with memory/multi-agent/skills dimensions
- nano-claude-code v3.0: multi-agent package (multi_agent/), memory package
  (memory/), skill package (skill/) with built-in /commit and /review skills,
  context compression (compaction.py), tool registry plugin system, diff view,
  17 slash commands, 18 built-in tools, 101 tests (~5000 lines total)
- original-source-code/src: add raw TypeScript source tree (1884 files)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
chauncygu
2026-04-03 10:26:29 -07:00
parent 3de4c595ea
commit 1d4ffa964d
1942 changed files with 521644 additions and 112 deletions

View File

@@ -0,0 +1,14 @@
"""skill package — reusable prompt templates (skills)."""
from .loader import ( # noqa: F401
SkillDef,
load_skills,
find_skill,
substitute_arguments,
register_builtin_skill,
_parse_skill_file,
_parse_list_field,
)
from .executor import execute_skill # noqa: F401
# Importing builtin registers the built-in skills
from . import builtin as _builtin # noqa: F401

View File

@@ -0,0 +1,100 @@
"""Built-in skills that ship with nano-claude-code."""
from __future__ import annotations
from .loader import SkillDef, register_builtin_skill
# ── /commit ────────────────────────────────────────────────────────────────
_COMMIT_PROMPT = """\
Review the current git state and create a well-structured commit.
## Steps
1. Run `git status` and `git diff --staged` to see what is staged.
- If nothing is staged, run `git diff` to see unstaged changes, then stage relevant files.
2. Analyze the changes:
- Summarize the nature of the change (feature, bug fix, refactor, docs, etc.)
- Write a concise commit title (≤72 chars) focusing on *why*, not just *what*.
- If multiple logical changes exist, ask the user whether to split them.
3. Create the commit:
```
git commit -m "<title>"
```
If additional context is needed, add a body separated by a blank line.
4. Print the commit hash and summary when done.
**Rules:**
- Never use `--no-verify`.
- Never commit files that likely contain secrets (.env, credentials, keys).
- Prefer imperative mood in the title: "Add X", "Fix Y", "Refactor Z".
User context: $ARGUMENTS
"""
_REVIEW_PROMPT = """\
Review the code or pull request and provide structured feedback.
## Steps
1. Understand the scope:
- If a PR number or URL is given in $ARGUMENTS, use `gh pr view $ARGUMENTS --patch` to get the diff.
- Otherwise, use `git diff main...HEAD` (or `git diff HEAD~1`) for local changes.
2. Analyze the diff:
- Correctness: Are there bugs, edge cases, or logic errors?
- Security: Injection, auth issues, exposed secrets, unsafe operations?
- Performance: N+1 queries, unnecessary allocations, blocking calls?
- Style: Does it follow existing conventions in the codebase?
- Tests: Are new behaviors tested? Do existing tests cover the change?
3. Write a structured review:
```
## Summary
One-line overview of what the change does.
## Issues
- [CRITICAL/MAJOR/MINOR] Description and location
## Suggestions
- Nice-to-have improvements
## Verdict
APPROVE / REQUEST CHANGES / COMMENT
```
4. If changes are needed, list specific file:line references.
User context: $ARGUMENTS
"""
def _register_builtins() -> None:
register_builtin_skill(SkillDef(
name="commit",
description="Review staged changes and create a well-structured git commit",
triggers=["/commit"],
tools=["Bash", "Read"],
prompt=_COMMIT_PROMPT,
file_path="<builtin>",
when_to_use="Use when the user wants to commit changes. Triggers: '/commit', 'commit changes', 'make a commit'.",
argument_hint="[optional context]",
arguments=[],
user_invocable=True,
context="inline",
source="builtin",
))
register_builtin_skill(SkillDef(
name="review",
description="Review code changes or a pull request and provide structured feedback",
triggers=["/review", "/review-pr"],
tools=["Bash", "Read", "Grep"],
prompt=_REVIEW_PROMPT,
file_path="<builtin>",
when_to_use="Use when the user wants a code review. Triggers: '/review', '/review-pr', 'review this PR'.",
argument_hint="[PR number or URL]",
arguments=["pr"],
user_invocable=True,
context="inline",
source="builtin",
))
_register_builtins()

View File

@@ -0,0 +1,66 @@
"""Skill execution: inline (current conversation) or forked (sub-agent)."""
from __future__ import annotations
from typing import Generator
from .loader import SkillDef, substitute_arguments
def execute_skill(
skill: SkillDef,
args: str,
state,
config: dict,
system_prompt: str,
) -> Generator:
"""Execute a skill.
If skill.context == "fork", runs as an isolated sub-agent and yields its events.
Otherwise (inline), injects the rendered prompt into the current agent loop.
Args:
skill: SkillDef to execute
args: raw argument string from user (after the trigger word)
state: AgentState
config: config dict (may contain _depth, model, etc.)
system_prompt: current system prompt string
Yields:
agent events (TextChunk, ToolStart, ToolEnd, TurnDone, …)
"""
rendered = substitute_arguments(skill.prompt, args, skill.arguments)
message = f"[Skill: {skill.name}]\n\n{rendered}"
if skill.context == "fork":
yield from _execute_forked(skill, message, config, system_prompt)
else:
yield from _execute_inline(message, state, config, system_prompt)
def _execute_inline(message: str, state, config: dict, system_prompt: str) -> Generator:
"""Run skill prompt inline in the current conversation."""
import agent as _agent
yield from _agent.run(message, state, config, system_prompt)
def _execute_forked(
skill: SkillDef,
message: str,
config: dict,
system_prompt: str,
) -> Generator:
"""Run skill as an isolated sub-agent (separate conversation context)."""
import agent as _agent
# Build a sub-agent config with depth tracking
depth = config.get("_depth", 0) + 1
sub_config = {**config, "_depth": depth, "_system_prompt": system_prompt}
if skill.model:
sub_config["model"] = skill.model
# Restrict tools if skill specifies allowed-tools
if skill.tools:
sub_config["_allowed_tools"] = skill.tools
# Run in fresh state (no shared history)
sub_state = _agent.AgentState()
yield from _agent.run(message, sub_state, sub_config, system_prompt)

View File

@@ -0,0 +1,184 @@
"""Skill loading: parse markdown files with YAML frontmatter into SkillDef objects."""
from __future__ import annotations
from dataclasses import dataclass, field
from pathlib import Path
from typing import Optional
@dataclass
class SkillDef:
name: str
description: str
triggers: list[str] # ["/commit", "commit changes"]
tools: list[str] # ["Bash", "Read"] (allowed-tools)
prompt: str # full prompt body after frontmatter
file_path: str
# Enhanced fields
when_to_use: str = "" # when Claude should auto-invoke this skill
argument_hint: str = "" # e.g. "[branch] [description]"
arguments: list[str] = field(default_factory=list) # named arg names
model: str = "" # model override
user_invocable: bool = True # appears in /skills list
context: str = "inline" # "inline" or "fork" (fork = sub-agent)
source: str = "user" # "user", "project", "builtin"
# ── Directory paths ────────────────────────────────────────────────────────
def _get_skill_paths() -> list[Path]:
return [
Path.cwd() / ".nano_claude" / "skills", # project-level (priority)
Path.home() / ".nano_claude" / "skills", # user-level
]
# ── List field parser ──────────────────────────────────────────────────────
def _parse_list_field(value: str) -> list[str]:
"""Parse YAML-like list: ``[a, b, c]`` or ``"a, b, c"``."""
value = value.strip()
if value.startswith("[") and value.endswith("]"):
value = value[1:-1]
return [item.strip().strip('"').strip("'") for item in value.split(",") if item.strip()]
# ── Single-file parser ─────────────────────────────────────────────────────
def _parse_skill_file(path: Path, source: str = "user") -> Optional[SkillDef]:
"""Parse a markdown file with ``---`` frontmatter into a SkillDef.
Frontmatter fields:
name, description, triggers, tools / allowed-tools,
when_to_use, argument-hint, arguments, model,
user-invocable, context
"""
try:
text = path.read_text(encoding="utf-8")
except Exception:
return None
if not text.startswith("---"):
return None
parts = text.split("---", 2)
if len(parts) < 3:
return None
frontmatter_raw = parts[1].strip()
prompt = parts[2].strip()
fields: dict[str, str] = {}
for line in frontmatter_raw.splitlines():
line = line.strip()
if not line or ":" not in line:
continue
key, _, val = line.partition(":")
fields[key.strip().lower()] = val.strip()
name = fields.get("name", "")
if not name:
return None
# allowed-tools wins over tools if present
tools_raw = fields.get("allowed-tools", fields.get("tools", ""))
tools = _parse_list_field(tools_raw) if tools_raw else []
triggers_raw = fields.get("triggers", "")
triggers = _parse_list_field(triggers_raw) if triggers_raw else [f"/{name}"]
arguments_raw = fields.get("arguments", "")
arguments = _parse_list_field(arguments_raw) if arguments_raw else []
user_invocable_raw = fields.get("user-invocable", "true")
user_invocable = user_invocable_raw.lower() not in ("false", "0", "no")
context = fields.get("context", "inline").strip().lower()
if context not in ("inline", "fork"):
context = "inline"
return SkillDef(
name=name,
description=fields.get("description", ""),
triggers=triggers,
tools=tools,
prompt=prompt,
file_path=str(path),
when_to_use=fields.get("when_to_use", ""),
argument_hint=fields.get("argument-hint", ""),
arguments=arguments,
model=fields.get("model", ""),
user_invocable=user_invocable,
context=context,
source=source,
)
# ── Registry of built-in skills (registered by builtin.py) ────────────────
_BUILTIN_SKILLS: list[SkillDef] = []
def register_builtin_skill(skill: SkillDef) -> None:
_BUILTIN_SKILLS.append(skill)
# ── Load all skills ────────────────────────────────────────────────────────
def load_skills(include_builtins: bool = True) -> list[SkillDef]:
"""Return skills from disk + builtins, deduplicated (project > user > builtin)."""
seen: dict[str, SkillDef] = {}
# Builtins go in first (lowest priority)
if include_builtins:
for sk in _BUILTIN_SKILLS:
seen[sk.name] = sk
# User-level next, project-level last (highest priority)
skill_paths = _get_skill_paths()
for i, skill_dir in enumerate(reversed(skill_paths)):
src = "user" if i == 0 else "project"
if not skill_dir.is_dir():
continue
for md_file in sorted(skill_dir.glob("*.md")):
skill = _parse_skill_file(md_file, source=src)
if skill:
seen[skill.name] = skill
return list(seen.values())
def find_skill(query: str) -> Optional[SkillDef]:
"""Find a skill whose trigger matches the first word (or whole string) of query."""
query = query.strip()
if not query:
return None
first_word = query.split()[0]
for skill in load_skills():
for trigger in skill.triggers:
if first_word == trigger:
return skill
if trigger.startswith(first_word + " "):
return skill
return None
# ── Argument substitution ─────────────────────────────────────────────────
def substitute_arguments(prompt: str, args: str, arg_names: list[str]) -> str:
"""Replace $ARGUMENTS (whole args string) and $ARG_NAME placeholders.
Named args are positional: first word → first name, etc.
"""
# Always substitute $ARGUMENTS
result = prompt.replace("$ARGUMENTS", args)
# Named args: split by whitespace
arg_values = args.split()
for i, arg_name in enumerate(arg_names):
placeholder = f"${arg_name.upper()}"
value = arg_values[i] if i < len(arg_values) else ""
result = result.replace(placeholder, value)
return result

View File

@@ -0,0 +1,110 @@
"""Skill tool: lets the model invoke skills by name via tool call."""
from __future__ import annotations
from tool_registry import ToolDef, register_tool
from .loader import find_skill, load_skills, substitute_arguments
_SKILL_SCHEMA = {
"name": "Skill",
"description": (
"Invoke a named skill (reusable prompt template). "
"Use SkillList to see available skills and their triggers."
),
"input_schema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Skill name (e.g. 'commit', 'review')",
},
"args": {
"type": "string",
"description": "Arguments to pass to the skill (replaces $ARGUMENTS)",
"default": "",
},
},
"required": ["name"],
},
}
_SKILL_LIST_SCHEMA = {
"name": "SkillList",
"description": "List all available skills with their names, triggers, and descriptions.",
"input_schema": {
"type": "object",
"properties": {},
"required": [],
},
}
def _skill_tool(params: dict, config: dict) -> str:
"""Execute a skill by name and return its output."""
skill_name = params.get("name", "").strip()
args = params.get("args", "")
# Look up by name first, then by trigger
skill = None
for s in load_skills():
if s.name == skill_name:
skill = s
break
if skill is None:
skill = find_skill(skill_name)
if skill is None:
names = [s.name for s in load_skills()]
return f"Error: skill '{skill_name}' not found. Available: {', '.join(names)}"
rendered = substitute_arguments(skill.prompt, args, skill.arguments)
message = f"[Skill: {skill.name}]\n\n{rendered}"
# Run inline via agent and collect text output
import agent as _agent
system_prompt = config.get("_system_prompt", "")
# Collect output text
output_parts: list[str] = []
sub_state = _agent.AgentState()
sub_config = {**config, "_depth": config.get("_depth", 0) + 1}
try:
for event in _agent.run(message, sub_state, sub_config, system_prompt):
if hasattr(event, "text"):
output_parts.append(event.text)
except Exception as e:
return f"Skill execution error: {e}"
return "".join(output_parts) or "(skill completed with no text output)"
def _skill_list_tool(params: dict, config: dict) -> str:
skills = load_skills()
if not skills:
return "No skills available."
lines = ["Available skills:\n"]
for s in skills:
triggers = ", ".join(s.triggers)
hint = f" args: {s.argument_hint}" if s.argument_hint else ""
when = f"\n when: {s.when_to_use}" if s.when_to_use else ""
lines.append(f"- **{s.name}** [{triggers}]{hint}\n {s.description}{when}")
return "\n".join(lines)
def _register() -> None:
register_tool(ToolDef(
name="Skill",
schema=_SKILL_SCHEMA,
func=_skill_tool,
read_only=False,
concurrent_safe=False,
))
register_tool(ToolDef(
name="SkillList",
schema=_SKILL_LIST_SCHEMA,
func=_skill_list_tool,
read_only=True,
concurrent_safe=True,
))
_register()