diff --git a/.github/workflows/js.yml b/.github/workflows/js.yml index 90d135cd..0b0202b7 100644 --- a/.github/workflows/js.yml +++ b/.github/workflows/js.yml @@ -148,6 +148,7 @@ jobs: ${{ matrix.run }} bash_basics.mjs ${{ matrix.run }} data_pipeline.mjs ${{ matrix.run }} llm_tool.mjs + ${{ matrix.run }} k8s_orchestrator.mjs ${{ matrix.run }} langchain_integration.mjs - name: Install Doppler CLI diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 1198f65c..8ac39bce 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -126,6 +126,8 @@ jobs: run: | python crates/bashkit-python/examples/bash_basics.py python crates/bashkit-python/examples/k8s_orchestrator.py + python crates/bashkit-python/examples/data_pipeline.py + python crates/bashkit-python/examples/llm_tool.py python crates/bashkit-python/examples/langgraph_async_tool.py python crates/bashkit-python/examples/fastapi_async_tool.py diff --git a/crates/bashkit-python/examples/data_pipeline.py b/crates/bashkit-python/examples/data_pipeline.py new file mode 100644 index 00000000..22a186e8 --- /dev/null +++ b/crates/bashkit-python/examples/data_pipeline.py @@ -0,0 +1,306 @@ +#!/usr/bin/env -S uv run --script +# /// script +# requires-python = ">=3.9" +# dependencies = [ +# "bashkit", +# ] +# /// +"""Data processing pipeline using bashkit. + +Demonstrates real-world data tasks: CSV processing with bash pipelines +(sort, uniq, awk), JSON transformation with jq, log analysis, and report +generation — all in a sandboxed virtual filesystem. + +Run: + uv run crates/bashkit-python/examples/data_pipeline.py + +uv automatically installs bashkit from PyPI (pre-built wheels, no Rust needed). +""" + +from __future__ import annotations + +import asyncio + +from bashkit import Bash + + +def demo_csv_processing(): + """Process CSV data with bash pipelines.""" + print("=== CSV Processing ===\n") + + bash = Bash() + + # Create sample sales data in the VFS + bash.execute_sync("""cat > /tmp/sales.csv << 'EOF' +date,product,quantity,price +2024-01-01,Widget A,10,29.99 +2024-01-01,Widget B,5,49.99 +2024-01-02,Widget A,8,29.99 +2024-01-02,Widget C,12,19.99 +2024-01-03,Widget B,3,49.99 +2024-01-03,Widget A,15,29.99 +2024-01-03,Widget C,7,19.99 +EOF""") + + # Total quantity per product using awk + sort + r = bash.execute_sync( + "tail -n +2 /tmp/sales.csv | awk -F, '{sum[$2]+=$3} END {for (p in sum) print p\": \"sum[p]}' | sort" + ) + print("Quantity by product:") + print(r.stdout) + + # Top selling day by total quantity + r = bash.execute_sync( + "tail -n +2 /tmp/sales.csv" + " | awk -F, '{sum[$1]+=$3} END {for (d in sum) print sum[d]\" \"d}'" + " | sort -rn | head -1" + ) + print(f"Top day: {r.stdout.strip()}") + + # Revenue calculation + r = bash.execute_sync("tail -n +2 /tmp/sales.csv | awk -F, '{rev+=$3*$4} END {printf \"$%.2f\\n\", rev}'") + print(f"Total revenue: {r.stdout.strip()}") + + # Unique products via sort | uniq + r = bash.execute_sync("tail -n +2 /tmp/sales.csv | cut -d, -f2 | sort | uniq") + print(f"Products: {', '.join(r.stdout.strip().split(chr(10)))}") + + print() + + +def demo_json_transformation(): + """Transform JSON data with jq pipelines.""" + print("=== JSON Transformation ===\n") + + bash = Bash() + + # Create API-like JSON response + bash.execute_sync("""cat > /tmp/api_response.json << 'EOF' +{ + "users": [ + {"id": 1, "name": "Alice", "email": "alice@example.com", "active": true, "role": "admin"}, + {"id": 2, "name": "Bob", "email": "bob@example.com", "active": false, "role": "user"}, + {"id": 3, "name": "Carol", "email": "carol@example.com", "active": true, "role": "user"}, + {"id": 4, "name": "Dave", "email": "dave@example.com", "active": true, "role": "admin"}, + {"id": 5, "name": "Eve", "email": "eve@example.com", "active": false, "role": "user"} + ] +} +EOF""") + + # Active admins + r = bash.execute_sync( + "cat /tmp/api_response.json | jq '[.users[] | select(.active and .role == \"admin\")] | length'" + ) + print(f"Active admins: {r.stdout.strip()}") + assert r.stdout.strip() == "2" + + # Build summary object with jq + r = bash.execute_sync(""" + cat /tmp/api_response.json | jq '{ + total: (.users | length), + active: ([.users[] | select(.active)] | length), + inactive: ([.users[] | select(.active | not)] | length), + admins: [.users[] | select(.role == "admin") | .name], + emails: [.users[] | select(.active) | .email] + }' + """) + import json + + summary = json.loads(r.stdout) + print(f"Total: {summary['total']}, Active: {summary['active']}, Inactive: {summary['inactive']}") + print(f"Admins: {', '.join(summary['admins'])}") + assert summary["total"] == 5 + assert summary["active"] == 3 + + print() + + +def demo_log_analysis(): + """Analyze log files with grep, awk, sort, uniq.""" + print("=== Log Analysis ===\n") + + bash = Bash() + + # Create sample access log + bash.execute_sync("""cat > /tmp/access.log << 'EOF' +2024-01-15T10:00:01 GET /api/users 200 45ms +2024-01-15T10:00:02 POST /api/users 201 120ms +2024-01-15T10:00:03 GET /api/users/1 200 30ms +2024-01-15T10:00:04 GET /api/health 200 5ms +2024-01-15T10:00:05 POST /api/login 401 15ms +2024-01-15T10:00:06 GET /api/users 200 42ms +2024-01-15T10:00:07 DELETE /api/users/3 403 10ms +2024-01-15T10:00:08 GET /api/products 500 200ms +2024-01-15T10:00:09 GET /api/users 200 38ms +2024-01-15T10:00:10 POST /api/login 200 95ms +EOF""") + + # Request counts by status code + r = bash.execute_sync("awk '{print $4}' /tmp/access.log | sort | uniq -c | sort -rn") + print("Requests by status:") + print(r.stdout) + + # Error requests (4xx/5xx) + r = bash.execute_sync("awk '$4 >= 400 {print $1, $2, $3, $4}' /tmp/access.log") + print("Errors:") + print(r.stdout) + + # Average response time + r = bash.execute_sync('awk \'{gsub(/ms/,"",$5); sum+=$5; n++} END {printf "%.1fms\\n", sum/n}\' /tmp/access.log') + print(f"Avg response time: {r.stdout.strip()}") + + # Most hit endpoints + r = bash.execute_sync("awk '{print $3}' /tmp/access.log | sort | uniq -c | sort -rn | head -3") + print("Top endpoints:") + print(r.stdout) + + +def demo_vfs_intermediate_files(): + """Demonstrate VFS for intermediate pipeline files.""" + print("=== VFS Intermediate Files ===\n") + + bash = Bash() + + # Stage 1: Generate raw data + bash.execute_sync(""" + mkdir -p /pipeline/stage1 + for i in $(seq 1 20); do + echo "$((RANDOM % 100)),$((RANDOM % 5 + 1))" >> /pipeline/stage1/raw.csv + done + """) + + # Stage 2: Filter and sort + bash.execute_sync(""" + mkdir -p /pipeline/stage2 + sort -t, -k1 -n /pipeline/stage1/raw.csv > /pipeline/stage2/sorted.csv + awk -F, '$1 >= 50' /pipeline/stage2/sorted.csv > /pipeline/stage2/filtered.csv + """) + + # Stage 3: Aggregate + bash.execute_sync(""" + mkdir -p /pipeline/stage3 + awk -F, '{sum+=$1; count+=$2; n++} END { + printf "records=%d\\ntotal=%d\\navg=%.1f\\nweight=%d\\n", n, sum, sum/n, count + }' /pipeline/stage2/filtered.csv > /pipeline/stage3/summary.txt + """) + + # Read final result + r = bash.execute_sync("cat /pipeline/stage3/summary.txt") + print("Pipeline summary:") + print(r.stdout) + + # Verify VFS state — all intermediate files exist + r = bash.execute_sync("find /pipeline -type f | sort") + print("VFS files:") + print(r.stdout) + + # Use FileSystem API directly + fs = bash.fs() + assert fs.exists("/pipeline/stage3/summary.txt") + content = fs.read_file("/pipeline/stage3/summary.txt").decode() + assert "records=" in content + print(f"Direct VFS read: {content.splitlines()[0]}") + + print() + + +async def demo_async_pipeline(): + """Async execution of a multi-step pipeline.""" + print("=== Async Pipeline ===\n") + + bash = Bash() + + # Step 1: Create data asynchronously + await bash.execute(""" + cat > /tmp/inventory.json << 'EOF' +[ + {"item": "laptop", "qty": 15, "price": 999.99}, + {"item": "mouse", "qty": 200, "price": 24.99}, + {"item": "keyboard", "qty": 85, "price": 74.99}, + {"item": "monitor", "qty": 30, "price": 449.99}, + {"item": "cable", "qty": 500, "price": 9.99} +] +EOF + """) + + # Step 2: Compute total value per item + r = await bash.execute(""" + cat /tmp/inventory.json \ + | jq -r '.[] | "\\(.item),\\(.qty),\\(.price),\\(.qty * .price)"' \ + | sort -t, -k4 -rn + """) + print("Inventory by total value (desc):") + print(r.stdout) + + # Step 3: Summary + r = await bash.execute(""" + cat /tmp/inventory.json | jq '{ + total_items: ([.[].qty] | add), + total_value: ([.[] | .qty * .price] | add | . * 100 | floor / 100), + most_expensive: (sort_by(-.price) | first | .item), + most_stocked: (sort_by(-.qty) | first | .item) + }' + """) + print("Summary:") + print(r.stdout) + assert r.success + + print() + + +def demo_report_generation(): + """Generate a markdown report from data.""" + print("=== Report Generation ===\n") + + bash = Bash() + + r = bash.execute_sync(""" + # Gather data + echo -e "alice,95\\nbob,82\\ncarol,91\\ndave,78\\neve,88" > /tmp/scores.csv + + # Build report + cat << 'HEADER' +# Student Report + +| Student | Score | Grade | +|---------|-------|-------| +HEADER + + while IFS=, read name score; do + if [ "$score" -ge 90 ]; then grade="A" + elif [ "$score" -ge 80 ]; then grade="B" + elif [ "$score" -ge 70 ]; then grade="C" + else grade="F" + fi + echo "| $name | $score | $grade |" + done < /tmp/scores.csv + + echo "" + AVG=$(awk -F, '{sum+=$2; n++} END {printf "%.1f", sum/n}' /tmp/scores.csv) + TOP=$(sort -t, -k2 -rn /tmp/scores.csv | head -1 | cut -d, -f1) + echo "**Average:** $AVG" + echo "**Top student:** $TOP" + """) + print(r.stdout) + assert "alice" in r.stdout + assert "Average:" in r.stdout + + +# ============================================================================= +# Main +# ============================================================================= + + +def main(): + print("Bashkit — Data Pipeline Examples\n") + demo_csv_processing() + demo_json_transformation() + demo_log_analysis() + demo_vfs_intermediate_files() + asyncio.run(demo_async_pipeline()) + demo_report_generation() + print("All examples passed.") + + +if __name__ == "__main__": + main() diff --git a/crates/bashkit-python/examples/llm_tool.py b/crates/bashkit-python/examples/llm_tool.py new file mode 100644 index 00000000..07bbb0f4 --- /dev/null +++ b/crates/bashkit-python/examples/llm_tool.py @@ -0,0 +1,216 @@ +#!/usr/bin/env -S uv run --script +# /// script +# requires-python = ">=3.9" +# dependencies = [ +# "bashkit", +# ] +# /// +"""BashTool as an LLM tool — shows how to wire bashkit into any AI framework. + +Demonstrates: +- Creating a BashTool instance +- Extracting tool metadata (description, input/output schemas) +- Simulating an LLM tool-call loop (no API key needed) +- Feeding results back as tool responses +- Using system_prompt() for LLM context +- Generic tool adapter pattern + +Run: + uv run crates/bashkit-python/examples/llm_tool.py + +uv automatically installs bashkit from PyPI (pre-built wheels, no Rust needed). +""" + +from __future__ import annotations + +import json + +from bashkit import BashTool + + +def demo_tool_definition(): + """Show how to extract tool metadata for any LLM framework.""" + print("=== Tool Definition ===\n") + + tool = BashTool(username="agent", hostname="sandbox") + + # These fields are what you'd send to any LLM as a tool definition + tool_def = { + "name": tool.name, + "description": tool.short_description, + "input_schema": json.loads(tool.input_schema()), + "output_schema": json.loads(tool.output_schema()), + } + + print(f"Name: {tool_def['name']}") + print(f"Description: {tool_def['description']}") + input_props = tool_def["input_schema"].get("properties", {}) + print(f"Input keys: {', '.join(input_props.keys())}") + print(f"Version: {tool.version}") + assert tool_def["name"] == "bashkit" + assert "commands" in input_props + + print() + + +def demo_system_prompt(): + """Show the token-efficient system prompt for LLM orchestration.""" + print("=== System Prompt (first 200 chars) ===\n") + + tool = BashTool() + prompt = tool.system_prompt() + print(prompt[:200] + "...\n") + + # The system prompt contains instructions for the LLM + assert len(prompt) > 100 + + # help() returns a longer markdown document + help_text = tool.help() + print(f"Help length: {len(help_text)} chars (vs system_prompt: {len(prompt)} chars)") + assert len(help_text) > len(prompt) + + print() + + +def demo_tool_call_loop(): + """Simulate an LLM tool-call loop — the core pattern for any framework.""" + print("=== Simulated Tool-Call Loop ===\n") + + tool = BashTool(username="agent", hostname="sandbox") + + # Pretend the LLM decided to call our tool with these commands + llm_tool_calls = [ + 'echo "Setting up project..."', + "mkdir -p /tmp/project/src", + "echo 'def main(): print(\"hello\")' > /tmp/project/src/app.py", + "cat /tmp/project/src/app.py", + "ls -la /tmp/project/src/", + "wc -l /tmp/project/src/app.py", + ] + + for commands in llm_tool_calls: + print(f"LLM calls: {commands}") + result = tool.execute_sync(commands) + + # Build the response you'd send back to the LLM + tool_response = { # noqa: F841 (illustrative — shows the shape) + "stdout": result.stdout, + "stderr": result.stderr, + "exit_code": result.exit_code, + } + + if result.exit_code == 0: + print(f" -> stdout: {result.stdout.strip() or '(empty)'}") + else: + print(f" -> error ({result.exit_code}): {result.stderr.strip()}") + print() + + # Verify state persisted across calls + r = tool.execute_sync("test -f /tmp/project/src/app.py && echo exists") + assert r.stdout.strip() == "exists" + + print() + + +def demo_error_handling(): + """Show how errors are reported back to the LLM.""" + print("=== Error Handling ===\n") + + tool = BashTool() + + # Non-zero exit + r = tool.execute_sync("exit 42") + print(f"exit 42: code={r.exit_code}, success={r.success}") + assert r.exit_code == 42 + assert not r.success + + # Command not found + r = tool.execute_sync("nonexistent_command") + print(f"not found: code={r.exit_code}, stderr={r.stderr.strip()!r}") + assert r.exit_code != 0 + + # The tool keeps working after errors + r = tool.execute_sync("echo 'recovered'") + print(f"recovered: {r.stdout.strip()}") + assert r.success + + print() + + +def demo_generic_adapter(): + """Create a generic tool adapter that works with any LLM framework.""" + print("=== Generic Tool Adapter ===\n") + + tool = BashTool() + + # This function converts BashTool into a format any framework can use + adapter = create_tool_adapter(tool) + print(f"Adapter name: {adapter['name']}") + schema_str = json.dumps(adapter["schema"]) + print(f"Adapter schema: {schema_str[:80]}...") + + # Execute through adapter + result = adapter["execute"]({"commands": "echo 'via adapter'"}) + print(f"Adapter result: {result['stdout'].strip()}") + assert result["exit_code"] == 0 + + # Anthropic tool-use format + anthropic_tool = { + "name": adapter["name"], + "description": adapter["description"], + "input_schema": adapter["schema"], + } + print(f"\nAnthropic tool format: name={anthropic_tool['name']!r}") + print(f" input_schema keys: {list(anthropic_tool['input_schema'].get('properties', {}).keys())}") + + # OpenAI function-calling format + openai_tool = { + "type": "function", + "function": { + "name": adapter["name"], + "description": adapter["description"], + "parameters": adapter["schema"], + }, + } + print(f"OpenAI tool format: type={openai_tool['type']!r}, function.name={openai_tool['function']['name']!r}") + + print() + + +def create_tool_adapter(bash_tool: BashTool) -> dict: + """Create a generic tool adapter from BashTool. + + Returns a dict with: + - name, description, schema (for tool registration) + - execute(params) (for tool invocation) + """ + + def execute(params: dict) -> dict: + r = bash_tool.execute_sync(params.get("commands", "")) + return {"stdout": r.stdout, "stderr": r.stderr, "exit_code": r.exit_code} + + return { + "name": bash_tool.name, + "description": bash_tool.description(), + "schema": json.loads(bash_tool.input_schema()), + "execute": execute, + } + + +# ============================================================================= +# Main +# ============================================================================= + + +def main(): + print("Bashkit — LLM Tool Examples\n") + demo_tool_definition() + demo_system_prompt() + demo_tool_call_loop() + demo_error_handling() + demo_generic_adapter() + print("All examples passed.") + + +if __name__ == "__main__": + main() diff --git a/examples/k8s_orchestrator.mjs b/examples/k8s_orchestrator.mjs new file mode 100644 index 00000000..ca6fea5d --- /dev/null +++ b/examples/k8s_orchestrator.mjs @@ -0,0 +1,392 @@ +#!/usr/bin/env node +/** + * Kubernetes API orchestrator using bashkit ScriptedTool. + * + * Demonstrates composing 12 mock K8s API tools into a single ScriptedTool that + * an LLM agent can call with bash scripts. Each tool becomes a bash builtin; + * the agent writes one script to orchestrate them all. + * + * Mirrors the Python k8s_orchestrator.py example. + * + * Run: + * node examples/k8s_orchestrator.mjs + */ + +import { ScriptedTool } from "@everruns/bashkit"; + +// ============================================================================= +// Fake k8s data +// ============================================================================= + +const NODES = [ + { name: "node-1", status: "Ready", cpu: "4", memory: "16Gi", pods: 23 }, + { name: "node-2", status: "Ready", cpu: "8", memory: "32Gi", pods: 41 }, + { name: "node-3", status: "NotReady", cpu: "4", memory: "16Gi", pods: 0 }, +]; + +const NAMESPACES = [ + { name: "default", status: "Active" }, + { name: "kube-system", status: "Active" }, + { name: "monitoring", status: "Active" }, + { name: "production", status: "Active" }, +]; + +const PODS = { + default: [ + { name: "web-abc12", status: "Running", restarts: 0, node: "node-1", image: "nginx:1.25" }, + { name: "api-def34", status: "Running", restarts: 2, node: "node-2", image: "api:v2.1" }, + { name: "worker-ghi56", status: "CrashLoopBackOff", restarts: 15, node: "node-2", image: "worker:v1.0" }, + ], + "kube-system": [ + { name: "coredns-aaa11", status: "Running", restarts: 0, node: "node-1", image: "coredns:1.11" }, + { name: "etcd-bbb22", status: "Running", restarts: 0, node: "node-1", image: "etcd:3.5" }, + ], + monitoring: [ + { name: "prometheus-ccc33", status: "Running", restarts: 0, node: "node-2", image: "prom:2.48" }, + { name: "grafana-ddd44", status: "Running", restarts: 1, node: "node-2", image: "grafana:10.2" }, + ], + production: [ + { name: "app-eee55", status: "Running", restarts: 0, node: "node-1", image: "app:v3.2" }, + { name: "app-fff66", status: "Running", restarts: 0, node: "node-2", image: "app:v3.2" }, + { name: "db-ggg77", status: "Pending", restarts: 0, node: "", image: "postgres:16" }, + ], +}; + +const DEPLOYMENTS = { + default: [ + { name: "web", replicas: 1, available: 1, image: "nginx:1.25" }, + { name: "api", replicas: 2, available: 2, image: "api:v2.1" }, + { name: "worker", replicas: 1, available: 0, image: "worker:v1.0" }, + ], + production: [ + { name: "app", replicas: 2, available: 2, image: "app:v3.2" }, + { name: "db", replicas: 1, available: 0, image: "postgres:16" }, + ], +}; + +const SERVICES = { + default: [ + { name: "web-svc", type: "LoadBalancer", clusterIP: "10.0.0.10", ports: "80/TCP" }, + { name: "api-svc", type: "ClusterIP", clusterIP: "10.0.0.20", ports: "8080/TCP" }, + ], + production: [ + { name: "app-svc", type: "LoadBalancer", clusterIP: "10.0.1.10", ports: "443/TCP" }, + ], +}; + +const CONFIGMAPS = { + default: [{ name: "app-config", data_keys: ["DATABASE_URL", "LOG_LEVEL", "CACHE_TTL"] }], + production: [{ name: "prod-config", data_keys: ["DATABASE_URL", "REDIS_URL"] }], +}; + +const EVENTS = [ + { namespace: "default", type: "Warning", reason: "BackOff", object: "pod/worker-ghi56", message: "Back-off restarting failed container" }, + { namespace: "production", type: "Warning", reason: "FailedScheduling", object: "pod/db-ggg77", message: "Insufficient memory on available nodes" }, + { namespace: "default", type: "Normal", reason: "Pulled", object: "pod/api-def34", message: "Successfully pulled image api:v2.1" }, + { namespace: "monitoring", type: "Normal", reason: "Started", object: "pod/prometheus-ccc33", message: "Started container prometheus" }, +]; + +const LOGS = { + "web-abc12": "2024-01-15T10:00:01Z GET /health 200 1ms\n2024-01-15T10:00:02Z GET /api/users 200 45ms\n", + "api-def34": "2024-01-15T10:00:01Z INFO Starting API server on :8080\n2024-01-15T10:00:02Z WARN High latency detected: 250ms\n", + "worker-ghi56": "2024-01-15T10:00:01Z ERROR Connection refused: redis://redis:6379\n2024-01-15T10:00:02Z FATAL Exiting due to unrecoverable error\n", +}; + +// Track mutable state for scale operations +const deploymentState = {}; + +// ============================================================================= +// Tool callbacks — each receives (params, stdin) => string +// ============================================================================= + +function getNodes() { + return JSON.stringify({ items: NODES }) + "\n"; +} + +function getNamespaces() { + return JSON.stringify({ items: NAMESPACES }) + "\n"; +} + +function getPods(params) { + const ns = params.namespace || "default"; + return JSON.stringify({ items: PODS[ns] || [] }) + "\n"; +} + +function getDeployments(params) { + const ns = params.namespace || "default"; + return JSON.stringify({ items: DEPLOYMENTS[ns] || [] }) + "\n"; +} + +function getServices(params) { + const ns = params.namespace || "default"; + return JSON.stringify({ items: SERVICES[ns] || [] }) + "\n"; +} + +function describePod(params) { + const name = params.name || ""; + const ns = params.namespace || "default"; + for (const pod of PODS[ns] || []) { + if (pod.name === name) { + const detail = { ...pod, namespace: ns, labels: { app: name.split("-").slice(0, -1).join("-") } }; + return JSON.stringify(detail) + "\n"; + } + } + throw new Error(`pod ${name} not found in ${ns}`); +} + +function getLogs(params) { + const name = params.name || ""; + const tail = params.tail || 50; + const logs = LOGS[name] || `No logs available for ${name}\n`; + const lines = logs.trim().split("\n"); + return lines.slice(-tail).join("\n") + "\n"; +} + +function getConfigmaps(params) { + const ns = params.namespace || "default"; + return JSON.stringify({ items: CONFIGMAPS[ns] || [] }) + "\n"; +} + +function getSecrets(params) { + const ns = params.namespace || "default"; + const secrets = [{ name: `${ns}-tls`, type: "kubernetes.io/tls", data: "***REDACTED***" }]; + return JSON.stringify({ items: secrets }) + "\n"; +} + +function getEvents(params) { + const ns = params.namespace; + const items = ns ? EVENTS.filter((e) => e.namespace === ns) : EVENTS; + return JSON.stringify({ items }) + "\n"; +} + +function scaleDeployment(params) { + const name = params.name || ""; + const ns = params.namespace || "default"; + const replicas = Number(params.replicas) || 1; + deploymentState[`${ns}/${name}`] = { replicas }; + return JSON.stringify({ deployment: name, namespace: ns, replicas, status: "scaling" }) + "\n"; +} + +function rolloutStatus(params) { + const name = params.name || ""; + const ns = params.namespace || "default"; + const key = `${ns}/${name}`; + if (deploymentState[key]) { + const r = deploymentState[key].replicas; + return JSON.stringify({ deployment: name, status: "progressing", replicas: r, updated: r }) + "\n"; + } + for (const dep of DEPLOYMENTS[ns] || []) { + if (dep.name === name) { + const status = dep.available === dep.replicas ? "available" : "progressing"; + return JSON.stringify({ deployment: name, status, ...dep }) + "\n"; + } + } + throw new Error(`deployment ${name} not found in ${ns}`); +} + +// ============================================================================= +// Build the ScriptedTool with all 12 k8s commands +// ============================================================================= + +function buildK8sTool() { + const tool = new ScriptedTool({ name: "kubectl", shortDescription: "Kubernetes cluster management API" }); + + tool.addTool("get_nodes", "List cluster nodes", getNodes); + + tool.addTool("get_namespaces", "List namespaces", getNamespaces); + + tool.addTool("get_pods", "List pods in a namespace", getPods, { + type: "object", + properties: { namespace: { type: "string", description: "Namespace" } }, + }); + + tool.addTool("get_deployments", "List deployments in a namespace", getDeployments, { + type: "object", + properties: { namespace: { type: "string", description: "Namespace" } }, + }); + + tool.addTool("get_services", "List services in a namespace", getServices, { + type: "object", + properties: { namespace: { type: "string", description: "Namespace" } }, + }); + + tool.addTool("describe_pod", "Describe a specific pod", describePod, { + type: "object", + properties: { + name: { type: "string", description: "Pod name" }, + namespace: { type: "string", description: "Namespace" }, + }, + required: ["name"], + }); + + tool.addTool("get_logs", "Get pod logs", getLogs, { + type: "object", + properties: { + name: { type: "string", description: "Pod name" }, + tail: { type: "integer", description: "Number of lines" }, + }, + required: ["name"], + }); + + tool.addTool("get_configmaps", "List configmaps in a namespace", getConfigmaps, { + type: "object", + properties: { namespace: { type: "string", description: "Namespace" } }, + }); + + tool.addTool("get_secrets", "List secrets in a namespace (values redacted)", getSecrets, { + type: "object", + properties: { namespace: { type: "string", description: "Namespace" } }, + }); + + tool.addTool("get_events", "Get cluster events", getEvents, { + type: "object", + properties: { namespace: { type: "string", description: "Filter namespace" } }, + }); + + tool.addTool("scale_deployment", "Scale a deployment to N replicas", scaleDeployment, { + type: "object", + properties: { + name: { type: "string", description: "Deployment name" }, + namespace: { type: "string", description: "Namespace" }, + replicas: { type: "integer", description: "Target replica count" }, + }, + required: ["name", "replicas"], + }); + + tool.addTool("rollout_status", "Check deployment rollout status", rolloutStatus, { + type: "object", + properties: { + name: { type: "string", description: "Deployment name" }, + namespace: { type: "string", description: "Namespace" }, + }, + required: ["name"], + }); + + tool.env("KUBECONFIG", "/etc/kubernetes/admin.conf"); + return tool; +} + +// ============================================================================= +// Demo scripts — what an LLM agent would generate +// ============================================================================= + +async function runDemos(tool) { + console.log("=".repeat(70)); + console.log("Kubernetes Orchestrator - 12 tools via ScriptedTool"); + console.log("=".repeat(70)); + + // -- Demo 1: Simple listing -- + console.log("\n--- Demo 1: List all nodes ---"); + let r = await tool.execute( + "get_nodes | jq -r '.items[] | \"\\(.name) \\(.status) cpu=\\(.cpu) mem=\\(.memory)\"'" + ); + console.log(r.stdout); + + // -- Demo 2: Unhealthy pods across all namespaces -- + console.log("--- Demo 2: Find unhealthy pods across namespaces ---"); + r = await tool.execute(` + get_namespaces | jq -r '.items[].name' | while read ns; do + get_pods --namespace "$ns" \ + | jq -r '.items[] | select(.status != "Running") | .name + " " + .status' \ + | while read line; do echo " $ns/$line"; done + done + `); + console.log(r.stdout); + + // -- Demo 3: Cluster health report -- + console.log("--- Demo 3: Full cluster health report ---"); + r = await tool.execute(` + echo "=== Cluster Health Report ===" + + # Node status + echo "" + echo "-- Nodes --" + nodes=$(get_nodes) + total=$(echo "$nodes" | jq '.items | length') + ready=$(echo "$nodes" | jq '[.items[] | select(.status == "Ready")] | length') + echo "Nodes: $ready/$total ready" + + # Pod status per namespace + echo "" + echo "-- Pods --" + get_namespaces | jq -r '.items[].name' | while read ns; do + pods=$(get_pods --namespace "$ns") + total=$(echo "$pods" | jq '.items | length') + running=$(echo "$pods" | jq '[.items[] | select(.status == "Running")] | length') + echo " $ns: $running/$total running" + done + + # Warnings + echo "" + echo "-- Recent warnings --" + get_events | jq -r '.items[] | select(.type == "Warning") | " [\\(.reason)] \\(.object): \\(.message)"' + `); + console.log(r.stdout); + + // -- Demo 4: Diagnose CrashLoopBackOff -- + console.log("--- Demo 4: Diagnose crashing pod ---"); + r = await tool.execute(` + # Find pods in CrashLoopBackOff + crash_pods=$(get_pods --namespace default | jq -r '.items[] | select(.status == "CrashLoopBackOff") | .name') + + for pod in $crash_pods; do + echo "=== Diagnosing: $pod ===" + describe_pod --name "$pod" --namespace default | jq '{name, status, restarts, image, node}' + echo "" + echo "Recent logs:" + get_logs --name "$pod" --tail 5 + echo "Related events:" + get_events --namespace default | jq -r '.items[] | " [" + .type + "] " + .reason + ": " + .message' + echo "" + done + `); + console.log(r.stdout); + + // -- Demo 5: Scale + rollout -- + console.log("--- Demo 5: Scale deployment and check rollout ---"); + r = await tool.execute(` + echo "Scaling 'app' in production to 5 replicas..." + scale_deployment --name app --namespace production --replicas 5 | jq '.' + echo "" + echo "Rollout status:" + rollout_status --name app --namespace production | jq '.' + `); + console.log(r.stdout); + + // -- Demo 6: Service + configmap inventory -- + console.log("--- Demo 6: Namespace inventory ---"); + r = await tool.execute(` + for ns in default production; do + echo "=== Namespace: $ns ===" + echo "Services:" + get_services --namespace "$ns" | jq -r '.items[] | " \\(.name) (\\(.type)) -> \\(.ports)"' + echo "ConfigMaps:" + get_configmaps --namespace "$ns" | jq -r '.items[] | " \\(.name): \\(.data_keys | join(", "))"' + echo "Secrets:" + get_secrets --namespace "$ns" | jq -r '.items[] | " \\(.name) (\\(.type))"' + echo "" + done + `); + console.log(r.stdout); +} + +// ============================================================================= +// Main +// ============================================================================= + +async function main() { + const tool = buildK8sTool(); + + // Show what the LLM sees + console.log(`Tool: ${tool.name} (${tool.toolCount()} commands)\n`); + console.log("--- System prompt (sent to LLM) ---"); + console.log(tool.systemPrompt()); + + // Run demos + await runDemos(tool); + + console.log("=".repeat(70)); + console.log("Done."); +} + +main().then(() => process.exit(0)); diff --git a/examples/package.json b/examples/package.json index 7d159265..6f84ea34 100644 --- a/examples/package.json +++ b/examples/package.json @@ -21,7 +21,7 @@ "jsondiffpatch": ">=0.7.2" }, "scripts": { - "examples": "node bash_basics.mjs && node data_pipeline.mjs && node llm_tool.mjs && node langchain_integration.mjs", + "examples": "node bash_basics.mjs && node data_pipeline.mjs && node llm_tool.mjs && node k8s_orchestrator.mjs && node langchain_integration.mjs", "examples:ai": "node openai_tool.mjs && node vercel_ai_tool.mjs && node langchain_agent.mjs" } }