From 7d504205e35cf6c0e50f13d1b2e493b2d2d9c80c Mon Sep 17 00:00:00 2001 From: 0x4D5352 <67082011+0x4D5352@users.noreply.github.com> Date: Thu, 5 Mar 2026 21:51:46 -0600 Subject: [PATCH 1/2] feat: add Agent Skills compatible nushell skill --- .agents/README.md | 12 + .agents/skills/nushell/SKILL.md | 71 ++++++ .agents/skills/nushell/references/advanced.md | 164 ++++++++++++++ .../nushell/references/bash-equivalents.md | 118 ++++++++++ .agents/skills/nushell/references/commands.md | 204 +++++++++++++++++ .../nushell/references/data-analysis.md | 173 ++++++++++++++ .../nushell/references/http-transport.md | 58 +++++ .../nushell/references/types-and-syntax.md | 212 ++++++++++++++++++ 8 files changed, 1012 insertions(+) create mode 100644 .agents/README.md create mode 100644 .agents/skills/nushell/SKILL.md create mode 100644 .agents/skills/nushell/references/advanced.md create mode 100644 .agents/skills/nushell/references/bash-equivalents.md create mode 100644 .agents/skills/nushell/references/commands.md create mode 100644 .agents/skills/nushell/references/data-analysis.md create mode 100644 .agents/skills/nushell/references/http-transport.md create mode 100644 .agents/skills/nushell/references/types-and-syntax.md diff --git a/.agents/README.md b/.agents/README.md new file mode 100644 index 00000000..dd325933 --- /dev/null +++ b/.agents/README.md @@ -0,0 +1,12 @@ +# AGENTS directory + +This directory holds examples for use with coding agents that interact with Nu, +either as a CLI tool or via the Nushell MCP server. + +## Skills + +`./skills/nushell/` - An [Agent Skills](https://agentskills.io/home) compliant +skill directory built via the Claude Code skill-writer superpower and human-tested +for validity and efficacy. Copy this directory to `~/.claude/skills/` for global +use in Claude Code, or to `~/.agent/skills/` for most non-Claude Code agentic +tools. See your preferred LLM provider's documentation for specific details. diff --git a/.agents/skills/nushell/SKILL.md b/.agents/skills/nushell/SKILL.md new file mode 100644 index 00000000..b1033ded --- /dev/null +++ b/.agents/skills/nushell/SKILL.md @@ -0,0 +1,71 @@ +--- +name: nushell +description: Use when writing or running Nushell commands, scripts, or pipelines - via the Nushell MCP server (mcp__nushell__evaluate), via Bash (nu -c), or in .nu script files. Also use when working with structured data (JSON, YAML, TOML, CSV, Parquet, SQLite), doing ad-hoc data analysis or exploration, or when the user's shell is Nushell. +--- + +# Using Nushell + +Nushell is a structured-data shell. Commands pass **tables, records, and lists** through pipelines - not text. + +**Two execution paths:** +- **MCP server**: `mcp__nushell__evaluate` - persistent REPL (variables survive across calls) +- **Bash tool**: `nu -c ''` - one-shot (use single quotes for outer wrapper) + +## Critical Rules + +**NEVER use bare `print` in MCP stdio mode.** Output will be lost (returns empty `[]`). Use `print -e "msg"` for stderr, or just return the value (implicit return). + +**String interpolation uses parentheses, NOT curly braces:** +```nu +# WRONG: $"hello {$name}" +# CORRECT: $"hello ($name)" +$"($env.HOME)/docs" $"2 + 2 = (2 + 2)" $"files: (ls | length)" +``` +Gotcha: `$"(some text)"` errors - parens are evaluated as code. Escape literal parens: `\(text\)`. + +**No bash syntax:** `cmd1; cmd2` not `&&`, `o+e>|` not `2>&1`, `$env.VAR` not `$VAR`, `(cmd)` not `$(cmd)`. + +## Common Mistakes + +| Mistake | Fix | +|---------|-----| +| `$"hello {$name}"` | `$"hello ($name)"` | +| `print "msg"` in MCP | `print -e "msg"` or return value | +| `command 2>&1` | `command o+e>\| ...` | +| `$HOME/path` | `$env.HOME` or `$"($env.HOME)/path"` | +| `export FOO=bar` | `$env.FOO = "bar"` | +| Mutating in closure | Use `reduce`, `generate`, or `each` | +| `\u001b` for ANSI | `ansi strip` to remove, `char --unicode '1b'` for ESC | +| `where ($in.a > 1) and ($in.b > 2)` | Second `$in` rebinds to bool. Use bare cols: `where a > 1 and b > 2` | +| `where not ($in.col \| cmd)` | `not` breaks `$in`. Use `where ($in.col \| cmd) == false` | +| `where col \| cmd` (no parens) | Parsed as two pipeline stages. Use `where ($in.col \| cmd)` | + +## When to Use Nushell + +**Always prefer Nushell for:** +- Any structured data (JSON, YAML, TOML, CSV, Parquet, SQLite) - unifies all formats +- CLI tools with `--json` flags - pipe JSON output directly into Nushell for querying (e.g. `^gh pr list --json title,state | from json`) +- Ad-hoc data analysis and exploration - faster than Python setup +- Initial data science/analytics - histograms, tabular output, basic aggregations +- Polars plugin for large datasets - DataFrames without Python overhead + +**Use Bash only when:** bash-specific tooling, MCP unavailable, or bash-syntax integrations. + +## Reference Files + +Read the relevant file(s) based on what you need: + +| File | Read when you need... | +|------|-----------------------| +| [commands.md](references/commands.md) | Command reference tables (filters, strings, conversions, filesystem, math, dates) | +| [types-and-syntax.md](references/types-and-syntax.md) | Type system, string types, operators, variables, control flow, cell-paths | +| [data-analysis.md](references/data-analysis.md) | Format conversion, HTTP, Polars, SQLite, aggregation patterns | +| [advanced.md](references/advanced.md) | Custom commands, modules, error handling, jobs, external commands, env config | +| [bash-equivalents.md](references/bash-equivalents.md) | Complete Bash-to-Nushell translation table | +| [http-transport.md](references/http-transport.md) | Differences when using HTTP transport instead of stdio | + +## MCP Server Quick Notes + +- `mcp__nushell__evaluate` - run code; `mcp__nushell__list_commands` - discover; `mcp__nushell__command_help` - help +- State persists between calls. `$history` stores prior results (access `$history.0`, etc.) +- Use `| to text` or `| to json` for large outputs. Use `ansi strip` for color removal. diff --git a/.agents/skills/nushell/references/advanced.md b/.agents/skills/nushell/references/advanced.md new file mode 100644 index 00000000..e8e1fa41 --- /dev/null +++ b/.agents/skills/nushell/references/advanced.md @@ -0,0 +1,164 @@ +# Nushell Advanced Patterns + +## Custom Commands with Full Signatures + +```nu +def search [ + pattern: string # required positional + path?: string # optional positional + --case-sensitive(-c) # boolean flag (defaults false) + --max-depth(-d): int # flag with value + ...extra: string # rest params +]: nothing -> list { + # implementation +} +``` + +### Pipeline Input/Output Types +```nu +def "str stats" []: string -> record { + let input = $in + {length: ($input | str length), words: ($input | split words | length)} +} + +# Multiple input types +def process []: [string -> record, list -> table] { ... } +``` + +### Environment-Modifying Commands +```nu +def --env goto [dir: string] { + cd $dir + $env.LAST_DIR = (pwd) +} +``` + +### Wrapped External Commands +```nu +def --wrapped mygrep [...rest] { + ^grep --color=always ...$rest +} +``` + +## Modules + +```nu +module utils { + export def double [x: number] { $x * 2 } + export def triple [x: number] { $x * 3 } + export-env { $env.UTILS_LOADED = true } +} + +use utils double # import specific command +use utils * # import all exports +use utils [] # import only environment +``` + +## Generate (Stateful Sequences) + +Closure comes first, initial value second. Returns `{out: value, next: state}`. Omit `next` to stop. + +```nu +# Fibonacci +generate {|fib| {out: $fib.0, next: [$fib.1, ($fib.0 + $fib.1)]}} [0, 1] | first 10 + +# Counter +generate {|i| if $i <= 5 { {out: $i, next: ($i + 1)} }} 0 + +# With input stream (two-arg closure) +1..5 | generate {|e, sum=0| let sum = $e + $sum; {out: $sum, next: $sum}} +``` + +## Functional Alternatives to Mutable State + +```nu +# Instead of: mut sum = 0; for x in list { $sum += $x } +[1 2 3 4 5] | reduce {|it, acc| $acc + $it} + +# With initial value +[2 3 4 5] | reduce --fold 1 {|it, acc| $acc * $it} + +# Instead of: mut results = []; for x in list { $results = ($results | append (f $x)) } +$list | each {|x| process $x} + +# Instead of: while with mutation +generate {|state| if $state.done { null } else { {out: $state.val, next: (advance $state)} }} $initial +``` + +## Error Handling + +```nu +# Basic try/catch +try { open nonexistent.json } catch {|e| $"Failed: ($e.msg)" } + +# With finally (v0.111.0+) +try { do-work } catch {|e| log-error $e.msg } finally { cleanup } + +# Custom errors +error make {msg: "Value must be non-negative"} +error make {msg: "Bad value", label: {text: "here", span: (metadata $value).span}} +``` + +## External Commands + +```nu +^external_cmd args # explicit external invocation +ls | to text | ^grep pattern # pipe structured data to external +^cmd arg1 arg2 o+e>| str trim # capture stdout+stderr +^cmd | complete # get {exit_code, stdout, stderr} record +$env.LAST_EXIT_CODE # check last exit code + +# External output to structured data +^git log --oneline -5 | lines | parse "{hash} {message}" + +# Pipe structured data to external +{data: "value"} | to json | ^curl -X POST -d @- https://api.example.com +``` + +## Background Jobs + +```nu +job spawn { long_running_cmd } # returns job ID +job spawn --tag "server" { uvicorn main:app } # with description +job list # list running jobs +job kill $id # terminate job + +# Getting results back via mailbox (main thread = job 0) +job spawn { expensive_calc | job send 0 }; job recv +job spawn { cmd | job send 0 --tag 1 }; job recv --tag 1 # filtered +job recv --timeout 5sec # with timeout +``` + +**Key rules:** `job recv` reads from current job's mailbox only (no job ID arg). Background jobs send to main with `job send 0`. Use `--tag` for filtering. + +## Environment & Configuration + +```nu +$env.PATH # PATH (list with ENV_CONVERSIONS, string otherwise) +$env.Path # PATH on Windows +$env.PATH = ($env.PATH | append "/new/path") +$env.MY_VAR = "value" +$env.MY_VAR? | default "fallback" # safe access with default +hide-env MY_VAR # unset variable + +# Configuration +$env.config.show_banner = false # set individual config values +$nu.default-config-dir # config directory path +$nu.home-dir # home directory +config nu # edit config in $EDITOR +``` + +## Using nu -c from Bash + +```bash +# Simple command +nu -c 'ls | where size > 1mb | to json' + +# String interpolation (outer single quotes to avoid $ conflicts) +nu -c 'let x = 42; $"answer: ($x)"' + +# Multi-statement +nu -c 'let data = (open file.json); $data | get field' +``` + +Use single quotes for the outer wrapper since Nushell uses `$` for variables. diff --git a/.agents/skills/nushell/references/bash-equivalents.md b/.agents/skills/nushell/references/bash-equivalents.md new file mode 100644 index 00000000..cc37728a --- /dev/null +++ b/.agents/skills/nushell/references/bash-equivalents.md @@ -0,0 +1,118 @@ +# Bash to Nushell Translation + +## File and Directory Operations + +| Bash | Nushell | +|------|---------| +| `ls` | `ls` | +| `ls -la` | `ls --long --all` or `ls -la` | +| `ls -d */` | `ls \| where type == dir` | +| `find . -name "*.rs"` | `glob **/*.rs` or `ls **/*.rs` | +| `mkdir -p path` | `mkdir path` (creates parents automatically) | +| `touch file` | `touch file` | +| `cp src dst` | `cp src dst` | +| `cp -r src dst` | `cp -r src dst` | +| `mv old new` | `mv old new` | +| `rm file` | `rm file` | +| `rm -rf dir` | `rm -r dir` | +| `cat file` | `open --raw file` | +| `head -5 file` | `open --raw file \| lines \| first 5` | +| `tail -5 file` | `open --raw file \| lines \| last 5` | +| `wc -l file` | `open --raw file \| lines \| length` | + +## Output Redirection + +| Bash | Nushell | +|------|---------| +| `> file` | `o> file` or `\| save file` | +| `>> file` | `o>> file` or `\| save --append file` | +| `> /dev/null` | `\| ignore` | +| `> /dev/null 2>&1` | `o+e>\| ignore` | +| `2>&1` | `o+e>\| ...` | +| `cmd \| tee log \| cmd2` | `cmd \| tee { save log } \| cmd2` | + +## Text Processing + +| Bash | Nushell | +|------|---------| +| `grep pattern` | `where $it =~ pattern` or `find pattern` | +| `grep -r pattern .` | `open --raw file \| lines \| where $it =~ pattern` | +| `sed 's/old/new/'` | `str replace 'old' 'new'` | +| `sed 's/old/new/g'` | `str replace --all 'old' 'new'` | +| `awk '{print $1}'` | `split column ' ' \| get column0` | +| `sort` | `sort` | +| `sort -u` | `sort \| uniq` | +| `uniq` | `uniq` | +| `cut -d: -f1` | `split column ':' \| get column0` | +| `tr '[:upper:]' '[:lower:]'` | `str downcase` | + +## Variables and Environment + +| Bash | Nushell | +|------|---------| +| `VAR=value` | `let var = "value"` | +| `export VAR=value` | `$env.VAR = "value"` | +| `echo $VAR` | `$env.VAR` | +| `echo $HOME` | `$env.HOME` | +| `echo $PATH` | `$env.PATH` (list on macOS/Linux) | +| `echo $?` | `$env.LAST_EXIT_CODE` | +| `unset VAR` | `hide-env VAR` | +| `${VAR:-default}` | `$env.VAR? \| default "value"` | +| `export PATH=$PATH:/new` | `$env.PATH = ($env.PATH \| append "/new")` | +| `FOO=bar cmd` | `FOO=bar cmd` (same syntax) | + +## Command Execution + +| Bash | Nushell | +|------|---------| +| `cmd1 && cmd2` | `cmd1; cmd2` | +| `cmd1 \|\| cmd2` | `try { cmd1 } catch { cmd2 }` | +| `$(command)` | `(command)` | +| `` `command` `` | `(command)` | +| `command &` | `job spawn { command }` | +| `jobs` | `job list` | +| `kill %1` | `job kill $id` | +| `type cmd` | `which cmd` | +| `man cmd` | `help cmd` | +| `bash -c 'code'` | `nu -c 'code'` | +| `source script.sh` | `source script.nu` | + +## Control Flow + +| Bash | Nushell | +|------|---------| +| `if [ cond ]; then ... fi` | `if $cond { ... }` | +| `[ -f file ]` | `"file" \| path exists` | +| `[ -d dir ]` | `("dir" \| path exists) and ("dir" \| path type) == "dir"` | +| `for f in *.md; do echo $f; done` | `ls *.md \| each { $in.name }` | +| `for i in $(seq 1 10); do ...; done` | `for i in 1..10 { ... }` | +| `while read line; do ...; done < file` | `open file \| lines \| each {\|line\| ... }` | +| `case $x in ... esac` | `match $x { pattern => result, ... }` | + +## Pipes and Subshells + +| Bash | Nushell | +|------|---------| +| `cmd \| head -5` | `cmd \| first 5` | +| `cmd \| tail -5` | `cmd \| last 5` | +| `\` (line continuation) | `(` wrap in parens `)` | +| `read var` | `let var = (input)` | +| `read -s secret` | `let secret = (input -s)` | + +## History + +| Bash | Nushell | +|------|---------| +| `!!` | `!!` (inserts, doesn't execute) | +| `!$` | `!$` | +| `Ctrl+R` | `Ctrl+R` | + +## Key Behavioral Differences + +1. **Structured vs text**: Nushell pipelines pass tables/records, not text streams +2. **Immutable by default**: `let` is immutable; use `mut` when needed +3. **No word splitting**: Nushell doesn't split variables on whitespace +4. **No glob expansion in variables**: Globs only expand in command position +5. **Static parsing**: Code is parsed before execution; no `eval` equivalent +6. **Implicit return**: Last expression is the return value; no `echo` needed +7. **`>` is comparison**: Use `o>` or `save` for file redirection diff --git a/.agents/skills/nushell/references/commands.md b/.agents/skills/nushell/references/commands.md new file mode 100644 index 00000000..5c71c7a4 --- /dev/null +++ b/.agents/skills/nushell/references/commands.md @@ -0,0 +1,204 @@ +# Nushell Command Reference + +## Filters +| Command | Purpose | Example | +|---------|---------|---------| +| `where` | Filter rows | `ls \| where size > 1mb` | +| `select` | Choose columns (returns table) | `ls \| select name size` | +| `get` | Extract value (returns raw value) | `ls \| get name` | +| `reject` | Remove columns | `ls \| reject mode` | +| `first` / `last` | Take from ends | `ls \| first 5` | +| `skip` / `take` | Skip/take rows | `ls \| skip 2` | +| `sort-by` | Sort by column | `ls \| sort-by size --reverse` | +| `reverse` | Reverse order | `[3 1 2] \| reverse` | +| `uniq` | Deduplicate | `[1 1 2] \| uniq` | +| `uniq-by` | Deduplicate by column | `$t \| uniq-by name` | +| `group-by` | Group rows | `$t \| group-by col --to-table` | +| `flatten` | Unnest lists/tables | `[[1 2] [3]] \| flatten` | +| `merge` | Merge tables horizontally | `$a \| merge $b` | +| `transpose` | Pivot record/table | `{a: 1} \| transpose k v` | +| `enumerate` | Add index | `[a b] \| enumerate` | +| `zip` | Pair elements | `[1 2] \| zip [a b]` | +| `window` | Sliding window | `[1 2 3 4] \| window 2` | +| `each` | Transform items | `[1 2] \| each {\|x\| $x * 2}` | +| `par-each` | Parallel transform | `[1 2] \| par-each {\|x\| $x * 2}` | +| `reduce` | Fold to value | `[1 2 3] \| reduce {\|it, acc\| $acc + $it}` | +| `any` | Test any match | `[1 2] \| any {\|x\| $x > 1}` | +| `all` | Test all match | `[1 2] \| all {\|x\| $x > 0}` | +| `length` | Count items | `[1 2 3] \| length` | +| `is-empty` | Check empty | `[] \| is-empty` | +| `columns` | Get column names | `{a: 1} \| columns` | +| `values` | Get values | `{a: 1} \| values` | +| `items` | Iterate key-value | `{a: 1} \| items {\|k,v\| ...}` | +| `insert` | Add column/index | `{a: 1} \| insert b 2` | +| `update` | Modify existing | `{a: 1} \| update a 10` | +| `upsert` | Insert or update | `{a: 1} \| upsert b 2` | +| `rename` | Rename columns | `$t \| rename col1 col2` | +| `move` | Reorder columns | `$t \| move col --after other` | +| `wrap` | Wrap as table column | `[1 2] \| wrap val` | +| `default` | Default for null | `null \| default 0` | +| `compact` | Remove nulls | `[1 null 3] \| compact` | +| `find` | Search text/data | `ls \| find ".rs"` | +| `tee` | Split pipeline | `ls \| tee { save log } \| length` | + +### `where` Clause Edge Cases + +**Piping inside `where` REQUIRES parentheses:** +```nu +# WRONG - parsed as (where col) | (cmd), silently returns wrong results +$table | where name | str ends-with ".csv" + +# CORRECT - parens make the expression evaluate per-row +$table | where ($in.name | str ends-with ".csv") +``` + +**`not` breaks `$in` binding:** +```nu +# WRONG - $in not found inside not (...) +$table | where not ($in.name | str ends-with ".csv") + +# CORRECT - negate after the expression +$table | where ($in.name | str ends-with ".csv") == false +``` + +**Multiple `$in` expressions with `and`/`or` — second `$in` rebinds to the bool result:** +```nu +# WRONG - second $in refers to the bool from first paren, not the row +$table | where ($in.size > 1kb) and ($in.name =~ ".csv") + +# CORRECT - use bare column names (preferred for simple comparisons) +$table | where size > 1kb and name =~ ".csv" +# CORRECT - use closure style for complex conditions +$table | where {|row| $row.size > 1kb and $row.name =~ ".csv"} +``` + +### Filter Gotchas + +- **`find` injects ANSI highlight codes** into matched strings — pipe through `ansi strip` to clean +- **`tee` takes a block, not a parameterized closure** — use `tee { $in | length }`, not `tee {|x| ...}` +- **`$in` semantics differ**: in `update` closures, `$in` = cell value; in `insert` closures, `$in` = entire row +- **`length`** only works on lists/tables — not records (`| columns | length`) or strings (`| str length`) +- **`columns`/`items`** only work on records (and `columns` also on tables) — not plain lists +- **`[] | first` / `[] | last`** return `[]` silently instead of erroring +- **`sort-by {} --natural`** fails — `{}` is a record, not a closure. Use `sort --natural` for lists +- **`group-by col --to-table`** produces columns named after the grouper and `items` (not `group`) +- **`select a.b`** on nested records creates a flat column literally named `"a.b"` + +## String Commands +| Command | Purpose | Example | +|---------|---------|---------| +| `str trim` | Trim whitespace | `" hi " \| str trim` | +| `str upcase` | Uppercase | `"hi" \| str upcase` | +| `str downcase` | Lowercase | `"HI" \| str downcase` | +| `str replace` | Replace text | `"hi" \| str replace "hi" "hello"` | +| `str contains` | Check substring | `"hello" \| str contains "ell"` | +| `str starts-with` | Check prefix | `"hello" \| str starts-with "he"` | +| `str ends-with` | Check suffix | `"hello" \| str ends-with "lo"` | +| `str substring` | Extract range | `"hello" \| str substring 1..3` | +| `str length` | String length | `"hello" \| str length` | +| `str reverse` | Reverse | `"hello" \| str reverse` | +| `str join` | Join list | `["a" "b"] \| str join ","` | +| `str index-of` | Find position | `"hello" \| str index-of "ll"` | +| `split row` | Split string | `"a,b" \| split row ","` | +| `split column` | Split to table | `"a:b" \| split column ":"` | +| `split chars` | To char list | `"abc" \| split chars` | +| `lines` | Split by newline | `"a\nb" \| lines` | +| `parse` | Extract patterns | `"v1.2" \| parse "v{major}.{minor}"` | +| `fill` | Pad string | `"42" \| fill -a right -c '0' -w 5` | +| `ansi strip` | Remove ANSI codes | `$colored \| ansi strip` | + +## Conversion Commands +| Command | Purpose | Example | +|---------|---------|---------| +| `into int` | Convert to int | `"42" \| into int` | +| `into float` | Convert to float | `"3.14" \| into float` | +| `into string` | Convert to string | `42 \| into string` | +| `into bool` | Convert to bool | `1 \| into bool` | +| `into datetime` | Parse date | `"2025-01-15" \| into datetime` | +| `into duration` | Parse duration | `"5sec" \| into duration` | +| `into filesize` | Parse size | `"1gb" \| into filesize` | +| `into binary` | Convert to binary | `"hello" \| into binary` | +| `into record` | Convert to record | `[[k v]; [a 1]] \| into record` | +| `to json` | Serialize JSON | `{a: 1} \| to json` | +| `to csv` | Serialize CSV | `$table \| to csv` | +| `to toml` | Serialize TOML | `{a: 1} \| to toml` | +| `to yaml` | Serialize YAML | `{a: 1} \| to yaml` | +| `to nuon` | Serialize NUON | `{a: 1} \| to nuon` | +| `to text` | Plain text | `$data \| to text` | +| `from json` | Parse JSON | `'{"a":1}' \| from json` | +| `from csv` | Parse CSV | `"a,b\n1,2" \| from csv` | +| `from toml` | Parse TOML | `$toml_str \| from toml` | +| `from yaml` | Parse YAML | `$yaml_str \| from yaml` | +| `from nuon` | Parse NUON | `$nuon_str \| from nuon` | +| `from tsv` | Parse TSV | `$tsv_str \| from tsv` | +| `from xml` | Parse XML | `$xml_str \| from xml` | +| `from ssv` | Parse space-separated | `$ssv_str \| from ssv` | + +## Filesystem Commands +| Command | Purpose | Example | +|---------|---------|---------| +| `ls` | List directory | `ls`, `ls **/*.rs` | +| `cd` | Change directory | `cd ~/projects` | +| `open` | Open file (auto-parse) | `open data.json` | +| `open --raw` | Open as raw text | `open --raw file.txt` | +| `save` | Save to file | `$data \| save file.json` | +| `save --append` | Append to file | `"text" \| save --append log.txt` | +| `save -f` | Force overwrite | `$data \| save -f file.json` | +| `cp` | Copy | `cp src dst` | +| `mv` | Move/rename | `mv old new` | +| `rm` | Remove | `rm file`, `rm -r dir` | +| `mkdir` | Create directory | `mkdir path/to/dir` | +| `touch` | Create file | `touch file.txt` | +| `glob` | Find files by pattern | `glob **/*.rs --depth 3` | +| `watch` | Watch for changes | `watch . --glob=**/*.rs {\|\| cargo test}` | + +## Path Commands +| Command | Purpose | Example | +|---------|---------|---------| +| `path join` | Join paths | `"/home" \| path join "user"` | +| `path parse` | Parse components | `"/a/b.txt" \| path parse` | +| `path exists` | Check existence | `"/tmp" \| path exists` | +| `path expand` | Resolve path | `"~" \| path expand` | +| `path basename` | File name | `"/a/b.txt" \| path basename` | +| `path dirname` | Directory | `"/a/b.txt" \| path dirname` | + +## System Commands +| Command | Purpose | +|---------|---------| +| `sys host` | Host info | +| `sys cpu` | CPU info | +| `sys mem` | Memory info | +| `sys disks` | Disk info | +| `ps` | Process list | +| `which cmd` | Find command | +| `sleep 1sec` | Sleep | + +## Math Commands +`math sum` `math avg` `math min` `math max` `math round` `math floor` +`math ceil` `math abs` `math sqrt` `math log` + +## Date Commands +```nu +date now # current datetime +date now | date to-timezone "UTC" # convert timezone +date now | format date "%Y-%m-%d" # format string +date now | into record # {year, month, day, ...} +``` + +## Random +```nu +random int # random integer +random int 1..100 # in range +random float # 0.0..1.0 +random bool # true or false +random chars --length 16 # random string +random uuid # UUID v4 +``` + +## Debug/Inspect +```nu +42 | describe # "int" +$val | describe --detailed # full type info +debug $val # debug representation +metadata $val # source info +``` diff --git a/.agents/skills/nushell/references/data-analysis.md b/.agents/skills/nushell/references/data-analysis.md new file mode 100644 index 00000000..0039c29e --- /dev/null +++ b/.agents/skills/nushell/references/data-analysis.md @@ -0,0 +1,173 @@ +# Nushell Data Analysis Reference + +## Format Conversion + +Nushell unifies structured data formats. `open` auto-detects by extension: +csv, eml, ics, ini, json, nuon, ods, ssv, toml, tsv, url, vcf, xlsx/xls, xml, yaml/yml, SQLite + +```nu +# Auto-detect +open data.json # parsed JSON -> table/record +open data.csv # parsed CSV -> table +open data.toml # parsed TOML -> record +open data.db # SQLite -> tables + +# Explicit parsing from strings +'{"a":1}' | from json +"a,b\n1,2" | from csv +$toml_string | from toml +$yaml_string | from yaml + +# Serialization +{a: 1} | to json +$table | to csv +{a: 1} | to toml +{a: 1} | to yaml +{a: 1} | to nuon # Nushell Object Notation + +# Raw text (skip parsing) +open --raw file.json # byte stream, not parsed +``` + +## Leveraging External JSON Output + +**If a CLI tool supports `--json` or similar structured output, prefer running it in Nushell** over Bash. The JSON is parsed automatically and queryable immediately: + +```nu +# External tool with JSON output -> instant structured data +^cargo metadata --format-version 1 | from json | get packages | select name version +^gh pr list --json title,state | from json | where state == "OPEN" +^docker ps --format json | lines | each { from json } | select Names State +^kubectl get pods -o json | from json | get items | select metadata.name status.phase +``` + +This eliminates the need for `jq`, `grep`, or `awk` pipelines. Any tool that outputs JSON becomes a first-class data source in Nushell. + +## HTTP Requests + +```nu +# GET - result is auto-parsed from JSON +http get https://api.example.com/data +http get https://api.example.com/data | get field + +# POST with JSON body +http post --content-type application/json https://api.example.com/endpoint {key: "val"} + +# POST with headers +http post https://api.example.com/sync -H {X-API-Key: "secret"} (bytes build) + +# POST with headers and body +http post --content-type application/json https://api.example.com/data -H {Authorization: "Bearer token"} {key: "value"} +``` + +## Data Manipulation Patterns + +### Grouping and Aggregation +```nu +# group-by with --to-table for aggregation +# Note: the group column is named after the grouper (e.g. "category"), not "group" +$data | group-by category --to-table | each {|g| + { + category: $g.category + count: ($g.items | length) + total: ($g.items | get price | math sum) + avg: ($g.items | get price | math avg) + } +} +``` + +### Pivoting Data +```nu +# Record to table +{name: "test", age: 30, city: "NYC"} | transpose key value + +# Table to record +[[key value]; [a 1] [b 2]] | into record +``` + +### Structured Log Parsing +```nu +# parse extracts named fields from text, returns a table (not a record) +"2025-01-15 ERROR auth: timeout" | parse "{date} {level} {service}: {message}" +# => table with one row: [{date: "2025-01-15", level: "ERROR", service: "auth", message: "timeout"}] +# Access first match: ... | parse "..." | first + +# Multi-line logs +open server.log | lines | parse "{date} [{level}] {msg}" | where level == "ERROR" +``` + +### Joining and Merging +```nu +# Horizontal merge (add columns) +$table1 | merge $table2 + +# Vertical append (add rows) +$table1 | append $table2 +$table1 ++ $table2 + +# Spread records +{...$base, ...$overrides, extra: "field"} +``` + +## SQLite + +```nu +open database.db # lists tables +open database.db | get table_name # get table data +open database.db | query db "SELECT * FROM users WHERE age > 25" +``` + +## Polars Plugin (Large Datasets) + +For datasets too large for Nushell's built-in table handling: + +```nu +plugin use polars + +# Open file formats efficiently +polars open large.parquet | polars select name status | polars collect +polars open data.csv | polars first 5 | polars into-nu + +# Convert Nushell table to DataFrame +ps | polars into-df | polars filter (polars col cpu > 50) | polars into-nu + +# Aggregation +polars open sales.csv + | polars group-by category + | polars agg (polars col price | polars sum) + | polars collect + +# Summary statistics +$data | polars into-df | polars summary + +# Save results +polars open input.parquet | polars select name status | polars save output.parquet +``` + +## Math/Statistics + +```nu +[1 2 3 4 5] | math sum # 15 +[1 2 3 4 5] | math avg # 3 +[1 2 3 4 5] | math min # 1 +[1 2 3 4 5] | math max # 5 +[1 2 3 4 5] | math median # 3 +[1.5 2.7] | math round # [2, 3] +[1.5 2.7] | math floor # [1, 2] +[1.5 2.7] | math ceil # [2, 3] +[-5 3] | math abs # [5, 3] +``` + +## NUON (Nushell Object Notation) + +A JSON superset supporting comments, optional commas, and Nushell literals: + +```nuon +{ + name: "project" # no quotes needed for simple keys + created: 2025-01-15 + timeout: 5sec + size: 1.5mb + data: [1 2 3] # commas optional +} +``` diff --git a/.agents/skills/nushell/references/http-transport.md b/.agents/skills/nushell/references/http-transport.md new file mode 100644 index 00000000..834627c8 --- /dev/null +++ b/.agents/skills/nushell/references/http-transport.md @@ -0,0 +1,58 @@ +# HTTP Transport Differences + +The Nushell MCP server can run in **stdio** (default) or **HTTP** transport mode. Most behavior is identical, but there are key differences to be aware of if using HTTP. + +## Parameter Renames + +| Tool | stdio parameter | HTTP parameter | +|---|---|---| +| `mcp__nushell__evaluate` | `command` | `input` | +| `mcp__nushell__command_help` | `command` | `name` | + +`mcp__nushell__list_commands` uses `find` in both modes. + +## Print Behavior + +| Pattern | stdio | HTTP | +|---|---|---| +| Bare `print "msg"` | `[]` (lost) | `[]` (lost) | +| `print -e "msg"` (stderr) | Captured | `[]` (lost) | +| Implicit return (just the value) | Works | Works | +| `print -e "msg"; "return val"` | Both visible | Only return value; stderr lost | + +**In HTTP mode there is no print workaround.** Always use implicit return (let the last expression be your output). + +## Response Format + +**stdio** returns a plain output string. + +**HTTP** returns a structured envelope: +``` +{cwd: "/path", history_index: 3, timestamp: 2026-03-06T..., output: "..."} +``` + +The `cwd` field is useful for tracking directory changes across calls. + +## Error Format + +**stdio** returns MCP error strings. + +**HTTP** returns MCP error `-32603` with a structured JSON body: +``` +{code: "nu::shell::error", msg: "...", labels: [[text, span, line, column]; ...]} +``` + +`try/catch` works identically in both modes. + +## Everything Else Is the Same + +These all work identically in both transports: +- Variable, env var, and custom `def` persistence across calls +- `$history` variable and `$env.NU_MCP_OUTPUT_LIMIT` truncation (use filesize type, e.g. `100b`) +- `job spawn` / `job send` / `job recv` (background jobs and mailbox) +- `cd` persistence, `glob`, `open`, `save`, file I/O +- Format conversions (`to json`, `to csv`, `to text`) +- `| complete` for capturing external command stderr/exit codes +- `par-each`, `reduce`, pipelines, closures +- `http get` / `http post` +- `ansi strip`, `list_commands`, `command_help` diff --git a/.agents/skills/nushell/references/types-and-syntax.md b/.agents/skills/nushell/references/types-and-syntax.md new file mode 100644 index 00000000..bf2d71f1 --- /dev/null +++ b/.agents/skills/nushell/references/types-and-syntax.md @@ -0,0 +1,212 @@ +# Nushell Types and Syntax Reference + +## Data Types + +| Type | Literal | Example | +|------|---------|---------| +| int | decimal, hex, octal, binary | `42`, `0xff`, `0o77`, `0b1010`, `100_000` | +| float | decimal point, scientific | `3.14`, `-2.5e10`, `Infinity`, `NaN` | +| string | quotes | `'single'`, `"double\n"`, `` `backtick` ``, `r#'raw'#` | +| bool | literal | `true`, `false` | +| duration | number + unit | `5sec`, `3min`, `2hr`, `1day`, `500ms`, `100ns`, `2wk` | +| filesize | number + unit | `64mb`, `1.5gb`, `10kib`, `2gib` | +| date | ISO format | `2025-01-15`, `2025-01-15T10:30:00-05:00` | +| range | `..` syntax | `1..5`, `0..<10`, `5..1`, `0..2..10` (step) | +| list | `[]` | `[1 2 3]`, `["a", "b"]`, `["mixed" 42 true]` | +| record | `{}` | `{name: "nu", ver: 1}`, `{"spaced key": true}` | +| table | list of records | `[{a: 1}, {a: 2}]`, `[[a b]; [1 2] [3 4]]` | +| nothing | `null` | `null` | +| binary | `0x[]` | `0x[FF EE]`, `0o[377]`, `0b[11111111]` | +| closure | `{\|\| }` | `{\|x\| $x + 1}`, `{\|x, y\| $x + $y}` | +| cell-path | dot notation | `$.name`, `$.0`, `$.users.0.name`, `$.field?` | +| glob | backtick | `` `**/*.rs` `` | + +## String Types in Detail + +**Single-quoted** - no escape processing: +```nu +'C:\path\to\file' # backslashes are literal +'no\nescape' # literal \n, not newline +``` + +**Double-quoted** - C-style escapes: +```nu +"line1\nline2" # newline +"tab\there" # tab +"quote: \"" # escaped quote +"\u{1F600}" # unicode codepoint +``` +Escapes: `\"` `\'` `\\` `\/` `\b` `\f` `\r` `\n` `\t` `\u{X...}` + +**Raw strings** - literal, can contain any quotes: +```nu +r#'Contains "double" and 'single' quotes'# +r##'Even r#'nested raw'# strings'## +``` + +**Backtick strings** - for paths/globs with spaces: +```nu +ls `./my directory` +ls `**/*.{rs,toml}` +``` + +**String interpolation** - `$"..."` or `$'...'`: +```nu +let name = "world" +$"Hello, ($name)!" # variable +$"2 + 2 = (2 + 2)" # expression +$"files: (ls | length)" # command +$"escaped parens: \(not code\)" # literal parens +$'single-quote interp: ($name)' # no escape processing +``` + +## Duration and Filesize Units + +**Duration**: `ns` `us` `ms` `sec` `min` `hr` `day` `wk` - supports arithmetic: `5sec + 2min` + +**Filesize (base-10)**: `b` `kb` `mb` `gb` `tb` `pb` `eb` +**Filesize (base-2)**: `kib` `mib` `gib` `tib` `pib` `eib` - supports arithmetic: `1gb + 500mb` + +## Ranges + +```nu +1..5 # inclusive: [1, 2, 3, 4, 5] +0..<5 # exclusive end: [0, 1, 2, 3, 4] +5..1 # reverse: [5, 4, 3, 2, 1] +0..2..10 # step by 2: [0, 2, 4, 6, 8, 10] +``` + +## Variables + +```nu +let x = 42 # immutable (preferred) +mut y = 0 # mutable (cannot be captured by closures) +const FILE = "a.nu" # parse-time constant (use with source/use) +``` + +**Closures cannot capture `mut` variables.** Use `reduce`, `generate`, or `each` instead. + +## Operators + +### Arithmetic +`+` `-` `*` `/` `//` (floor div) `mod` `**` (power) + +### Comparison +`==` `!=` `<` `<=` `>` `>=` + +### String/Pattern +`=~` or `like` (regex match), `!~` or `not-like` (regex non-match) +`starts-with` `ends-with` `in` `not-in` `has` `not-has` + +### Logical +`not` `and` `or` `xor` (all short-circuit except xor) + +### Bitwise +`bit-and` `bit-or` `bit-xor` `bit-shl` `bit-shr` + +### Assignment +`=` `+=` `-=` `*=` `/=` `++=` (append) + +### Concatenation +`++` (append lists or strings) + +### Precedence (high to low) +1. Parentheses +2. `**` +3. `*` `/` `//` `mod` +4. `+` `-` +5. `bit-shl` `bit-shr` +6. Comparison, membership, regex +7. `bit-and` -> `bit-xor` -> `bit-or` +8. `and` -> `xor` -> `or` +9. Assignment +10. `not` + +## Control Flow + +```nu +# if/else (returns value) +let size = if $x > 100 { "big" } else if $x > 10 { "medium" } else { "small" } + +# match (pattern matching) +match $val { + 1..10 => "small" + "hello" => "greeting" + {name: $n} => $"found ($n)" # record destructuring + $x if $x > 100 => "big" # guard clause + _ => "other" # default +} + +# loops (prefer each/par-each for functional style) +for item in $list { ... } +while $cond { ... } +loop { if $done { break }; ... } +``` + +## Accessing Structured Data + +```nu +$record.field # record field +$list.2 # list index +$table | get column # column values as list +$table | select col1 col2 # subtable with columns +$data.users.0.name # nested access +$data.field? # optional (returns null if missing) +$data.field? | default "fallback" # with default +``` + +**`get` vs `select`**: `get name` returns a flat list of values. `select name` returns a table preserving column structure. + +## Records + +```nu +{a: 1, b: 2} | insert c 3 # add field +{a: 1, b: 2} | update a 10 # modify field +{a: 1, b: 2} | merge {c: 3} # combine records +{...$rec1, ...$rec2, d: 4} # spread syntax +{a: 1, b: 2} | transpose k v # to table +{a: 1, b: 2} | items {|k,v| $"($k)=($v)"} # iterate +{a: 1, b: 2} | columns # ["a", "b"] +{a: 1, b: 2} | values # [1, 2] +``` + +## Lists + +```nu +[1 2 3] | append 4 # add to end +[1 2 3] | prepend 0 # add to start +[1 2 3] ++ [4 5] # concatenate +[1 2 3] | insert 1 99 # insert at index +[1 2 3] | each {|x| $x * 2} # transform +[1 2 3] | par-each {|x| $x * 2} # parallel transform +[1 2 3] | where $it > 1 # filter +[1 2 3] | reduce {|it, acc| $acc + $it} # fold +[1 2 3] | any {|x| $x > 2} # test any +[1 2 3] | all {|x| $x > 0} # test all +[1 [2 3] [4]] | flatten # unnest +``` + +## Tables + +```nu +$table | sort-by column_name +$table | group-by column --to-table +$table | rename new_col1 new_col2 +$table | reject unwanted_col +$table | move col --after other_col +$table | enumerate | flatten # add index column +``` + +## Special Variables + +| Variable | Purpose | +|----------|---------| +| `$in` | Pipeline input to current expression | +| `$it` | Current item in `where` row conditions | +| `$env` | Environment variables and config | +| `$nu` | System info, paths, runtime (`$nu.home-dir`, `$nu.default-config-dir`, `$nu.os-info`, `$nu.pid`) | +| `$env.config` | Nushell configuration record | +| `$env.LAST_EXIT_CODE` | Last external command exit code | +| `$env.CMD_DURATION_MS` | Last command duration in ms | +| `$env.CURRENT_FILE` | Current script/module file path | +| `$env.PATH` / `$env.Path` | Executable search path (macOS-Linux / Windows); list with `ENV_CONVERSIONS`, string otherwise | From ff2feb3ddc5c5bc6a88b59fde26ad64b6d0066c9 Mon Sep 17 00:00:00 2001 From: 0x4D5352 <67082011+0x4D5352@users.noreply.github.com> Date: Thu, 5 Mar 2026 22:25:32 -0600 Subject: [PATCH 2/2] fix: move skills/ to project root and updat README.md with GH Copilot --- .agents/README.md | 12 --------- skills/README.md | 27 +++++++++++++++++++ {.agents/skills => skills}/nushell/SKILL.md | 0 .../nushell/references/advanced.md | 0 .../nushell/references/bash-equivalents.md | 0 .../nushell/references/commands.md | 0 .../nushell/references/data-analysis.md | 0 .../nushell/references/http-transport.md | 0 .../nushell/references/types-and-syntax.md | 0 9 files changed, 27 insertions(+), 12 deletions(-) delete mode 100644 .agents/README.md create mode 100644 skills/README.md rename {.agents/skills => skills}/nushell/SKILL.md (100%) rename {.agents/skills => skills}/nushell/references/advanced.md (100%) rename {.agents/skills => skills}/nushell/references/bash-equivalents.md (100%) rename {.agents/skills => skills}/nushell/references/commands.md (100%) rename {.agents/skills => skills}/nushell/references/data-analysis.md (100%) rename {.agents/skills => skills}/nushell/references/http-transport.md (100%) rename {.agents/skills => skills}/nushell/references/types-and-syntax.md (100%) diff --git a/.agents/README.md b/.agents/README.md deleted file mode 100644 index dd325933..00000000 --- a/.agents/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# AGENTS directory - -This directory holds examples for use with coding agents that interact with Nu, -either as a CLI tool or via the Nushell MCP server. - -## Skills - -`./skills/nushell/` - An [Agent Skills](https://agentskills.io/home) compliant -skill directory built via the Claude Code skill-writer superpower and human-tested -for validity and efficacy. Copy this directory to `~/.claude/skills/` for global -use in Claude Code, or to `~/.agent/skills/` for most non-Claude Code agentic -tools. See your preferred LLM provider's documentation for specific details. diff --git a/skills/README.md b/skills/README.md new file mode 100644 index 00000000..69b72825 --- /dev/null +++ b/skills/README.md @@ -0,0 +1,27 @@ +# Agent Skills + +This directory holds examples for use with coding agents that interact with Nu, +either as a CLI tool or via the Nushell MCP server. + +## Nushell Skill + +`./skills/nushell/` - An [Agent Skills](https://agentskills.io/home) compliant +skill directory built via the Claude Code skill-writer superpower and human-tested +for validity and efficacy. Once added to the appropriate location, begin a new +session with your coding agent and it should automatically register the skill +and use it when you reference Nushell, mention the skill directly, or trigger a +use case mentioned in the description of the SKILL.md frontmatter. + +### Global usage + +Copy the `nushell/` directory to `~/.claude/skills/` for global use in Claude Code, +to `~/.copilot/skills` for global use with the Copilot coding agent or GitHub +Copilot CLI, or to `~/.agent/skills/` for Codex, OpenCode, and other coding agents. +See your preferred LLM provider's documentation for specific details. + +### Project usage + +If you only wish to load this skill for specific projects, or for use with non- +CLI based coding agents that canot access a global skills directory, create a +`.claude/skills/`, `.github/skills/`, or `.agents/skills/` directory at the root +of your project and copy the nushell skill directory into it. diff --git a/.agents/skills/nushell/SKILL.md b/skills/nushell/SKILL.md similarity index 100% rename from .agents/skills/nushell/SKILL.md rename to skills/nushell/SKILL.md diff --git a/.agents/skills/nushell/references/advanced.md b/skills/nushell/references/advanced.md similarity index 100% rename from .agents/skills/nushell/references/advanced.md rename to skills/nushell/references/advanced.md diff --git a/.agents/skills/nushell/references/bash-equivalents.md b/skills/nushell/references/bash-equivalents.md similarity index 100% rename from .agents/skills/nushell/references/bash-equivalents.md rename to skills/nushell/references/bash-equivalents.md diff --git a/.agents/skills/nushell/references/commands.md b/skills/nushell/references/commands.md similarity index 100% rename from .agents/skills/nushell/references/commands.md rename to skills/nushell/references/commands.md diff --git a/.agents/skills/nushell/references/data-analysis.md b/skills/nushell/references/data-analysis.md similarity index 100% rename from .agents/skills/nushell/references/data-analysis.md rename to skills/nushell/references/data-analysis.md diff --git a/.agents/skills/nushell/references/http-transport.md b/skills/nushell/references/http-transport.md similarity index 100% rename from .agents/skills/nushell/references/http-transport.md rename to skills/nushell/references/http-transport.md diff --git a/.agents/skills/nushell/references/types-and-syntax.md b/skills/nushell/references/types-and-syntax.md similarity index 100% rename from .agents/skills/nushell/references/types-and-syntax.md rename to skills/nushell/references/types-and-syntax.md