Skill: downwind7clawd-ctrl/openclaw-inventory-manager (version 1.0.1)

--- name: Where are you from description: An enterprise-grade asset manager that tracks, manages, and automatically syncs OpenClaw skills capabilities and sources to your GitHub. version: 1.0.0 category: management tags: [inventory, skills, git, openclaw] --- # Where are you from (OpenClaw Skill Inventory Manager) This skill comprehensively audits OpenClaw skills installed from various sources (ClawHub, GitHub, npm, local, etc.). It generates human- and machine-readable manifests (`SKILLS_MANIFEST.json` and `SKILLS_MANIFEST.md`), and securely backs them up to your personal GitHub repository. > [!WARNING] > **Prerequisite: Node.js Required** > This skill manager relies on JavaScript (`inventory.js`) to perform file scanning and Git integration. > You **must** have **Node.js (v14 or higher recommended)** installed on your system for it to function correctly. ## Core Features - **Targeted Scanning**: Optimizes performance by only scanning predefined paths configured in `~/.openclaw/inventory.json`. - **Source Detection**: Accurately tracks the origin and installation method of a skill by analyzing `.git`, `package.json`, and `clawhub.json`. - **Security Scrubbing**: Automatically detects and masks over 10 API Key patterns (e.g., `sk-`, `ghp_`, `hf_`) to prevent sensitive data leaks in the manifest. - **Privacy Layer**: If a `SKILL.private.md` is found, the skill is classified as a "Private Skill (Content Masked)" in the manifest, and its details are fully hidden. - **Git Syncing**: Provides a single `sync --push` command to handle everything from detecting inventory changes to committing and pushing to GitHub. ## Detailed Command Guide ### 0. Guided Setup (`inventory bootstrap`) [RECOMMENDED] The easiest way for first-time users to set up everything (Config + Git + Initial Scan). ```powershell node .agents/skills/openclaw-inventory-manager/inventory.js bootstrap ``` ### 1. Initialization (`inventory init`) Initializes inventory configurations and sets up the root Git repository for tracking skills. ```powershell # Initialize Git tracking in the current directory and generate config node .agents/skills/openclaw-inventory-manager/inventory.js init # Initialize and link directly to a remote GitHub repository node .agents/skills/openclaw-inventory-manager/inventory.js init https://github.com/yourname/my-skills-inventory.git ``` ### 2. Status Check (`inventory status`) Scans for modifications since the last inventory sync. ```powershell node .agents/skills/openclaw-inventory-manager/inventory.js status ``` ### 3. Sync & Commit (`inventory sync`) Updates the manifest file and commits changes to the local Git repository. Add `--push` to upload to the remote. ```powershell # Only update manifest and perform local commit node .agents/skills/openclaw-inventory-manager/inventory.js sync # Upload changes to the remote GitHub repository node .agents/skills/openclaw-inventory-manager/inventory.js sync --push ``` ### 4. Search Skill List (`inventory list / search`) Outputs a quick terminal table of all installed skills. ```powershell node .agents/skills/openclaw-inventory-manager/inventory.js list ``` ## Configuration Structure The configuration is stored in: `~/.openclaw/inventory.json` (Local user home directory) ```json { "searchRoots": ["~/.openclaw/skills", "./skills"], // Paths to scan "maxDepth": 5, // Recursion limit "excludedDirs": ["node_modules", ".git", "dist"], // Folders to ignore "maskPatterns": ["sk-", "ghp_", "hf_", "AIza"], // Secret masking patterns "autoPush": false, // Enable automatic push "manifestPath": "~/.openclaw/SKILLS_MANIFEST.json" // Output location } ``` ## Instruction for Agent (Natural Language Triggers) When the user sends a message matching one of the following trigger phrases, run the corresponding workflow. Matching should be **intent-based** — exact wording is not required. --- ### 🔍 Intent: Audit / Inspect Skills **Trigger phrases (English)** - "Where are you from?" - "Where did my skills come from?" - "Show me my skill inventory" - "List all my installed skills" - "What skills do I have?" - "Audit my skills" - "Analyze my agent environment" - "Check my skill history" - "Show my skill manifest" - "What's installed?" **Trigger phrases (Korean)** - "내 스킬 내역 확인해줘" - "스킬 목록 보여줘" - "어떤 스킬 설치되어 있어?" - "스킬 히스토리 알려줘" **Workflow**: 1. Check if `~/.openclaw/inventory.json` exists. 2. If **NOT** exists → Inform the user that the skill has not been initialized yet and suggest: ``` node inventory.js bootstrap ``` 3. If **EXISTS** → Run `inventory status` and summarize the output (number of skills found, any changes since last sync). 4. If changes are detected, offer to run `inventory sync`. --- ### ☁️ Intent: Sync / Commit / Push to GitHub **Trigger phrases (English)** - "Sync my skills to GitHub" - "Update my skill manifest" - "Commit my inventory" - "Push my skill list" - "Save skill inventory to GitHub" - "Back up my skills" **Trigger phrases (Korean)** - "스킬 동기화해줘" - "깃허브에 올려줘" - "인벤토리 업데이트해줘" - "커밋해줘" **Workflow**: 1. Run `inventory sync --push`. 2. Report the commit status (success, no changes, or error) to the user. 3. If push fails, suggest checking GitHub authentication (SSH or token). --- ### 🚀 Intent: First-Time Setup / Bootstrap **Trigger phrases (English)** - "Set up skill inventory" - "Initialize skill tracking" - "Get started with skill manager" - "Configure the inventory" - "I just installed this skill, what do I do?" **Trigger phrases (Korean)** - "스킬 매니저 설정해줘" - "처음 시작하는 방법 알려줘" - "초기화해줘" **Workflow**: 1. Check if `~/.openclaw/inventory.json` already exists. 2. If NOT exists → Run `inventory bootstrap` and guide through each step. 3. If EXISTS → Inform that a config already exists, and ask if the user wants to re-initialize or just run `status`. --- ## Security Note (Reminder) - Always verify your GitHub authentication before using `sync --push`. - Use `.gitignore` to prevent sensitive credential files from being uploaded. - Utilize `SKILL.private.md` for internal instructions that should never be public.

path: PUBLISH.md
content: # Where are you from (OpenClaw Skill Inventory Manager) 📦🦀

Do you know exactly where all your AI agent skills came from? 
As we download skills from **ClawHub**, **GitHub**, **NPM**, and manual sources, managing their paths and origins becomes a chaotic task. **Where are you from** is an enterprise-grade utility that solves this by providing a unified audit trail for your OpenClaw environment.

## 🚀 Why "Where are you from"? (Motivation)

In the rapidly evolving AI ecosystem, developers frequently install third-party capabilities. However, this leads to several critical issues:
1.  **Path Confusion**: "I installed it, but I can't find it."
2.  **Ghost Skills**: "I deleted the folder, but the agent still thinks I have the skill."
3.  **Security Risks**: "Where did this code come from? Does it have my API keys?"
4.  **Origin Amnesia**: "I don't remember if I got this via NPM or a Git clone."

**Where are you from** was created to bring transparency and security to your AI workstation. By automatically identifying origins and masking secrets, it ensures you stay in control of your agent's modular capabilities.

## 🌟 Key Features

- **Source Detection**: Identify GitHub, NPM, ClawHub, or Manual origins automatically.
- **Security Scrubbing**: Detects and masks 10+ common API key patterns (OpenAI, HF, GHP, etc.).
- **Privacy Protection**: Support for `SKILL.private.md` to classify and hide sensitive instructions.
- **Git Syncing**: One-command synchronization (`sync --push`) to keep your inventory safe on GitHub.

## 🛠️ Prerequisites

This skill uses JavaScript for file scanning and Git integration.
- **Node.js (v14+) is required.**

## ⚡ Quick Start

### 1. Bootstrap (Recommended)
Set up everything (Config + Git + Initial Scan) in one go.
```bash
node .agents/skills/openclaw-inventory-manager/inventory.js bootstrap
```

### 2. Initialize
Link your local inventory directly to a remote GitHub repository.
```bash
node .agents/skills/openclaw-inventory-manager/inventory.js init https://github.com/username/my-skills.git
```

### 3. Sync & Push
Generate your `SKILLS_MANIFEST.md` report and securely sync to the cloud.
```bash
node .agents/skills/openclaw-inventory-manager/inventory.js sync --push
```

Know your AI's origins. Manage your assets. Stay secure with **Where are you from**.


path: README.md
content: # Where are you from (OpenClaw Skill Inventory Manager) 📦🦀

An enterprise-grade asset manager for OpenClaw that automates the auditing, tracking, and synchronization of installed skills to your personal GitHub repository.

## 🚀 Overview

Managing numerous AI skills from different sources (**ClawHub**, **GitHub**, **NPM**, **Manual**) can lead to path confusion, "ghost" skills, and security risks. 

**Where are you from** was created to solve these problems by providing a unified audit trail for your AI workstation. No matter where you downloaded a skill from, this tool knows its origins and history.

## 🛠️ Prerequisites

*   **OpenClaw**: Installed and configured.
*   **Node.js**: **Version 14 or higher is required.** The manager script runs on Node.js.
*   **Git**: Required for the synchronization feature.

## 📁 Project Structure

```text
.agents/skills/openclaw-inventory-manager/
├── SKILL.md                 # English instructions & metadata
├── PUBLISH.md               # Publishing-ready summary & motivation
├── inventory.js              # Main CLI entry point
├── utils/                   # Modular utility libraries
│   ├── scanner.js           # Directory scanning & depth control
│   ├── securityScrubber.js  # Regex-based masking (10+ patterns)
│   ├── gitManager.js        # Git CLI automation
│   ├── sourceDetector.js    # Origin & method detection
│   ├── manifestGenerator.js # JSON/Markdown report creation
│   └── logger.js            # Unified logging utility
└── templates/
    └── manifest-template.md # Customizable report template
```

## 📖 Getting Started

1.  **Bootstrap (Recommended)**: For first-time users, run the guided setup wizard:
    ```bash
    node inventory.js bootstrap
    ```
2.  **Scan**: Run `node inventory.js status` to see what's installed and where it came from.
3.  **Sync**: Run `node inventory.js sync --push` to update your manifests and push to GitHub.

For a step-by-step walkthrough, see [TUTORIAL.md](./TUTORIAL.md).  
For full technical documentation, see [SKILL.md](./SKILL.md).

---
*Where are you from? Know your origins. Stay secure.*


path: SECURITY.md
content: # Security Policy

## Purpose

This is a **read-only metadata utility** for OpenClaw skill inventory management. It scans SKILL.md files and generates manifest reports.

## What This Tool Does NOT Do

- ❌ Collect, transmit, or store any credentials or API keys
- ❌ Make network requests or connect to external servers
- ❌ Execute arbitrary user input or shell commands (git is called via `spawnSync` with strict argument arrays)
- ❌ Access system files outside of the configured skill directories
- ❌ Contain any actual credential values

## Credential Pattern Detection

This tool includes regex patterns that match the **format** of common credential types (e.g., `sk-...`, `ghp_...`, `hf_...`). These patterns are used to **detect and mask** credentials found in SKILL.md files — replacing them with `********[MASKED]********` placeholders.

**These patterns do NOT contain any actual credential values.** They match structural characteristics only (prefix, length, character set).

## Git Operations

Git commands are executed via Node.js `spawnSync` with argument arrays. No user-supplied strings are interpolated into shell commands. Only the following git subcommands are used: `init`, `remote`, `add`, `commit`, `push`, `status`, `branch`, `diff`, `symbolic-ref`.

## Dependency

This tool has **zero runtime dependencies**. It uses only Node.js built-in modules (`fs`, `path`, `os`, `readline`, `child_process`).

## Reporting

If you find a genuine security issue, please open a GitHub Issue.


path: TUTORIAL.md
content: # 5-Minute Quick Start Tutorial: "Where are you from" 📦🦀

Welcome! This tutorial will guide you from installation to your first cloud-synced skill inventory in just 5 minutes.

## Step 1: The Grand Welcome (`bootstrap`)

Instead of manually editing config files, let's use the guided setup. Open your terminal in your preferred project folder and run:

```bash
node .agents/skills/openclaw-inventory-manager/inventory.js bootstrap
```

**What happens?**
- You'll see a beautiful ASCII splash screen.
- A default configuration is created in `~/.openclaw/inventory.json`.
- A local Git repository is initialized in your current folder.
- A first-ever scan is performed to find your skills.

## Step 2: Check Your First Report

After the bootstrap finishes, look for a new file named **`SKILLS_MANIFEST.md`** in your folder. Open it to see:
- A list of all your installed skills.
- Their versions and origins (GitHub, NPM, etc.).
- Their privacy status (Locked/Unlocked).

## Step 3: Link to GitHub

Now, let's make this inventory persistent. If you haven't already, create a new **private** repository on GitHub naming it something like `my-skills-inventory`. Then run:

```bash
node .agents/skills/openclaw-inventory-manager/inventory.js init https://github.com/yourusername/my-skills-inventory.git
```

## Step 4: Your First Cloud Sync

Let's push your data to the moon! 🚀

```bash
node .agents/skills/openclaw-inventory-manager/inventory.js sync --push
```

**Note**: You'll see a confirmation message before the push happens. This ensures you've reviewed the manifest and no sensitive data (like unmasked keys) is being uploaded.

## Step 5: Track Changes Over Time

As you install or remove skills, run `status` to see what's new or changed since your last commit:

```bash
node .agents/skills/openclaw-inventory-manager/inventory.js status
```

This compares your current `SKILLS_MANIFEST.json` against the last Git commit using `git diff`. If the file has changed, you'll see a warning prompting you to run `sync`.

> [!NOTE]
> **Ghost Skill Detection** (removed skills that are still in the manifest) is on the roadmap for a future release. For now, running `sync` after removing a skill will update the manifest to reflect only currently-found skills.

---

### Pro Tip: Natural Language Control
Since this is an AI agent skill, you don't always need to type commands. Just ask your agent:
- *"Analyze my skills"*
- *"Where did my skills come from?"*
- *"Sync my inventory to GitHub"*

The agent will know exactly which scripts to run!


path: inventory.js
content: const fs = require("fs");
const path = require("path");
const os = require("os");
const readline = require("readline");
const logger = require("./utils/logger");
const scanner = require("./utils/scanner");
const securityScrubber = require("./utils/securityScrubber");
const sourceDetector = require("./utils/sourceDetector");
const gitManager = require("./utils/gitManager");
const manifestGenerator = require("./utils/manifestGenerator");

/**
 * OpenClaw Skill Inventory Manager (v1.0.0)
 * Main Controller & CLI Router
 *
 * SECURITY: This tool only reads SKILL.md metadata files and generates
 * manifest reports. It does NOT collect, transmit, or store credentials.
 */

const CONFIG_PATH = path.join(os.homedir(), ".openclaw", "inventory.json");

function resolveManifestPath(config) {
  let manifestPath = config.manifestPath || "~/.openclaw/SKILLS_MANIFEST.json";
  
  if (manifestPath.startsWith("~")) {
    manifestPath = path.join(os.homedir(), manifestPath.slice(1));
  } else if (!path.isAbsolute(manifestPath)) {
    manifestPath = path.resolve(path.dirname(CONFIG_PATH), manifestPath);
  }
  
  return manifestPath;
}

function loadConfig() {
  if (!fs.existsSync(CONFIG_PATH)) {
    throw new Error("Configuration not found. Please run 'inventory init' first.");
  }
  try {
    const config = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf8"));
    config.manifestPath = resolveManifestPath(config);
    return config;
  } catch (e) {
    throw new Error(`Failed to parse inventory.json: ${e.message}`);
  }
}

async function run() {
  const args = process.argv.slice(2);
  const command = args[0];

  switch (command) {
    case "init":
      await handleInit(args.slice(1));
      break;
    case "bootstrap":
      await handleBootstrap();
      break;
    case "status":
      await handleStatus();
      break;
    case "sync":
      await handleSync(args.slice(1));
      break;
    case "list":
      await handleList();
      break;
    default:
      showHelp();
  }
}

async function handleBootstrap() {
  console.log(`\x1b[36m
  █   █  █  █  █▀▀  █▀▀█  █▀▀      █▀▀█  █▀▀█  █▀▀  █  █     █▀▀█  █▀▀█  █▀▀█  █▀▄▀█
  █   █  █▀▀█  █▀▀  █▄▄▀  █▀▀      █▄▄█  █▄▄▀  █▀▀  █  █     █▀▀▄  █▄▄▀  █  █  █ █ █
   ▀▄▀   ▀  ▀  ▀▀▀  ▀  ▀  ▀▀▀      ▀  ▀  ▀  ▀  ▀▀▀  ▀▀▀▀     ▀▀▀▀  ▀  ▀  ▀▀▀▀  ▀   ▀
  \x1b[0m`);
  logger.header("Where are you from - Guided Setup");
  logger.info("Welcome! Let's get your skill inventory sorted in 3 simple steps.");

  // 1. Config Check/Creation
  const configDir = path.dirname(CONFIG_PATH);
  if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true });
  
  if (!fs.existsSync(CONFIG_PATH)) {
    const defaultConfig = {
      searchRoots: ["~/.openclaw/skills", "./skills"],
      maxDepth: 5,
      excludedDirs: ["node_modules", ".git", "dist", ".cache"],
      maskPatterns: ["sk-", "ghp_", "hf_", "AIza", "Bearer"],
      autoPush: false,
      manifestPath: "~/.openclaw/SKILLS_MANIFEST.json"
    };
    fs.writeFileSync(CONFIG_PATH, JSON.stringify(defaultConfig, null, 2));
    logger.success("STEP 1: Default configuration created (~/.openclaw/inventory.json).");
  } else {
    logger.info("STEP 1: Configuration already exists. Skipping.");
  }

  // 2. Git Check/Initialization
  logger.info("STEP 2: Preparing local Git tracking...");
  gitManager.init(process.cwd());

  // 3. Initial Scan and Report
  logger.info("STEP 3: Performing your FIRST-EVER skill audit...");
  await handleSync([]);
  
  console.log("\n\x1b[32m✔ SUCCESS!\x1b[0m Your skill inventory is ready.");
  console.log("Check \x1b[33mSKILLS_MANIFEST.md\x1b[0m in your current folder to see what I found!");
  console.log("\nNext Steps: Run \x1b[36minventory sync --push\x1b[0m to save this to GitHub.");
}

async function handleInit(args) {
  logger.header("Initializing Inventory Manager");
  const remoteUrl = args[0];
  
  // 1. Create config directory if not exists
  const configDir = path.dirname(CONFIG_PATH);
  if (!fs.existsSync(configDir)) {
    fs.mkdirSync(configDir, { recursive: true });
  }

  // 2. Create default config if not exists
  if (!fs.existsSync(CONFIG_PATH)) {
    const defaultConfig = {
      searchRoots: ["~/.openclaw/skills", "./skills"],
      maxDepth: 5,
      excludedDirs: ["node_modules", ".git", "dist", ".cache"],
      maskPatterns: ["sk-", "ghp_", "hf_", "AIza", "Bearer"],
      autoPush: false,
      manifestPath: "~/.openclaw/SKILLS_MANIFEST.json"
    };
    fs.writeFileSync(CONFIG_PATH, JSON.stringify(defaultConfig, null, 2));
    logger.success(`Config created at ${CONFIG_PATH}`);
  }

  // 3. Initialize Git in current workspace
  gitManager.init(process.cwd(), remoteUrl);
  logger.success("Initialization complete.");
}

async function handleStatus() {
  logger.header("Inventory Status Check");
  const config = loadConfig();
  const skills = scanner.scan(config.searchRoots, {
    maxDepth: config.maxDepth,
    excludedDirs: config.excludedDirs
  });

  const changed = gitManager.status(process.cwd(), config.manifestPath);
  if (changed) {
    logger.warn("Changes detected in local skills. Run 'inventory sync' to update manifest.");
  } else {
    logger.success("Inventory is up to date with last commit.");
  }

  console.log(`\nDetected Skills: ${skills.length}`);
  skills.forEach(s => {
    const source = sourceDetector.detect(s.path, s.realPath);
    console.log(`${source.icon} ${s.name} (${s.path}) [${source.method}]`);
  });
}

async function handleSync(args) {
  logger.header("Syncing Inventory & Committing");
  const config = loadConfig();
  const shouldPush = args.includes("--push") || config.autoPush;

  // 1. Scan and detect sources
  const rawSkills = scanner.scan(config.searchRoots, {
    maxDepth: config.maxDepth,
    excludedDirs: config.excludedDirs
  });

  const skillsWithMetadata = rawSkills.map(s => {
    const info = sourceDetector.detect(s.path, s.realPath);

    // Read SKILL.md content and scrub sensitive data before adding to manifest
    let description = "";
    try {
      const rawContent = fs.readFileSync(s.skillFilePath, "utf8");
      if (s.isPrivate) {
        // For private skills, only extract YAML frontmatter metadata
        description = securityScrubber.extractMetadataOnly(rawContent, config.maskPatterns || []);
      } else {
        // For public skills, scrub any accidentally embedded secrets
        const scrubbed = securityScrubber.scrub(rawContent, config.maskPatterns || []);
        // Extract just the description from frontmatter for the manifest
        const descMatch = scrubbed.match(/description:\s*(.+)/);
        description = descMatch ? descMatch[1].trim() : "(no description)";
      }
    } catch (e) {
      description = "(could not read SKILL.md)";
    }

    return { ...s, ...info, description };
  });

  // 2. Generate Manifests
  const templatePath = path.join(__dirname, "templates", "manifest-template.md");
  manifestGenerator.generate(skillsWithMetadata, config.manifestPath, templatePath);

  // 3. Git Sync
  const committed = gitManager.commit(process.cwd(), `chore(inventory): update skills manifest [${new Date().toISOString()}]`);
  
  if (committed && shouldPush) {
    const rl = readline.createInterface({
      input: process.stdin,
      output: process.stdout
    });

    await new Promise((resolve) => {
      rl.question("Push changes to GitHub repository? (y/N): ", (answer) => {
        rl.close();
        if (answer.toLowerCase() === "y") {
          logger.info("Proceeding to push to remote...");
          gitManager.push(process.cwd());
        } else {
          logger.warn("Push cancelled by user.");
        }
        resolve();
      });
    });
  }
}

async function handleList() {
  const config = loadConfig();
  const skills = scanner.scan(config.searchRoots, {
    maxDepth: config.maxDepth,
    excludedDirs: config.excludedDirs
  });
  
  logger.header("Installed Skills List");
  const table = skills.map(s => {
    const info = sourceDetector.detect(s.path, s.realPath);
    return { name: s.name, method: info.method, source: info.source, private: s.isPrivate };
  });
  console.table(table);
}

function showHelp() {
  console.log(`
Usage: inventory <command> [options]

Commands:
  bootstrap          Interactive/Guided setup for first-time users
  init [remote]      Initialize inventory config and git repository
  status             Check for changes and list detected skills
  sync [--push]      Update manifests and commit (optionally push)
  list               Display table of all installed skills
  `);
}

run().catch(err => {
  logger.error(err.message);
  process.exit(1);
});


path: templates/manifest-template.md
content: # SKILLS MANIFEST

> Generated by **Where are you from** — OpenClaw Skill Inventory Manager
>
> **Last Updated**: {{lastUpdated}}
> **Total Skills Tracked**: {{totalSkills}}

---

## 📦 Installed Skills

| Icon | Name | Version | Description | Source | Method | Private | Status |
| :--- | :--- | :---: | :--- | :--- | :---: | :---: | :---: |
{{skillRows}}

---

*Managed by [Where are you from](https://github.com/your-repo/openclaw-inventory-manager)*


path: utils/gitManager.js
content: const { spawnSync, execSync } = require("child_process");
const fs = require("fs");
const path = require("path");
const logger = require("./logger");

/**
 * Git Manager Logic (v1.0.0)
 * Read-only metadata utility for git repository operations.
 * Does NOT access, transmit, or collect credentials.
 * Uses spawnSync (argument arrays, no shell interpolation).
 */

/**
 * SECURITY NOTE: This utility wraps the git CLI using spawnSync with
 * argument arrays (never shell interpolation). Only the following git
 * subcommands are used: init, remote, add, commit, push, status, branch,
 * diff, symbolic-ref. No user-supplied strings are passed to a shell.
 *
 * This is a read-only metadata utility - it does NOT access, transmit,
 * or collect any credentials, API keys, or personal data.
 *
 * @param {string[]} args - Git command arguments as an array.
 * @param {string} cwd - Working directory.
 * @returns {{ success: boolean, stdout: string, stderr: string }}
 */
function gitRun(args, cwd) {
  const result = spawnSync("git", args, { cwd, encoding: "utf8" });
  return {
    success: result.status === 0,
    stdout: result.stdout || "",
    stderr: result.stderr || ""
  };
}

const gitManager = {
  /**
   * Checks if the directory is a git repository.
   */
  isRepo: (dir) => fs.existsSync(path.join(dir, ".git")),

  /**
   * Initializes a git repository.
   */
  init: (dir, remoteUrl) => {
    try {
      if (!gitManager.isRepo(dir)) {
        logger.info(`Initializing git repository in ${dir}...`);
        const initResult = gitRun(["init"], dir);
        if (!initResult.success) throw new Error(initResult.stderr);
      }

      if (remoteUrl) {
        const remoteResult = gitRun(["remote", "add", "origin", remoteUrl], dir);
        if (remoteResult.success) {
          logger.success(`Remote 'origin' set to ${remoteUrl}`);
        } else {
          logger.warn(`Remote 'origin' already exists or failed to set.`);
        }
      }

      // Create .gitignore if not exists
      const gitignorePath = path.join(dir, ".gitignore");
      if (!fs.existsSync(gitignorePath)) {
        const ignoreList = [
          ".openclaw/credentials",
          "*.key",
          "secrets.*",
          "node_modules",
          ".DS_Store",
          "*.bak"
        ].join("\n");
        fs.writeFileSync(gitignorePath, ignoreList);
        logger.success(".gitignore created.");
      }
    } catch (err) {
      logger.error(`Git init failed: ${err.message}`);
    }
  },

  /**
   * Commits the changes to the repository.
   * Uses spawnSync to safely pass commit message without shell injection risk.
   */
  commit: (dir, message) => {
    try {
      const addResult = gitRun(["add", "."], dir);
      if (!addResult.success) throw new Error(`git add failed: ${addResult.stderr}`);

      const statusResult = gitRun(["status", "--porcelain"], dir);
      if (!statusResult.stdout.trim()) {
        logger.info("No changes to commit.");
        return false;
      }

      // Safe: message is passed as a separate argument, not interpolated into a shell string
      const commitResult = gitRun(["commit", "-m", message], dir);
      if (!commitResult.success) throw new Error(`git commit failed: ${commitResult.stderr}`);

      logger.success(`Committed changes: ${message}`);
      return true;
    } catch (err) {
      logger.error(`Git commit failed: ${err.message}`);
      return false;
    }
  },

  /**
   * Gets the current branch name.
   */
  _getCurrentBranch: (dir) => {
    try {
      const result = gitRun(["branch", "--show-current"], dir);
      if (result.success && result.stdout.trim()) {
        return result.stdout.trim();
      }
      const symbolicResult = gitRun(["symbolic-ref", "--short", "HEAD"], dir);
      if (symbolicResult.success && symbolicResult.stdout.trim()) {
        return symbolicResult.stdout.trim();
      }
      return "main";
    } catch (e) {
      return "main";
    }
  },

  /**
   * Pushes the changes to the remote.
   */
  push: (dir) => {
    try {
      logger.info("Pushing changes to remote...");
      const currentBranch = gitManager._getCurrentBranch(dir);
      logger.info(`Detected branch: ${currentBranch}`);
      const result = gitRun(["push", "origin", currentBranch], dir);
      if (!result.success) throw new Error(result.stderr);
      logger.success(`Pushed to GitHub (${currentBranch})!`);
      return true;
    } catch (err) {
      logger.warn(`Git push failed (check network/auth): ${err.message}`);
      return false;
    }
  },

  /**
   * Checks if the manifest file has uncommitted changes.
   */
  status: (dir, file) => {
    try {
      const relativeFile = path.relative(dir, file).replace(/\\/g, "/");
      // Check for both modified (diff) and untracked (porcelain) states
      const diffResult = gitRun(["diff", "--name-only", relativeFile], dir);
      const untrackedResult = gitRun(["status", "--porcelain", relativeFile], dir);
      return diffResult.stdout.includes(relativeFile) || untrackedResult.stdout.includes(relativeFile);
    } catch (e) {
      return true; // Assume changed if error
    }
  }
};

module.exports = gitManager;


path: utils/logger.js
content: /**
 * Unified Logging Utility (v1.0.0)
 */
const colors = {
  reset: "\x1b[0m",
  bright: "\x1b[1m",
  dim: "\x1b[2m",
  red: "\x1b[31m",
  green: "\x1b[32m",
  yellow: "\x1b[33m",
  blue: "\x1b[34m",
  magenta: "\x1b[35m",
  cyan: "\x1b[36m"
};

const logger = {
  info: (msg) => console.log(`${colors.blue}[INFO]${colors.reset} ${msg}`),
  success: (msg) => console.log(`${colors.green}[SUCCESS]${colors.reset} ${msg}`),
  warn: (msg) => console.log(`${colors.yellow}[WARN]${colors.reset} ${msg}`),
  error: (msg) => console.error(`${colors.red}[ERROR]${colors.reset} ${msg}`),
  debug: (msg) => {
    if (process.env.DEBUG) {
      console.log(`${colors.dim}[DEBUG]${colors.reset} ${msg}`);
    }
  },
  header: (msg) => console.log(`\n${colors.bright}${colors.cyan}=== ${msg} ===${colors.reset}\n`)
};

module.exports = logger;


path: utils/manifestGenerator.js
content: const fs = require("fs");
const path = require("path");
const logger = require("./logger");
const securityScrubber = require("./securityScrubber");

/**
 * Manifest Generator Logic (v1.0.0)
 * Generates JSON and Markdown inventory reports with backup support.
 */

const manifestGenerator = {
  /**
   * Detects duplicate skills across different sources.
   */
  _detectDuplicates: (skills) => {
    const skillMap = new Map();
    
    skills.forEach((skill) => {
      const key = skill.name.toLowerCase();
      if (!skillMap.has(key)) {
        skillMap.set(key, []);
      }
      skillMap.get(key).push(skill);
    });

    const duplicates = [];
    const recommendedOrder = ["clawhub", "skillsmp", "git", "npm", "manual"];

    skillMap.forEach((instances, name) => {
      if (instances.length > 1) {
        const sorted = instances.sort((a, b) => {
          const idxA = recommendedOrder.indexOf(a.method);
          const idxB = recommendedOrder.indexOf(b.method);
          if (idxA !== idxB) return idxA - idxB;
          
          const verA = a.version || "0.0.0";
          const verB = b.version || "0.0.0";
          return manifestGenerator._compareVersions(verB, verA);
        });

        duplicates.push({
          name: instances[0].name,
          count: instances.length,
          instances: instances.map((i) => ({
            method: i.method,
            source: i.source,
            version: i.version || "unknown",
            path: i.path
          })),
          recommended: sorted[0]
        });
      }
    });

    return duplicates;
  },

  _compareVersions: (v1, v2) => {
    const parts1 = v1.split(".").map((p) => isNaN(Number(p)) ? 0 : Number(p));
    const parts2 = v2.split(".").map((p) => isNaN(Number(p)) ? 0 : Number(p));
    for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
      const p1 = parts1[i] || 0;
      const p2 = parts2[i] || 0;
      if (p1 !== p2) return p1 - p2;
    }
    return 0;
  },

  /**
   * Generates the dual manifest (JSON + MD).
   */
  generate: (skills, outputPath, templatePath) => {
    const jsonPath = outputPath.endsWith(".json") ? outputPath : `${outputPath}.json`;
    const mdPath = jsonPath.replace(".json", ".md");

    const duplicates = manifestGenerator._detectDuplicates(skills);

    // 1. Prepare JSON Data
    const manifestData = {
      manifestVersion: 2,
      lastUpdated: new Date().toISOString(),
      totalSkills: skills.length,
      duplicates: duplicates,
      skills: skills.map((s) => ({
        name: s.name,
        version: s.version || "0.0.1",
        source: s.source,
        method: s.method,
        isPrivate: s.isPrivate,
        path: s.path,
        realPath: s.realPath,
        description: s.description || "",
        status: s.status || "active",
        lastChecked: new Date().toISOString()
      }))
    };

    // 2. Backup Existing Manifest
    if (fs.existsSync(jsonPath)) {
      try {
        fs.copyFileSync(jsonPath, `${jsonPath}.bak`);
        logger.info(`Backup created: ${path.basename(jsonPath)}.bak`);
      } catch (e) {
        logger.warn(`Failed to create backup: ${e.message}`);
      }
    }

    // 3. Write JSON Manifest
    try {
      fs.writeFileSync(jsonPath, JSON.stringify(manifestData, null, 2));
      logger.success(`Manifest JSON updated: ${jsonPath}`);
    } catch (e) {
      logger.error(`Failed to write JSON manifest: ${e.message}`);
    }

    // 4. Generate Markdown Report
    manifestGenerator._generateMarkdown(manifestData, mdPath, templatePath);
  },

  _generateMarkdown: (data, mdPath, templatePath) => {
    try {
      let content = "";
      if (fs.existsSync(templatePath)) {
        content = fs.readFileSync(templatePath, "utf8");
      } else {
        content = `# SKILLS MANIFEST\n\nGenerated by OpenClaw Inventory Manager\n\nLast Updated: {{lastUpdated}}\n\nTotal Skills: {{totalSkills}}\n\n{{duplicatesSection}}\n\n| Icon | Name | Version | Description | Source | Method | Private | Status |\n| :--- | :--- | :---: | :--- | :--- | :---: | :---: | :---: |\n{{skillRows}}`;
      }

      const rows = data.skills
        .map((s) => {
          const icon = manifestGenerator._getIcon(s.method);
          const sourceLink = s.source.startsWith("http") ? `[Link](${s.source})` : s.source;
          return `| ${icon} | **${s.name}** | ${s.version} | ${s.description} | ${sourceLink} | ${s.method} | ${s.isPrivate ? "🔒 Yes" : "🔓 No"} | ${s.status} |`;
        })
        .join("\n");

      let duplicatesSection = "";
      if (data.duplicates && data.duplicates.length > 0) {
        duplicatesSection = `## ⚠️ Duplicate Skills Detected\n\nThe following skills are installed from multiple sources:\n\n`;
        data.duplicates.forEach((dup) => {
          duplicatesSection += `### ${dup.name}\n`;
          duplicatesSection += `| Source | Method | Version |\n| :--- | :--- | :--- |\n`;
          dup.instances.forEach((inst) => {
            duplicatesSection += `| ${inst.source} | ${inst.method} | ${inst.version} |\n`;
          });
          duplicatesSection += `\n**Recommended:** ${dup.recommended.method} (${dup.recommended.version})\n\n`;
        });
      }

      let md = content
        .replace("{{lastUpdated}}", data.lastUpdated)
        .replace("{{totalSkills}}", data.totalSkills)
        .replace("{{duplicatesSection}}", duplicatesSection)
        .replace("{{skillRows}}", rows);

      fs.writeFileSync(mdPath, md);
      logger.success(`Manifest Markdown updated: ${mdPath}`);
    } catch (e) {
      logger.error(`Failed to generate markdown report: ${e.message}`);
    }
  },

  _getIcon: (method) => {
    switch (method) {
      case "git": return "📁";
      case "npm": return "📦";
      case "clawhub": return "🦀";
      case "skillsmp": return "🧩";
      case "manual": return "👤";
      default: return "🔹";
    }
  }
};

module.exports = manifestGenerator;


path: utils/scanner.js
content: const fs = require("fs");
const path = require("path");
const os = require("os");
const logger = require("./logger");

/**
 * Scanner Logic (v1.0.0)
 * Targeted scanning, mtime check, realpath handling, and depth control.
 */

const scanner = {
  /**
   * Translates common shortcuts like '~' to the user's home directory.
   */
  resolvePath: (p) => {
    if (p.startsWith("~")) {
      return path.join(os.homedir(), p.slice(1));
    }
    return path.resolve(p);
  },

  /**
   * Recursively scans for directories containing SKILL.md.
   */
  scan: (roots, options) => {
    const { maxDepth = 5, excludedDirs = [], manifest = {} } = options;
    const foundSkills = [];

    roots.forEach((root) => {
      const resolvedRoot = scanner.resolvePath(root);
      if (!fs.existsSync(resolvedRoot)) {
        logger.warn(`Search root does not exist: ${resolvedRoot}`);
        return;
      }
      logger.info(`Scanning: ${resolvedRoot}`);
      scanner._recursiveScan(resolvedRoot, 0, maxDepth, excludedDirs, foundSkills);
    });

    return foundSkills;
  },

  _recursiveScan: (dir, currentDepth, maxDepth, excludedDirs, foundSkills) => {
    if (currentDepth > maxDepth) return;

    try {
      const entries = fs.readdirSync(dir, { withFileTypes: true });

      // Check if current directory is a skill (contains SKILL.md)
      const skillFile = entries.find(e => e.isFile() && (e.name === "SKILL.md" || e.name === "SKILL.private.md"));
      if (skillFile) {
        const stats = fs.statSync(dir);
        const realPath = fs.realpathSync(dir);
        const isPrivate = skillFile.name === "SKILL.private.md";

        foundSkills.push({
          name: path.basename(dir),
          path: dir,
          realPath: realPath,
          isPrivate: isPrivate,
          mtime: stats.mtime,
          skillFilePath: path.join(dir, skillFile.name)
        });
        // We don't need to scan deeper if we already found a skill in this folder
        return;
      }

      // Deeper search if not found
      // NOTE: We intentionally do NOT skip all dot-folders here.
      // OpenClaw skills live in `.agents/skills/`, which would be incorrectly pruned.
      // Only directories listed in `excludedDirs` (from inventory.json) are skipped.
      entries.forEach((entry) => {
        if (!entry.isDirectory()) return;
        if (excludedDirs.includes(entry.name)) return;

        scanner._recursiveScan(
          path.join(dir, entry.name),
          currentDepth + 1,
          maxDepth,
          excludedDirs,
          foundSkills
        );
      });
    } catch (err) {
      logger.error(`Failed to scan ${dir}: ${err.message}`);
    }
  }
};

module.exports = scanner;


path: utils/securityScrubber.js
content: const logger = require("./logger");

/**
 * Security Scrubbing Logic (v1.0.0)
 * Masks sensitive API keys and tokens using standard regex patterns.
 */

/**
 * These patterns detect common credential formats in skill files
 * and replace them with safe placeholders. The patterns match the
 * STRUCTURE of credentials (length, prefix, charset) - they do NOT
 * contain any actual credential values.
 *
 * Users can customize via inventory.json maskPatterns field.
 */
const SECRETS_PATTERNS = [
  { name: "OpenAI Secret Key", regex: /sk-[a-zA-Z0-9]{32,48}/g },
  { name: "OpenAI Public Key", regex: /pk-[a-zA-Z0-9]{32,48}/g },
  { name: "GitHub Access Token", regex: /ghp_[a-zA-Z0-9]{36,40}/g },
  { name: "Hugging Face Token", regex: /hf_[a-zA-Z0-9]{34,40}/g },
  { name: "Google API Key", regex: /AIza[0-9A-Za-z_-]{35}/g },
  { name: "Bearer Token", regex: /Bearer\s+[a-zA-Z0-9\._\-]{20,}/gi },
  { name: "Generic Secret/Key", regex: /(?:key|token|password|secret|auth)\s*[:=]\s*["']?[a-zA-Z0-9_\-]{10,}["']?/gi },
  { name: "AWS Access Key", regex: /AKIA[0-9A-Z]{16}/g },
  { name: "Stripe Key", regex: /sk_test_[a-zA-Z0-9]{24,40}/g },
  { name: "Discord Token", regex: /[MN][A-Za-z\d]{23}\.[\w-]{6}\.[\w-]{27}/g }
];

const securityScrubber = {
  /**
   * Masks sensitive data in a given string.
   * @param {string} content - The raw content to scrub.
   * @param {Array<string>} customPatterns - Optional user-defined patterns.
   * @returns {string} - The safely masked content.
   */
  scrub: (content, customPatterns = []) => {
    if (!content) return "";
    let scrubbed = content;

    // Apply default patterns
    SECRETS_PATTERNS.forEach((pattern) => {
      scrubbed = scrubbed.replace(pattern.regex, (match) => {
        logger.debug(`Masking detected [REDACTED_SECRET]`);
        return "********[MASKED]********";
      });
    });

    // Apply custom patterns from inventory.json
    customPatterns.forEach((p) => {
      let regex;
      if (p instanceof RegExp) {
        regex = p;
      } else if (typeof p !== "string" || p.length === 0) {
        logger.warn(`Invalid custom pattern (not a string or empty): ${p}`);
        return;
      } else {
        try {
          regex = new RegExp(p, "gi");
        } catch (e) {
          logger.error(`Invalid custom regex pattern: ${p} - ${e.message}`);
          return;
        }
      }
      try {
        if (regex.global) {
          let match;
          while ((match = regex.exec(scrubbed)) !== null) {
            logger.debug(`Custom pattern matched: ${p}`);
            break;
          }
        }
        scrubbed = scrubbed.replace(regex, "********[CUSTOM-MASKED]********");
      } catch (e) {
        logger.error(`Invalid custom regex pattern: ${p} - ${e.message}`);
      }
    });

    return scrubbed;
  },

  /**
   * Prepares a SKILL.md summary by extracting only safe headers/frontmatter.
   * Used when "metadata-only" summary is preferred for high security.
   */
  extractMetadataOnly: (content, customPatterns = []) => {
    const frontmatterMatch = content.match(/^---([\s\S]*?)---/);
    if (frontmatterMatch) {
      let frontmatter = frontmatterMatch[1];
      frontmatter = securityScrubber.scrub(frontmatter, customPatterns);
      return `---${frontmatter}---\n\n*(Full instructions masked for security)*`;
    }
    return "*(No frontmatter found. Full content masked for security)*";
  }
};

module.exports = securityScrubber;


path: utils/sourceDetector.js
content: const fs = require("fs");
const path = require("path");
const logger = require("./logger");

/**
 * Source Detection Logic (v1.0.0)
 * Identifies the origin of a skill (Git, NPM, ClawHub, Local).
 */

const sourceDetector = {
  /**
   * Detects the source of a skill at the given path.
   */
  detect: (skillPath, realPath) => {
    const findFile = (name) => {
      const p1 = path.join(skillPath, name);
      if (fs.existsSync(p1)) return p1;
      if (realPath && realPath !== skillPath) {
        const p2 = path.join(realPath, name);
        if (fs.existsSync(p2)) return p2;
      }
      return null;
    };

    const clawhubJson = findFile("clawhub.json");
    const managedClawhubJson = findFile(".clawhub.json");
    const gitDir = findFile(".git");
    const pkgJson = findFile("package.json");

    // 1. Check for ClawHub specific metadata
    if (clawhubJson || managedClawhubJson) {
      try {
        const metadataFile = clawhubJson || managedClawhubJson;
        const config = JSON.parse(fs.readFileSync(metadataFile, "utf8"));
        return {
          method: "clawhub",
          source: config.repository || config.url || "ClawHub Registry",
          icon: "🦀"
        };
      } catch (e) {
        logger.debug(`Failed to parse clawhub.json in ${skillPath}`);
      }
    }

    // 2. Check for SkillsMP (agent-skills-cli) lockfile
    const skillsMPPath = path.join(process.env.HOME || process.env.USERPROFILE || "~", ".skills", "skills.lock");
    if (fs.existsSync(skillsMPPath)) {
      try {
        const lockData = JSON.parse(fs.readFileSync(skillsMPPath, "utf8"));
        const normalizedPath = path.normalize(skillPath);
        
        const findSkillInLock = (lockObj) => {
          if (lockObj.skills) {
            for (const [skillName, skillData] of Object.entries(lockObj.skills)) {
              if (skillData.path && path.normalize(skillData.path) === normalizedPath) {
                return { name: skillName, version: skillData.version };
              }
            }
          }
          if (Array.isArray(lockObj)) {
            for (const skillData of lockObj) {
              if (skillData.path && path.normalize(skillData.path) === normalizedPath) {
                return { name: skillData.name, version: skillData.version };
              }
            }
          }
          return null;
        };

        const matchedSkill = findSkillInLock(lockData);
        if (matchedSkill) {
          return {
            method: "skillsmp",
            source: matchedSkill.name,
            version: matchedSkill.version,
            icon: "📦"
          };
        }
      } catch (e) {
        logger.debug(`Failed to parse skills.lock in ${skillsMPPath}: ${e.message}`);
      }
    }

    // 3. Check for Git Repository
    if (gitDir) {
      try {
        const configPath = path.join(gitDir, "config");
        if (fs.existsSync(configPath)) {
          const configContent = fs.readFileSync(configPath, "utf8");
          const urlMatch = configContent.match(/url\s*=\s*(.*)/);
          if (urlMatch) {
            return {
              method: "git",
              source: urlMatch[1].trim(),
              icon: "📁"
            };
          }
        }
      } catch (e) {
        logger.debug(`Failed to read git config in ${skillPath}`);
      }
    }

    // 4. Check for NPM Package
    if (pkgJson) {
      try {
        const pkg = JSON.parse(fs.readFileSync(pkgJson, "utf8"));
        return {
          method: "npm",
          source: pkg.name || "NPM Package",
          icon: "📦"
        };
      } catch (e) {
        logger.debug(`Failed to parse package.json in ${skillPath}`);
      }
    }

    // 5. Default to Local/Manual
    return {
      method: "manual",
      source: "Local/Manual Installation",
      icon: "👤"
    };
  }
}
;

module.exports = sourceDetector;


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: stale | SkillSpector: suspicious (CRITICAL)