Skill: frank-bot07/openclaw-interchange (version 1.0.0)

--- name: openclaw-interchange description: Shared .md interchange library for OpenClaw skills — atomic writes, deterministic serialization, YAML frontmatter, advisory locking, and schema validation. The foundation all other OpenClaw skills build on. --- # @openclaw/interchange The shared library that powers agent-to-agent communication via `.md` files. ## Usage ```javascript import { writeMd, readMd, acquireLock } from '@openclaw/interchange'; // Write an interchange file atomically await writeMd('ops/status.md', { skill: 'crm', status: 'healthy' }, '## Status\nAll systems go.'); // Read it back const { meta, content } = readMd('ops/status.md'); ``` ## Key Features - Atomic writes (tmp + fsync + rename) - Deterministic serialization (sorted keys, stable YAML) - Advisory file locking with stale lock detection - YAML frontmatter parsing - Schema validation - Circuit breaker pattern - Generation tracking + content hashing

path: CODEX_REVIEW.md
content: # Code Review: @openclaw/interchange

**Reviewer:** Codex subagent  
**Date:** 2026-02-19  
**Scope:** Full library review — all src/, test/, package.json, README.md  
**Verdict:** Solid foundation with several issues that need attention before this is truly bulletproof.

---

## CRITICAL

### 1. Lock is not actually exclusive under race conditions (lock.js)

The `O_CREAT | O_EXCL` open is atomic on POSIX, so the initial acquire is safe. **However**, the stale-lock cleanup path has a TOCTOU race:

```js
const lockPid = parseInt(fs.readFileSync(lockPath, 'utf8').trim(), 10);
if (lockPid && !isProcessAlive(lockPid)) {
  try { fs.unlinkSync(lockPath); } catch {}
  continue; // Two processes can both unlink + re-create
}
```

Two waiters can both read the stale PID, both confirm it's dead, both unlink, and both succeed on the next `O_EXCL` open — one gets the lock, then the other immediately overwrites the lock file on its next iteration. This breaks mutual exclusion.

**Fix:** After unlinking, don't `continue` — fall through to the normal `O_EXCL` open attempt which will naturally resolve the race (only one wins). The current code already does this structurally, but the `continue` bypasses the timeout check, creating an infinite-loop risk if unlink keeps failing silently. Wrap the whole stale-detection in a single atomic rename-and-recreate instead.

### 2. Lock fd is never closed on the happy path before unlink (lock.js)

`releaseLock` calls `closeSync` then `unlinkSync`. But the fd was opened with `O_EXCL` and written to — it's held open for the entire lock duration. The file descriptor is **not used for flock()** — it's just a marker file. This means:

- Another process could unlink the lock file while the holder still has it open (stale detection does exactly this)
- The holder wouldn't know its lock was stolen

This is an **advisory** lock by convention only. It works for single-process sequential use but is not robust for true multi-process concurrency. Document this limitation explicitly or switch to `flock()` via a native addon.

### 3. `writeMd` doesn't pass caller's frontmatter fields through validation (io.js)

`writeMd` never calls `validateFrontmatter`. A caller can write a file with `{ skill: 'test' }` and no `type`, `layer`, `version`, `generator`, or `tags` — producing an invalid interchange file that will fail validation when read by other skills. The "CRITICAL" fields (per validate.js) are silently omitted.

**Fix:** Either validate in `writeMd` and throw on invalid meta, or document that callers must validate themselves. The current API is a footgun.

---

## HIGH

### 4. readMd regex fails on edge cases (io.js)

```js
const match = raw.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
```

- **Windows line endings (`\r\n`):** Regex won't match, returning `{ meta: {}, content: raw }` silently. Any file edited on Windows or by a tool that uses CRLF breaks the parser with zero error.
- **Empty frontmatter (`---\n\n---\n`):** The `\n` between `---` markers requires at least one char in the capture group due to `*?` being non-greedy — actually `[\s\S]*?` matches empty string, so this works. But `\n---` requires a newline before the closing `---`, so `---\n---\n` (no content between) fails.
- **Trailing whitespace after `---`:** `---  \n` won't match.

**Fix:** Normalize `\r\n` → `\n` before matching. Make the regex more lenient: `/^---[ \t]*\n([\s\S]*?)\n---[ \t]*\n?([\s\S]*)$/`.

### 5. `atomicWrite` rename is NOT atomic across filesystems (io.js)

`fs.renameSync` fails with `EXDEV` if tmp and target are on different filesystems. Since `tmp` is in the same directory as `filePath`, this is fine in practice. But if someone mounts `INTERCHANGE_ROOT` on a different filesystem from `/tmp`, and a future refactor puts temps in `/tmp`, it would silently break. Add a comment documenting this assumption.

### 6. indexer.js doesn't use locking (indexer.js)

`updateIndex` and `rebuildIndex` call `atomicWrite` directly without `acquireLock`. If two skills rebuild their index simultaneously, the master index write could race. The atomic write prevents corruption but you could lose one update (last-writer-wins with stale data).

**Fix:** Use `writeMd` or at least `acquireLock` around the master index write.

### 7. `isProcessAlive` false positives (lock.js)

`process.kill(pid, 0)` returns true if the PID exists but belongs to a **different process** (PID reuse). On macOS/Linux, PIDs can be reused quickly under high process churn. The lock would appear held by a live process when the original holder is long dead.

**Fix:** Also store and check process start time, or accept this as a known limitation with documentation.

---

## MEDIUM

### 8. Date serialization non-determinism (serialize.js)

`js-yaml` serializes `Date` objects using their native `toISOString()`. If a field contains a Date object, `sortObject` skips it (`instanceof Date` check), so js-yaml handles it — but the output depends on the Date's milliseconds. If `updated` is set to `new Date().toISOString()` (a string), this is fine. But if someone passes a `Date` object, js-yaml may format it differently than the string representation. The `sortObject` Date check prevents deep-sorting, but doesn't ensure deterministic serialization.

**Fix:** Coerce all Date values to ISO strings in `serializeFrontmatter` before passing to `yaml.dump`.

### 9. `serializeTable` doesn't escape pipe characters (serialize.js)

If any cell value contains `|`, the table structure breaks. This is a markdown table — pipes in content need to be escaped as `\|`.

**Fix:** `const pad = (s, w) => String(s).replace(/\|/g, '\\|').padEnd(w);` (but then width calculation needs to account for escaped length).

### 10. CircuitBreaker `lastResult` memory leak (circuit-breaker.js)

`lastResult` holds a reference to the last successful result forever. If results are large objects (e.g., API responses), this prevents GC. No way to clear it.

**Fix:** Add a `reset()` method and/or allow configuring whether to cache stale results.

### 11. CircuitBreaker HALF_OPEN allows unlimited concurrent probes (circuit-breaker.js)

When transitioning from OPEN → HALF_OPEN, every concurrent caller gets through to the actual function. Standard circuit breaker pattern allows only ONE probe request in HALF_OPEN.

**Fix:** Add a flag like `this._probing = true` to gate HALF_OPEN to a single caller.

### 12. `backoffMs` is unused internally (circuit-breaker.js)

The `backoffMs` method exists but `call()` never uses it. There's no retry logic in the breaker — callers must implement their own. This is either a missing feature or a misleading API.

**Fix:** Either integrate retry logic into `call()` or document that `backoffMs` is a helper for callers to use externally.

### 13. No path traversal protection (io.js, indexer.js)

`writeMd`, `atomicWrite`, `readMd` accept arbitrary paths. A skill could write to `../../etc/passwd` or any location. While skills are trusted code, defense-in-depth would restrict writes to within `INTERCHANGE_ROOT`.

**Fix:** Add an optional `assertWithinRoot(filePath)` guard to `writeMd`.

### 14. `reconcileDbToInterchange` doesn't check `updated` timestamps (reconcile.js)

The function compares hashes but ignores `updated` field from the file objects, even though the type signature includes it. If the DB and file have the same hash but different timestamps, it's reported as "unchanged" — but the metadata is actually drifted.

---

## LOW

### 15. `relativeTime` says "0 seconds ago" for just-now times (helpers.js)

When `seconds < 60` and `seconds === 0`, output is "0 seconds ago". Consider returning "just now".

### 16. `slugify` strips non-ASCII entirely (helpers.js)

`/[^a-z0-9]+/g` removes all non-Latin characters. A CRM contact named "José García" becomes "jos-garc-a". Consider using a transliteration library or at least documenting this limitation.

### 17. `nextGenerationId` is unused by writeMd (generation.js)

`writeMd` implements its own generation logic inline. `nextGenerationId` exists as a public API but is redundant — it reads the file a second time without locking. If someone calls `nextGenerationId` then `writeMd`, they'd get a stale generation number.

**Fix:** Either remove `nextGenerationId` from public API or document it as read-only/informational.

### 18. No `engines` field in package.json

The code uses `??`, optional chaining, and other modern syntax. Should specify `"engines": { "node": ">=18" }`.

### 19. `formatTable` re-export creates a confusing dual export (helpers.js / index.js)

`formatTable` is exported from both `helpers.js` (as re-export of `serializeTable`) and from `index.js` directly as `serializeTable`. Users might import both, not realizing they're the same function. Pick one name.

### 20. Test coverage gaps

Missing tests for:
- `readMd` with no frontmatter (plain markdown)
- `readMd` with malformed YAML (should it throw or return empty meta?)
- `writeMd` with `force: true` 
- `updateIndex` / `rebuildIndex` / `listInterchange` — zero test coverage
- `validateLayer` with Windows-style paths
- `nextGenerationId` on missing file
- `slugify` with empty string, unicode, special characters
- `relativeTime` with future dates
- `isStale` with missing `ttl` field (returns false — is this correct?)
- Concurrent lock contention with stale lock cleanup
- `atomicWrite` cleanup on write failure

---

## Summary

| Severity | Count |
|----------|-------|
| CRITICAL | 3     |
| HIGH     | 4     |
| MEDIUM   | 7     |
| LOW      | 6     |

**Bottom line:** The core design is sound — atomic write protocol, deterministic serialization, and the idempotency contract are well-conceived. The main risks are: (1) the lock mechanism has race conditions in stale cleanup that could break mutual exclusion, (2) `writeMd` doesn't enforce its own validation schema, and (3) the regex parser is fragile with CRLF. Fix the 3 CRITICALs and the indexer locking gap before any skill goes to production.


path: README.md
content: # @openclaw/interchange

[![Tests](https://img.shields.io/badge/tests-32%20passing-brightgreen)]() [![Node](https://img.shields.io/badge/node-%3E%3D18-blue)]() [![License: MIT](https://img.shields.io/badge/license-MIT-yellow)]()

> The shared .md interchange library that every OpenClaw skill builds on.

Interchange provides atomic file I/O, deterministic serialization, YAML frontmatter parsing, advisory locking, and schema validation for `.md` interchange files. It's the foundation layer — ensuring that every skill speaks the same language and every write is crash-safe.

## Features

- **Atomic writes** — tmp + fsync + rename protocol prevents partial writes
- **Deterministic serialization** — same input always produces byte-identical output
- **YAML frontmatter** — read/write structured metadata in `.md` files
- **Advisory locking** — prevent concurrent writes from multiple agents
- **Schema validation** — validate frontmatter and interchange layers
- **Content hashing & generations** — track changes with generation IDs and content hashes
- **Staleness detection** — check if interchange files need regeneration
- **DB-to-interchange reconciliation** — sync database state to `.md` files
- **Circuit breaker** — resilient I/O with automatic failure detection
- **Helpers** — slugify, table formatting, currency formatting, relative time

## Quick Start

```bash
cd skills/interchange
npm install
```

```js
import { readMd, writeMd, acquireLock, releaseLock, validateFrontmatter } from '@openclaw/interchange';

// Read a .md interchange file
const doc = readMd('path/to/file.md');
console.log(doc.frontmatter, doc.body);

// Atomic write with frontmatter
writeMd('path/to/output.md', {
  frontmatter: { type: 'report', generated: new Date().toISOString() },
  body: '## Summary\nAll systems operational.'
});

// Advisory locking
const lock = acquireLock('path/to/file.md');
try {
  // ... safe writes ...
} finally {
  releaseLock(lock);
}
```

## API Reference

| Export | Description |
|--------|-------------|
| `readMd(path)` | Parse `.md` file into `{ frontmatter, body }` |
| `writeMd(path, doc)` | Atomic write with YAML frontmatter |
| `atomicWrite(path, content)` | Raw atomic write (tmp → fsync → rename) |
| `acquireLock(path)` / `releaseLock(lock)` | Advisory file locking |
| `validateFrontmatter(data, schema)` | Validate frontmatter against a schema |
| `validateLayer(layer)` | Validate interchange layer structure |
| `nextGenerationId()` | Generate a new generation ID |
| `contentHash(content)` | SHA-256 content hash |
| `serializeFrontmatter(obj)` | Deterministic YAML serialization |
| `serializeTable(rows)` | Deterministic markdown table |
| `updateIndex(dir)` / `rebuildIndex(dir)` | Maintain interchange indexes |
| `listInterchange(dir)` | List all interchange files in a directory |
| `isStale(path, db)` | Check if a file needs regeneration |
| `reconcileDbToInterchange(db, dir)` | Sync DB state → `.md` files |
| `CircuitBreaker` | Resilient I/O wrapper |
| `slugify(str)` | URL-safe slug from string |
| `formatTable(rows)` / `formatCurrency(n)` / `relativeTime(date)` | Formatting helpers |

## Architecture

Pure JavaScript library (no database, no CLI). Exports functions that other skills import to read, write, and validate `.md` interchange files. All I/O follows the atomic write protocol to prevent corruption.

## Testing

```bash
npm test
```

32 tests covering atomic writes, deterministic serialization, frontmatter parsing, locking, schema validation, indexing, staleness detection, reconciliation, and circuit breaker behavior.

## Part of the OpenClaw Ecosystem

This is the foundation package. Every other OpenClaw skill (`orchestration`, `monitoring`, `crm`, `ecommerce`, `voice`) depends on `@openclaw/interchange` for reading and writing `.md` interchange files.

## License

MIT


path: ecommerce/ops/capabilities.md
content: # E-commerce Capabilities

## Commands
- `ecom watch add <name> --url <url> [--target <price>]`
- `ecom watch list`
- `ecom watch remove <id>`
- `ecom price history <id>`
- `ecom price update <id> <price>`
- `ecom order add <name> [--tracking <num>]`
- `ecom order list [--status <status>]`
- `ecom order update <id> [--status 
] [--tracking <n>]`
- `ecom margin --cost <c> --sell 
 [--fees <pct>]`
- `ecom alert list [--pending]`
- `ecom alert ack <id>`
- `ecom refresh`


path: ecommerce/state/summary.md
content: # E-commerce State

## Watchlist
- Products tracked: 1
- Below target price: 0

## Orders
- Active: 1
- Shipped: 0

## Alerts
- Pending: 0


path: package-lock.json
content: {
  "name": "@openclaw/interchange",
  "version": "1.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "@openclaw/interchange",
      "version": "1.0.0",
      "license": "MIT",
      "dependencies": {
        "js-yaml": "^4.1.0"
      },
      "devDependencies": {
        "vitest": "^3.0.0"
      }
    },
    "node_modules/@esbuild/aix-ppc64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "ppc64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "aix"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/android-arm": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "android"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/android-arm64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "android"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/android-x64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "android"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/darwin-arm64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "darwin"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/darwin-x64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "darwin"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/freebsd-arm64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "freebsd"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/freebsd-x64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "freebsd"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/linux-arm": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/linux-arm64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/linux-ia32": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "ia32"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/linux-loong64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "loong64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/linux-mips64el": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "mips64el"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/linux-ppc64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "ppc64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/linux-riscv64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "riscv64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/linux-s390x": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "s390x"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/linux-x64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/netbsd-arm64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "netbsd"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/netbsd-x64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "netbsd"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/openbsd-arm64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "openbsd"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/openbsd-x64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "openbsd"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/openharmony-arm64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "openharmony"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/sunos-x64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "sunos"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/win32-arm64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "win32"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/win32-ia32": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "ia32"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "win32"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/win32-x64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "win32"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@jridgewell/sourcemap-codec": {
      "version": "1.5.5",
      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/@rollup/rollup-android-arm-eabi": {
      "version": "4.57.1",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "android"
      ]
    },
    "node_modules/@rollup/rollup-android-arm64": {
      "version": "4.57.1",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "android"
      ]
    },
    "node_modules/@rollup/rollup-darwin-arm64": {
      "version": "4.57.1",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "darwin"
      ]
    },
    "node_modules/@rollup/rollup-darwin-x64": {
      "version": "4.57.1",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "darwin"
      ]
    },
    "node_modules/@rollup/rollup-freebsd-arm64": {
      "version": "4.57.1",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "freebsd"
      ]
    },
    "node_modules/@rollup/rollup-freebsd-x64": {
      "version": "4.57.1",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "freebsd"
      ]
    },
    "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
      "version": "4.57.1",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ]
    },
    "node_modules/@rollup/rollup-linux-arm-musleabihf": {
      "version": "4.57.1",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ]
    },
    "node_modules/@rollup/rollup-linux-arm64-gnu": {
      "version": "4.57.1",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ]
    },
    "node_modules/@rollup/rollup-linux-arm64-musl": {
      "version": "4.57.1",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ]
    },
    "node_modules/@rollup/rollup-linux-loong64-gnu": {
      "version": "4.57.1",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "loong64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ]
    },
    "node_modules/@rollup/rollup-linux-loong64-musl": {
      "version": "4.57.1",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "loong64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ]
    },
    "node_modules/@rollup/rollup-linux-ppc64-gnu": {
      "version": "4.57.1",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "ppc64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ]
    },
    "node_modules/@rollup/rollup-linux-ppc64-musl": {
      "version": "4.57.1",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "ppc64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ]
    },
    "node_modules/@rollup/rollup-linux-riscv64-gnu": {
      "version": "4.57.1",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "riscv64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ]
    },
    "node_modules/@rollup/rollup-linux-riscv64-musl": {
      "version": "4.57.1",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "riscv64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ]
    },
    "node_modules/@rollup/rollup-linux-s390x-gnu": {
      "version": "4.57.1",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "s390x"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ]
    },
    "node_modules/@rollup/rollup-linux-x64-gnu": {
      "version": "4.57.1",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ]
    },
    "node_modules/@rollup/rollup-linux-x64-musl": {
      "version": "4.57.1",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ]
    },
    "node_modules/@rollup/rollup-openbsd-x64": {
      "version": "4.57.1",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "openbsd"
      ]
    },
    "node_modules/@rollup/rollup-openharmony-arm64": {
      "version": "4.57.1",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "openharmony"
      ]
    },
    "node_modules/@rollup/rollup-win32-arm64-msvc": {
      "version": "4.57.1",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "win32"
      ]
    },
    "node_modules/@rollup/rollup-win32-ia32-msvc": {
      "version": "4.57.1",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "ia32"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "win32"
      ]
    },
    "node_modules/@rollup/rollup-win32-x64-gnu": {
      "version": "4.57.1",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "win32"
      ]
    },
    "node_modules/@rollup/rollup-win32-x64-msvc": {
      "version": "4.57.1",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "win32"
      ]
    },
    "node_modules/@types/chai": {
      "version": "5.2.3",
      "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@types/deep-eql": "*",
        "assertion-error": "^2.0.1"
      }
    },
    "node_modules/@types/deep-eql": {
      "version": "4.0.2",
      "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/@types/estree": {
      "version": "1.0.8",
      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/@vitest/expect": {
      "version": "3.2.4",
      "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@types/chai": "^5.2.2",
        "@vitest/spy": "3.2.4",
        "@vitest/utils": "3.2.4",
        "chai": "^5.2.0",
        "tinyrainbow": "^2.0.0"
      },
      "funding": {
        "url": "https://opencollective.com/vitest"
      }
    },
    "node_modules/@vitest/mocker": {
      "version": "3.2.4",
      "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@vitest/spy": "3.2.4",
        "estree-walker": "^3.0.3",
        "magic-string": "^0.30.17"
      },
      "funding": {
        "url": "https://opencollective.com/vitest"
      },
      "peerDependencies": {
        "msw": "^2.4.9",
        "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
      },
      "peerDependenciesMeta": {
        "msw": {
          "optional": true
        },
        "vite": {
          "optional": true
        }
      }
    },
    "node_modules/@vitest/pretty-format": {
      "version": "3.2.4",
      "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "tinyrainbow": "^2.0.0"
      },
      "funding": {
        "url": "https://opencollective.com/vitest"
      }
    },
    "node_modules/@vitest/runner": {
      "version": "3.2.4",
      "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@vitest/utils": "3.2.4",
        "pathe": "^2.0.3",
        "strip-literal": "^3.0.0"
      },
      "funding": {
        "url": "https://opencollective.com/vitest"
      }
    },
    "node_modules/@vitest/snapshot": {
      "version": "3.2.4",
      "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@vitest/pretty-format": "3.2.4",
        "magic-string": "^0.30.17",
        "pathe": "^2.0.3"
      },
      "funding": {
        "url": "https://opencollective.com/vitest"
      }
    },
    "node_modules/@vitest/spy": {
      "version": "3.2.4",
      "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "tinyspy": "^4.0.3"
      },
      "funding": {
        "url": "https://opencollective.com/vitest"
      }
    },
    "node_modules/@vitest/utils": {
      "version": "3.2.4",
      "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@vitest/pretty-format": "3.2.4",
        "loupe": "^3.1.4",
        "tinyrainbow": "^2.0.0"
      },
      "funding": {
        "url": "https://opencollective.com/vitest"
      }
    },
    "node_modules/argparse": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Python-2.0"
    },
    "node_modules/assertion-error": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/cac": {
      "version": "6.7.14",
      "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/chai": {
      "version": "5.3.3",
      "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "assertion-error": "^2.0.1",
        "check-error": "^2.1.1",
        "deep-eql": "^5.0.1",
        "loupe": "^3.1.0",
        "pathval": "^2.0.0"
      },
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/check-error": {
      "version": "2.1.3",
      "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">= 16"
      }
    },
    "node_modules/debug": {
      "version": "4.4.3",
      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "ms": "^2.1.3"
      },
      "engines": {
        "node": ">=6.0"
      },
      "peerDependenciesMeta": {
        "supports-color": {
          "optional": true
        }
      }
    },
    "node_modules/deep-eql": {
      "version": "5.0.2",
      "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=6"
      }
    },
    "node_modules/es-module-lexer": {
      "version": "1.7.0",
      "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/esbuild": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "hasInstallScript": true,
      "license": "MIT",
      "bin": {
        "esbuild": "bin/esbuild"
      },
      "engines": {
        "node": ">=18"
      },
      "optionalDependencies": {
        "@esbuild/aix-ppc64": "0.27.3",
        "@esbuild/android-arm": "0.27.3",
        "@esbuild/android-arm64": "0.27.3",
        "@esbuild/android-x64": "0.27.3",
        "@esbuild/darwin-arm64": "0.27.3",
        "@esbuild/darwin-x64": "0.27.3",
        "@esbuild/freebsd-arm64": "0.27.3",
        "@esbuild/freebsd-x64": "0.27.3",
        "@esbuild/linux-arm": "0.27.3",
        "@esbuild/linux-arm64": "0.27.3",
        "@esbuild/linux-ia32": "0.27.3",
        "@esbuild/linux-loong64": "0.27.3",
        "@esbuild/linux-mips64el": "0.27.3",
        "@esbuild/linux-ppc64": "0.27.3",
        "@esbuild/linux-riscv64": "0.27.3",
        "@esbuild/linux-s390x": "0.27.3",
        "@esbuild/linux-x64": "0.27.3",
        "@esbuild/netbsd-arm64": "0.27.3",
        "@esbuild/netbsd-x64": "0.27.3",
        "@esbuild/openbsd-arm64": "0.27.3",
        "@esbuild/openbsd-x64": "0.27.3",
        "@esbuild/openharmony-arm64": "0.27.3",
        "@esbuild/sunos-x64": "0.27.3",
        "@esbuild/win32-arm64": "0.27.3",
        "@esbuild/win32-ia32": "0.27.3",
        "@esbuild/win32-x64": "0.27.3"
      }
    },
    "node_modules/estree-walker": {
      "version": "3.0.3",
      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@types/estree": "^1.0.0"
      }
    },
    "node_modules/expect-type": {
      "version": "1.3.0",
      "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "Apache-2.0",
      "engines": {
        "node": ">=12.0.0"
      }
    },
    "node_modules/fdir": {
      "version": "6.5.0",
      "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=12.0.0"
      },
      "peerDependencies": {
        "picomatch": "^3 || ^4"
      },
      "peerDependenciesMeta": {
        "picomatch": {
          "optional": true
        }
      }
    },
    "node_modules/fsevents": {
      "version": "2.3.3",
      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "hasInstallScript": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "darwin"
      ],
      "engines": {
        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
      }
    },
    "node_modules/js-tokens": {
      "version": "9.0.1",
      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/js-yaml": {
      "version": "4.1.1",
      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "argparse": "^2.0.1"
      },
      "bin": {
        "js-yaml": "bin/js-yaml.js"
      }
    },
    "node_modules/loupe": {
      "version": "3.2.1",
      "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/magic-string": {
      "version": "0.30.21",
      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@jridgewell/sourcemap-codec": "^1.5.5"
      }
    },
    "node_modules/ms": {
      "version": "2.1.3",
      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/nanoid": {
      "version": "3.3.11",
      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/ai"
        }
      ],
      "license": "MIT",
      "bin": {
        "nanoid": "bin/nanoid.cjs"
      },
      "engines": {
        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
      }
    },
    "node_modules/pathe": {
      "version": "2.0.3",
      "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/pathval": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">= 14.16"
      }
    },
    "node_modules/picocolors": {
      "version": "1.1.1",
      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "ISC"
    },
    "node_modules/picomatch": {
      "version": "4.0.3",
      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=12"
      },
      "funding": {
        "url": "https://github.com/sponsors/jonschlinkert"
      }
    },
    "node_modules/postcss": {
      "version": "8.5.6",
      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "funding": [
        {
          "type": "opencollective",
          "url": "https://opencollective.com/postcss/"
        },
        {
          "type": "tidelift",
          "url": "https://tidelift.com/funding/github/npm/postcss"
        },
        {
          "type": "github",
          "url": "https://github.com/sponsors/ai"
        }
      ],
      "license": "MIT",
      "dependencies": {
        "nanoid": "^3.3.11",
        "picocolors": "^1.1.1",
        "source-map-js": "^1.2.1"
      },
      "engines": {
        "node": "^10 || ^12 || >=14"
      }
    },
    "node_modules/rollup": {
      "version": "4.57.1",
      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@types/estree": "1.0.8"
      },
      "bin": {
        "rollup": "dist/bin/rollup"
      },
      "engines": {
        "node": ">=18.0.0",
        "npm": ">=8.0.0"
      },
      "optionalDependencies": {
        "@rollup/rollup-android-arm-eabi": "4.57.1",
        "@rollup/rollup-android-arm64": "4.57.1",
        "@rollup/rollup-darwin-arm64": "4.57.1",
        "@rollup/rollup-darwin-x64": "4.57.1",
        "@rollup/rollup-freebsd-arm64": "4.57.1",
        "@rollup/rollup-freebsd-x64": "4.57.1",
        "@rollup/rollup-linux-arm-gnueabihf": "4.57.1",
        "@rollup/rollup-linux-arm-musleabihf": "4.57.1",
        "@rollup/rollup-linux-arm64-gnu": "4.57.1",
        "@rollup/rollup-linux-arm64-musl": "4.57.1",
        "@rollup/rollup-linux-loong64-gnu": "4.57.1",
        "@rollup/rollup-linux-loong64-musl": "4.57.1",
        "@rollup/rollup-linux-ppc64-gnu": "4.57.1",
        "@rollup/rollup-linux-ppc64-musl": "4.57.1",
        "@rollup/rollup-linux-riscv64-gnu": "4.57.1",
        "@rollup/rollup-linux-riscv64-musl": "4.57.1",
        "@rollup/rollup-linux-s390x-gnu": "4.57.1",
        "@rollup/rollup-linux-x64-gnu": "4.57.1",
        "@rollup/rollup-linux-x64-musl": "4.57.1",
        "@rollup/rollup-openbsd-x64": "4.57.1",
        "@rollup/rollup-openharmony-arm64": "4.57.1",
        "@rollup/rollup-win32-arm64-msvc": "4.57.1",
        "@rollup/rollup-win32-ia32-msvc": "4.57.1",
        "@rollup/rollup-win32-x64-gnu": "4.57.1",
        "@rollup/rollup-win32-x64-msvc": "4.57.1",
        "fsevents": "~2.3.2"
      }
    },
    "node_modules/siginfo": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "ISC"
    },
    "node_modules/source-map-js": {
      "version": "1.2.1",
      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "BSD-3-Clause",
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/stackback": {
      "version": "0.0.2",
      "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/std-env": {
      "version": "3.10.0",
      "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/strip-literal": {
      "version": "3.1.0",
      "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "js-tokens": "^9.0.1"
      },
      "funding": {
        "url": "https://github.com/sponsors/antfu"
      }
    },
    "node_modules/tinybench": {
      "version": "2.9.0",
      "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/tinyexec": {
      "version": "0.3.2",
      "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/tinyglobby": {
      "version": "0.2.15",
      "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "fdir": "^6.5.0",
        "picomatch": "^4.0.3"
      },
      "engines": {
        "node": ">=12.0.0"
      },
      "funding": {
        "url": "https://github.com/sponsors/SuperchupuDev"
      }
    },
    "node_modules/tinypool": {
      "version": "1.1.1",
      "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": "^18.0.0 || >=20.0.0"
      }
    },
    "node_modules/tinyrainbow": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=14.0.0"
      }
    },
    "node_modules/tinyspy": {
      "version": "4.0.4",
      "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=14.0.0"
      }
    },
    "node_modules/vite": {
      "version": "7.3.1",
      "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "esbuild": "^0.27.0",
        "fdir": "^6.5.0",
        "picomatch": "^4.0.3",
        "postcss": "^8.5.6",
        "rollup": "^4.43.0",
        "tinyglobby": "^0.2.15"
      },
      "bin": {
        "vite": "bin/vite.js"
      },
      "engines": {
        "node": "^20.19.0 || >=22.12.0"
      },
      "funding": {
        "url": "https://github.com/vitejs/vite?sponsor=1"
      },
      "optionalDependencies": {
        "fsevents": "~2.3.3"
      },
      "peerDependencies": {
        "@types/node": "^20.19.0 || >=22.12.0",
        "jiti": ">=1.21.0",
        "less": "^4.0.0",
        "lightningcss": "^1.21.0",
        "sass": "^1.70.0",
        "sass-embedded": "^1.70.0",
        "stylus": ">=0.54.8",
        "sugarss": "^5.0.0",
        "terser": "^5.16.0",
        "tsx": "^4.8.1",
        "yaml": "^2.4.2"
      },
      "peerDependenciesMeta": {
        "@types/node": {
          "optional": true
        },
        "jiti": {
          "optional": true
        },
        "less": {
          "optional": true
        },
        "lightningcss": {
          "optional": true
        },
        "sass": {
          "optional": true
        },
        "sass-embedded": {
          "optional": true
        },
        "stylus": {
          "optional": true
        },
        "sugarss": {
          "optional": true
        },
        "terser": {
          "optional": true
        },
        "tsx": {
          "optional": true
        },
        "yaml": {
          "optional": true
        }
      }
    },
    "node_modules/vite-node": {
      "version": "3.2.4",
      "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "cac": "^6.7.14",
        "debug": "^4.4.1",
        "es-module-lexer": "^1.7.0",
        "pathe": "^2.0.3",
        "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
      },
      "bin": {
        "vite-node": "vite-node.mjs"
      },
      "engines": {
        "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
      },
      "funding": {
        "url": "https://opencollective.com/vitest"
      }
    },
    "node_modules/vitest": {
      "version": "3.2.4",
      "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@types/chai": "^5.2.2",
        "@vitest/expect": "3.2.4",
        "@vitest/mocker": "3.2.4",
        "@vitest/pretty-format": "^3.2.4",
        "@vitest/runner": "3.2.4",
        "@vitest/snapshot": "3.2.4",
        "@vitest/spy": "3.2.4",
        "@vitest/utils": "3.2.4",
        "chai": "^5.2.0",
        "debug": "^4.4.1",
        "expect-type": "^1.2.1",
        "magic-string": "^0.30.17",
        "pathe": "^2.0.3",
        "picomatch": "^4.0.2",
        "std-env": "^3.9.0",
        "tinybench": "^2.9.0",
        "tinyexec": "^0.3.2",
        "tinyglobby": "^0.2.14",
        "tinypool": "^1.1.1",
        "tinyrainbow": "^2.0.0",
        "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0",
        "vite-node": "3.2.4",
        "why-is-node-running": "^2.3.0"
      },
      "bin": {
        "vitest": "vitest.mjs"
      },
      "engines": {
        "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
      },
      "funding": {
        "url": "https://opencollective.com/vitest"
      },
      "peerDependencies": {
        "@edge-runtime/vm": "*",
        "@types/debug": "^4.1.12",
        "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
        "@vitest/browser": "3.2.4",
        "@vitest/ui": "3.2.4",
        "happy-dom": "*",
        "jsdom": "*"
      },
      "peerDependenciesMeta": {
        "@edge-runtime/vm": {
          "optional": true
        },
        "@types/debug": {
          "optional": true
        },
        "@types/node": {
          "optional": true
        },
        "@vitest/browser": {
          "optional": true
        },
        "@vitest/ui": {
          "optional": true
        },
        "happy-dom": {
          "optional": true
        },
        "jsdom": {
          "optional": true
        }
      }
    },
    "node_modules/why-is-node-running": {
      "version": "2.3.0",
      "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "siginfo": "^2.0.0",
        "stackback": "0.0.2"
      },
      "bin": {
        "why-is-node-running": "cli.js"
      },
      "engines": {
        "node": ">=8"
      }
    }
  }
}


path: package.json
content: {
  "name": "@openclaw/interchange",
  "version": "1.0.0",
  "description": "Shared .md interchange library for OpenClaw skills — atomic writes, deterministic serialization, schema validation",
  "type": "module",
  "exports": {
    ".": "./src/index.js"
  },
  "main": "./src/index.js",
  "scripts": {
    "test": "vitest run",
    "test:watch": "vitest"
  },
  "dependencies": {
    "js-yaml": "^4.1.0"
  },
  "devDependencies": {
    "vitest": "^3.0.0"
  },
  "engines": {
    "node": ">=18"
  },
  "license": "MIT"
}


path: src/circuit-breaker.js
content: /**
 * Circuit breaker for external API calls.
 * States: CLOSED (normal), OPEN (failing, serve stale), HALF_OPEN (testing recovery).
 */
export class CircuitBreaker {
  /**
   * @param {Object} [opts]
   * @param {number} [opts.threshold=5] - Consecutive failures before opening
   * @param {number} [opts.cooldownMs=300000] - Cooldown in ms (default 5min)
   * @param {number} [opts.maxBackoffMs=60000] - Max backoff for retries
   */
  constructor(opts = {}) {
    this.threshold = opts.threshold ?? 5;
    this.cooldownMs = opts.cooldownMs ?? 300_000;
    this.maxBackoffMs = opts.maxBackoffMs ?? 60_000;
    this.state = 'CLOSED';
    this.failures = 0;
    this.lastFailure = 0;
    this.lastResult = undefined;
  }

  /**
   * Execute a function through the circuit breaker.
   * @template T
   * @param {() => Promise<T>} fn - The function to call
   * @returns {Promise<T>}
   * @throws {Error} If circuit is open and no stale data available
   */
  async call(fn) {
    if (this.state === 'OPEN') {
      if (Date.now() - this.lastFailure > this.cooldownMs) {
        this.state = 'HALF_OPEN';
      } else {
        if (this.lastResult !== undefined) return this.lastResult;
        throw new Error('Circuit breaker is OPEN and no stale data available');
      }
    }

    try {
      const result = await fn();
      this.lastResult = result;
      this.failures = 0;
      this.state = 'CLOSED';
      return result;
    } catch (err) {
      this.failures++;
      this.lastFailure = Date.now();
      if (this.failures >= this.threshold) {
        this.state = 'OPEN';
      }
      if (this.lastResult !== undefined) return this.lastResult;
      throw err;
    }
  }

  /**
   * Calculate backoff with jitter for retry delay.
   * @param {number} attempt - Attempt number (0-based)
   * @returns {number} Delay in ms
   */
  backoffMs(attempt) {
    const base = Math.min(1000 * Math.pow(2, attempt), this.maxBackoffMs);
    return base + Math.random() * base * 0.1;
  }
}


path: src/generation.js
content: import crypto from 'node:crypto';
import { readMd } from './io.js';

/**
 * Compute SHA-256 hash of content body.
 * @param {string} content - Content to hash
 * @returns {string} Hash prefixed with "sha256:"
 */
export function contentHash(content) {
  const hash = crypto.createHash('sha256').update(content, 'utf8').digest('hex');
  return `sha256:${hash}`;
}

/**
 * Read the current generation_id from an existing file and return the next one.
 * If file doesn't exist, returns 1.
 * @param {string} filePath - Path to the .md file
 * @returns {number} Next generation ID
 */
export function nextGenerationId(filePath) {
  try {
    const { meta } = readMd(filePath);
    return (meta.generation_id || 0) + 1;
  } catch {
    return 1;
  }
}


path: src/helpers.js
content: /**
 * Convert a name to a URL/filesystem-safe slug.
 * @param {string} name
 * @returns {string}
 */
export function slugify(name) {
  return String(name)
    .toLowerCase()
    .trim()
    .replace(/[^a-z0-9]+/g, '-')
    .replace(/^-|-$/g, '');
}

/**
 * Format a markdown table. Re-exports serializeTable for convenience.
 * @param {string[]} headers
 * @param {string[][]} rows
 * @returns {string}
 */
export { serializeTable as formatTable } from './serialize.js';

/**
 * Format a number as USD currency.
 * @param {number} amount
 * @returns {string}
 */
export function formatCurrency(amount) {
  return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(amount);
}

/**
 * Return a human-readable relative time string.
 * @param {Date|string|number} date
 * @returns {string}
 */
export function relativeTime(date) {
  const now = Date.now();
  const then = new Date(date).getTime();
  const diffMs = now - then;
  const absDiff = Math.abs(diffMs);
  const future = diffMs < 0;

  const seconds = Math.floor(absDiff / 1000);
  const minutes = Math.floor(seconds / 60);
  const hours = Math.floor(minutes / 60);
  const days = Math.floor(hours / 24);

  let str;
  if (seconds < 60) str = `${seconds} seconds`;
  else if (minutes < 60) str = `${minutes} minute${minutes !== 1 ? 's' : ''}`;
  else if (hours < 24) str = `${hours} hour${hours !== 1 ? 's' : ''}`;
  else str = `${days} day${days !== 1 ? 's' : ''}`;

  return future ? `in ${str}` : `${str} ago`;
}


path: src/index.js
content: /**
 * @openclaw/interchange — Shared .md interchange library
 *
 * ALL writes use atomic protocol: tmp + fsync + rename
 * ALL serialization is deterministic: same input = byte-identical output
 */

export { readMd, writeMd, atomicWrite } from './io.js';
export { acquireLock, releaseLock } from './lock.js';
export { validateFrontmatter, validateLayer } from './validate.js';
export { nextGenerationId, contentHash } from './generation.js';
export { serializeFrontmatter, serializeTable } from './serialize.js';
export { updateIndex, rebuildIndex, listInterchange } from './indexer.js';
export { isStale } from './stale.js';
export { reconcileDbToInterchange } from './reconcile.js';
export { CircuitBreaker } from './circuit-breaker.js';
export { slugify, formatTable, formatCurrency, relativeTime } from './helpers.js';


path: src/indexer.js
content: import fs from 'node:fs';
import path from 'node:path';
import { atomicWrite, readMd } from './io.js';
import { acquireLock, releaseLock } from './lock.js';
import { serializeFrontmatter, serializeTable } from './serialize.js';
import { contentHash } from './generation.js';

const INTERCHANGE_ROOT = path.resolve(process.env.INTERCHANGE_ROOT || path.join(process.env.HOME || '/tmp', '.openclaw', 'workspace', 'interchange'));

/**
 * Update the _index.md for a skill atomically.
 * @param {string} skillName
 * @param {{ path: string, type: string, layer: string, updated: string }[]} files
 */
export async function updateIndex(skillName, files) {
  const headers = ['File', 'Type', 'Layer', 'Updated'];
  const rows = files
    .sort((a, b) => a.path.localeCompare(b.path))
    .map(f => [f.path, f.type, f.layer, f.updated]);
  const table = serializeTable(headers, rows);
  const content = `# ${skillName} Index\n\n${table}\n`;
  const hash = contentHash(content);

  const indexPath = path.join(INTERCHANGE_ROOT, skillName, '_index.md');
  const lock = await acquireLock(indexPath);
  try {
    let gen = 1;
    try {
      const existing = readMd(indexPath);
      gen = (existing.meta.generation_id || 0);
      if (existing.meta.content_hash === hash) return; // idempotent
      gen++;
    } catch {}

    const meta = {
      skill: skillName,
      type: 'summary',
      layer: 'ops',
      updated: new Date().toISOString(),
      version: 1,
      generation_id: gen,
      content_hash: hash,
      generator: '@openclaw/interchange',
      tags: ['index'],
    };

    const yamlStr = serializeFrontmatter(meta);
    const data = `---\n${yamlStr}\n---\n${content}`;
    atomicWrite(indexPath, data);
  } finally {
    releaseLock(lock);
  }
}

/**
 * List interchange files, optionally filtered by skill and/or layer.
 * @param {string} [skill] - Filter by skill name
 * @param {'ops'|'state'} [layer] - Filter by layer
 * @returns {string[]} Array of file paths
 */
export function listInterchange(skill, layer) {
  const root = skill ? path.join(INTERCHANGE_ROOT, skill) : INTERCHANGE_ROOT;
  if (!fs.existsSync(root)) return [];
  return walkDir(root).filter(f => {
    if (!f.endsWith('.md')) return false;
    if (layer) {
      const normalized = f.replace(/\\/g, '/');
      if (!normalized.includes(`/${layer}/`)) return false;
    }
    return true;
  });
}

/**
 * Rebuild the master _index.md from disk scan.
 * @param {string} [skill] - If provided, only rebuild that skill's index
 */
export function rebuildIndex(skill) {
  const skills = skill
    ? [skill]
    : fs.existsSync(INTERCHANGE_ROOT)
      ? fs.readdirSync(INTERCHANGE_ROOT, { withFileTypes: true })
          .filter(d => d.isDirectory())
          .map(d => d.name)
      : [];

  for (const s of skills) {
    const files = listInterchange(s).map(f => {
      try {
        const { meta } = readMd(f);
        return { path: path.relative(INTERCHANGE_ROOT, f), type: meta.type || 'unknown', layer: meta.layer || 'unknown', updated: meta.updated || 'unknown' };
      } catch {
        return { path: path.relative(INTERCHANGE_ROOT, f), type: 'unknown', layer: 'unknown', updated: 'unknown' };
      }
    });
    if (files.length > 0) updateIndex(s, files);
  }

  // Master index
  if (!skill) {
    const allSkills = skills.filter(s => fs.existsSync(path.join(INTERCHANGE_ROOT, s, '_index.md')));
    const headers = ['Skill', 'Files', 'Last Updated'];
    const rows = allSkills.sort().map(s => {
      const count = listInterchange(s).length;
      let lastUpdated = 'unknown';
      try {
        const { meta } = readMd(path.join(INTERCHANGE_ROOT, s, '_index.md'));
        lastUpdated = meta.updated || 'unknown';
      } catch {}
      return [s, String(count), lastUpdated];
    });
    const table = serializeTable(headers, rows);
    const content = `# Interchange Master Index\n\n${table}\n`;
    const hash = contentHash(content);

    const masterPath = path.join(INTERCHANGE_ROOT, '_index.md');
    let gen = 1;
    try {
      const existing = readMd(masterPath);
      gen = (existing.meta.generation_id || 0);
      if (existing.meta.content_hash === hash) return;
      gen++;
    } catch {}

    const meta = {
      skill: '_master',
      type: 'summary',
      layer: 'ops',
      updated: new Date().toISOString(),
      version: 1,
      generation_id: gen,
      content_hash: hash,
      generator: '@openclaw/interchange',
      tags: ['index', 'master'],
    };
    const yamlStr = serializeFrontmatter(meta);
    const data = `---\n${yamlStr}\n---\n${content}`;
    atomicWrite(masterPath, data);
  }
}

/**
 * Recursively walk a directory.
 * @param {string} dir
 * @returns {string[]}
 */
function walkDir(dir) {
  const results = [];
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
    const full = path.join(dir, entry.name);
    if (entry.isDirectory()) results.push(...walkDir(full));
    else results.push(full);
  }
  return results;
}


path: src/io.js
content: import fs from 'node:fs';
import path from 'node:path';
import crypto from 'node:crypto';
import yaml from 'js-yaml';
import { acquireLock, releaseLock } from './lock.js';
import { serializeFrontmatter } from './serialize.js';
import { contentHash } from './generation.js';
import { validateFrontmatter } from './validate.js';

/**
 * Atomic write: write to tmp file, fsync, rename to target.
 * @param {string} filePath - Target file path
 * @param {string} data - File content
 */
export function atomicWrite(filePath, data) {
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
  const tmp = `${filePath}.tmp.${process.pid}.${Date.now()}`;
  const fd = fs.openSync(tmp, 'w');
  try {
    fs.writeSync(fd, data);
    fs.fsyncSync(fd);
    fs.closeSync(fd);
    fs.renameSync(tmp, filePath);
  } catch (err) {
    try { fs.closeSync(fd); } catch {}
    try { fs.unlinkSync(tmp); } catch {}
    throw err;
  }
}

/**
 * Read and parse a .md file with YAML frontmatter.
 * @param {string} filePath - Path to the .md file
 * @returns {{ meta: Record<string, any>, content: string, raw: string }}
 */
export function readMd(filePath) {
  const raw = fs.readFileSync(filePath, 'utf8');
  // Normalize CRLF → LF for cross-platform safety
  const normalized = raw.replace(/\r\n/g, '\n');
  const match = normalized.match(/^---[ \t]*\n([\s\S]*?)\n---[ \t]*\n?([\s\S]*)$/);
  if (!match) {
    return { meta: {}, content: normalized, raw };
  }
  const meta = yaml.load(match[1]) || {};
  const content = match[2];
  return { meta, content, raw };
}

/**
 * Write a .md file atomically with frontmatter.
 * Handles locking, generation_id auto-increment, and content_hash.
 *
 * If content hasn't changed (same content_hash), the file is NOT rewritten
 * and generation_id stays the same (idempotency contract).
 *
 * @param {string} filePath - Target file path
 * @param {Record<string, any>} frontmatter - Frontmatter fields
 * @param {string} content - Markdown body content
 * @param {{ force?: boolean, skipValidation?: boolean }} [opts] - Options; force=true skips idempotency check, skipValidation=true skips frontmatter validation
 */
export async function writeMd(filePath, frontmatter, content, opts = {}) {
  const lock = await acquireLock(filePath);
  try {
    const hash = contentHash(content);

    // Read existing file for idempotency check
    let currentGen = 0;
    try {
      const existing = readMd(filePath);
      currentGen = existing.meta.generation_id || 0;
      if (!opts.force && existing.meta.content_hash === hash) {
        return; // No change — idempotent skip
      }
    } catch {
      // File doesn't exist yet
    }

    const meta = {
      ...frontmatter,
      content_hash: hash,
      generation_id: currentGen + 1,
      updated: new Date().toISOString(),
    };

    // Validate frontmatter unless explicitly skipped (e.g., internal/test use)
    if (!opts.skipValidation) {
      const validation = validateFrontmatter(meta);
      if (!validation.valid) {
        throw new Error(`Invalid frontmatter: ${validation.errors.join('; ')}`);
      }
    }

    const yamlStr = serializeFrontmatter(meta);
    const data = `---\n${yamlStr}\n---\n${content}`;
    atomicWrite(filePath, data);
  } finally {
    releaseLock(lock);
  }
}


path: src/lock.js
content: import fs from 'node:fs';
import path from 'node:path';

/**
 * @typedef {Object} Lock
 * @property {number} fd - File descriptor
 * @property {string} lockPath - Path to the lock file
 */

/**
 * Acquire an advisory flock-based lock on a .lock file adjacent to the target path.
 * @param {string} filePath - Path to the file being locked
 * @param {number} [timeout=5000] - Timeout in ms
 * @returns {Promise<Lock>}
 */
export async function acquireLock(filePath, timeout = 5000) {
  const lockPath = filePath + '.lock';
  fs.mkdirSync(path.dirname(lockPath), { recursive: true });

  const start = Date.now();
  let staleCleaned = false;
  while (true) {
    try {
      const fd = fs.openSync(lockPath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL, 0o644);
      fs.writeSync(fd, String(process.pid));
      fs.fsyncSync(fd);
      return { fd, lockPath };
    } catch (err) {
      if (err.code !== 'EEXIST') throw err;

      // Stale lock detection (once per acquire attempt to avoid TOCTOU loop).
      // If the PID in the lock file is dead, attempt to atomically replace it
      // by renaming to a .stale file, then retrying O_EXCL. Only one process
      // wins the rename; the other gets ENOENT on rename and retries normally.
      if (!staleCleaned) {
        staleCleaned = true;
        try {
          const lockPid = parseInt(fs.readFileSync(lockPath, 'utf8').trim(), 10);
          if (lockPid && !isProcessAlive(lockPid)) {
            const stalePath = `${lockPath}.stale.${process.pid}`;
            try {
              fs.renameSync(lockPath, stalePath); // Atomic — only one process wins
              fs.unlinkSync(stalePath);
            } catch {
              // Another process already renamed it — that's fine, retry
            }
            continue; // Retry the O_EXCL open
          }
        } catch {
          // Lock file unreadable or gone — retry the O_EXCL open
          continue;
        }
      }

      if (Date.now() - start > timeout) {
        throw new Error(`Lock timeout: could not acquire lock on ${lockPath} within ${timeout}ms`);
      }
      await new Promise(r => setTimeout(r, 50 + Math.random() * 50));
    }
  }
}

/**
 * Check if a process is still alive.
 * @param {number} pid
 * @returns {boolean}
 */
function isProcessAlive(pid) {
  try {
    process.kill(pid, 0); // Signal 0 = existence check, no actual signal sent
    return true;
  } catch {
    return false;
  }
}

/**
 * Release a previously acquired lock.
 * @param {Lock} lock
 */
export function releaseLock(lock) {
  try { fs.closeSync(lock.fd); } catch {}
  try { fs.unlinkSync(lock.lockPath); } catch {}
}


path: src/reconcile.js
content: /**
 * Detect drift between database state and interchange .md files.
 *
 * @param {Record<string, any>} dbState - Map of entity ID → { hash, updated, ... }
 * @param {{ id: string, content_hash: string, updated: string }[]} files - Parsed interchange files
 * @returns {{ added: string[], removed: string[], changed: string[], unchanged: string[] }}
 */
export function reconcileDbToInterchange(dbState, files) {
  const fileMap = new Map(files.map(f => [f.id, f]));
  const dbIds = new Set(Object.keys(dbState));
  const fileIds = new Set(files.map(f => f.id));

  const added = [...dbIds].filter(id => !fileIds.has(id)).sort();
  const removed = [...fileIds].filter(id => !dbIds.has(id)).sort();
  const changed = [];
  const unchanged = [];

  for (const id of dbIds) {
    if (!fileIds.has(id)) continue;
    const file = fileMap.get(id);
    if (file.content_hash !== dbState[id].hash) {
      changed.push(id);
    } else {
      unchanged.push(id);
    }
  }

  return { added, removed, changed: changed.sort(), unchanged: unchanged.sort() };
}


path: src/serialize.js
content: import yaml from 'js-yaml';

/**
 * Serialize frontmatter to deterministic YAML string.
 * Keys are sorted alphabetically; output is stable across runs.
 * @param {Record<string, any>} meta - Frontmatter object
 * @returns {string} YAML string (without --- delimiters)
 */
export function serializeFrontmatter(meta) {
  const sorted = sortObject(coerceDates(meta));
  return yaml.dump(sorted, {
    sortKeys: true,
    lineWidth: -1,
    noRefs: true,
    quotingType: '"',
    forceQuotes: false,
  }).trimEnd();
}

/**
 * Coerce Date objects to ISO strings for deterministic serialization.
 * @param {any} obj
 * @returns {any}
 */
function coerceDates(obj) {
  if (obj instanceof Date) return obj.toISOString();
  if (Array.isArray(obj)) return obj.map(coerceDates);
  if (obj !== null && typeof obj === 'object') {
    const result = {};
    for (const [k, v] of Object.entries(obj)) {
      result[k] = coerceDates(v);
    }
    return result;
  }
  return obj;
}

/**
 * Deep-sort object keys alphabetically for deterministic output.
 * @param {any} obj
 * @returns {any}
 */
function sortObject(obj) {
  if (Array.isArray(obj)) return obj.map(sortObject);
  if (obj !== null && typeof obj === 'object' && !(obj instanceof Date)) {
    const sorted = {};
    for (const key of Object.keys(obj).sort()) {
      sorted[key] = sortObject(obj[key]);
    }
    return sorted;
  }
  return obj;
}

/**
 * Serialize a markdown table deterministically.
 * @param {string[]} headers - Column headers
 * @param {string[][]} rows - Table rows
 * @returns {string} Markdown table string
 */
export function serializeTable(headers, rows) {
  const cols = headers.length;
  // Calculate column widths
  const widths = headers.map((h, i) => {
    const cellWidths = rows.map(r => String(r[i] ?? '').length);
    return Math.max(String(h).length, ...cellWidths, 3);
  });

  const escape = (s) => String(s).replace(/\|/g, '\\|');
  const pad = (s, w) => escape(s).padEnd(w);
  const headerLine = '| ' + headers.map((h, i) => pad(h, widths[i])).join(' | ') + ' |';
  const sepLine = '| ' + widths.map(w => '-'.repeat(w)).join(' | ') + ' |';
  const dataLines = rows.map(
    row => '| ' + headers.map((_, i) => pad(row[i] ?? '', widths[i])).join(' | ') + ' |'
  );

  return [headerLine, sepLine, ...dataLines].join('\n');
}


path: src/stale.js
content: import { readMd } from './io.js';

/**
 * Check if a file's data is stale based on its TTL frontmatter field.
 * @param {string} filePath - Path to the .md file
 * @returns {boolean} True if stale or unreadable
 */
export function isStale(filePath) {
  try {
    const { meta } = readMd(filePath);
    if (!meta.ttl || !meta.updated) return false;
    const updatedMs = new Date(meta.updated).getTime();
    const ttlMs = meta.ttl * 1000;
    return Date.now() > updatedMs + ttlMs;
  } catch {
    return true;
  }
}


path: src/validate.js
content: /**
 * Required frontmatter fields.
 */
const REQUIRED_FIELDS = [
  'skill', 'type', 'layer', 'updated', 'version',
  'generation_id', 'content_hash', 'generator', 'tags',
];

const VALID_LAYERS = ['ops', 'state'];
const VALID_TYPES = ['summary', 'detail', 'alert'];

/**
 * Validate frontmatter has all required fields with correct types.
 * @param {Record<string, any>} meta
 * @returns {{ valid: boolean, errors: string[] }}
 */
export function validateFrontmatter(meta) {
  const errors = [];

  for (const field of REQUIRED_FIELDS) {
    if (meta[field] === undefined || meta[field] === null) {
      errors.push(`Missing required field: ${field}`);
    }
  }

  if (meta.layer && !VALID_LAYERS.includes(meta.layer)) {
    errors.push(`Invalid layer: ${meta.layer} (must be ops or state)`);
  }
  if (meta.type && !VALID_TYPES.includes(meta.type)) {
    errors.push(`Invalid type: ${meta.type} (must be summary, detail, or alert)`);
  }
  if (meta.tags && !Array.isArray(meta.tags)) {
    errors.push('tags must be an array');
  }
  if (meta.generation_id !== undefined && (typeof meta.generation_id !== 'number' || meta.generation_id < 1)) {
    errors.push('generation_id must be a positive number');
  }
  if (meta.content_hash && !String(meta.content_hash).startsWith('sha256:')) {
    errors.push('content_hash must start with sha256:');
  }

  return { valid: errors.length === 0, errors };
}

/**
 * Validate that a file's path matches its layer field.
 * @param {string} filePath - File path
 * @param {Record<string, any>} meta - Frontmatter
 * @returns {{ valid: boolean, errors: string[] }}
 */
export function validateLayer(filePath, meta) {
  const errors = [];
  if (!meta.layer) {
    errors.push('No layer field in frontmatter');
    return { valid: false, errors };
  }
  const normalized = filePath.replace(/\\/g, '/');
  if (!normalized.includes(`/${meta.layer}/`)) {
    errors.push(`File path "${filePath}" does not contain /${meta.layer}/ but layer is "${meta.layer}"`);
  }
  return { valid: errors.length === 0, errors };
}


path: test/interchange.test.js
content: import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import {
  readMd, writeMd, atomicWrite,
  acquireLock, releaseLock,
  validateFrontmatter, validateLayer,
  contentHash, nextGenerationId,
  serializeFrontmatter, serializeTable,
  isStale,
  reconcileDbToInterchange,
  CircuitBreaker,
  slugify, formatTable, formatCurrency, relativeTime,
} from '../src/index.js';

let tmpDir;

beforeEach(() => {
  tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'interchange-test-'));
});

afterEach(() => {
  fs.rmSync(tmpDir, { recursive: true, force: true });
});

describe('atomicWrite', () => {
  it('writes file atomically', () => {
    const fp = path.join(tmpDir, 'test.md');
    atomicWrite(fp, 'hello world');
    expect(fs.readFileSync(fp, 'utf8')).toBe('hello world');
  });

  it('creates parent directories', () => {
    const fp = path.join(tmpDir, 'a', 'b', 'c.md');
    atomicWrite(fp, 'deep');
    expect(fs.readFileSync(fp, 'utf8')).toBe('deep');
  });

  it('leaves no tmp files on success', () => {
    const fp = path.join(tmpDir, 'clean.md');
    atomicWrite(fp, 'data');
    const files = fs.readdirSync(tmpDir);
    expect(files).toEqual(['clean.md']);
  });
});

const VALID_META = {
  skill: 'test', type: 'summary', layer: 'ops',
  version: 1, generator: 'test@1.0', tags: ['test'],
};

describe('readMd / writeMd', () => {
  it('round-trips frontmatter and content', async () => {
    const fp = path.join(tmpDir, 'doc.md');
    await writeMd(fp, VALID_META, '# Hello\n\nWorld');
    const { meta, content } = readMd(fp);
    expect(meta.skill).toBe('test');
    expect(meta.generation_id).toBe(1);
    expect(meta.content_hash).toMatch(/^sha256:/);
    expect(content).toContain('# Hello');
  });

  it('idempotent: same content does not increment generation_id', async () => {
    const fp = path.join(tmpDir, 'idem.md');
    await writeMd(fp, VALID_META, 'same content');
    const { meta: m1 } = readMd(fp);
    await writeMd(fp, VALID_META, 'same content');
    const { meta: m2 } = readMd(fp);
    expect(m2.generation_id).toBe(m1.generation_id);
  });

  it('increments generation_id on content change', async () => {
    const fp = path.join(tmpDir, 'gen.md');
    await writeMd(fp, VALID_META, 'v1');
    await writeMd(fp, VALID_META, 'v2');
    const { meta } = readMd(fp);
    expect(meta.generation_id).toBe(2);
  });

  it('rejects invalid frontmatter by default', async () => {
    const fp = path.join(tmpDir, 'invalid.md');
    await expect(writeMd(fp, { skill: 'test' }, 'content')).rejects.toThrow(/Invalid frontmatter/);
  });

  it('allows skipValidation for internal use', async () => {
    const fp = path.join(tmpDir, 'skip.md');
    await writeMd(fp, { skill: 'test' }, 'content', { skipValidation: true });
    expect(readMd(fp).meta.skill).toBe('test');
  });

  it('handles CRLF line endings', () => {
    const fp = path.join(tmpDir, 'crlf.md');
    atomicWrite(fp, '---\r\nskill: test\r\n---\r\n# Content\r\n');
    const { meta, content } = readMd(fp);
    expect(meta.skill).toBe('test');
    expect(content).toContain('# Content');
  });
});

describe('locking', () => {
  it('acquire and release', async () => {
    const fp = path.join(tmpDir, 'locktest.md');
    const lock = await acquireLock(fp);
    expect(fs.existsSync(fp + '.lock')).toBe(true);
    releaseLock(lock);
    expect(fs.existsSync(fp + '.lock')).toBe(false);
  });

  it('times out on contended lock', async () => {
    const fp = path.join(tmpDir, 'contend.md');
    const lock1 = await acquireLock(fp);
    await expect(acquireLock(fp, 200)).rejects.toThrow(/Lock timeout/);
    releaseLock(lock1);
  });
});

describe('concurrent writes', () => {
  it('do not corrupt file', async () => {
    const fp = path.join(tmpDir, 'concurrent.md');
    const writes = Array.from({ length: 10 }, (_, i) =>
      writeMd(fp, VALID_META, `content-${i}`, { force: true })
    );
    await Promise.allSettled(writes);
    // File must be parseable
    const { meta, content } = readMd(fp);
    expect(meta.skill).toBe('test');
    expect(content).toMatch(/^content-\d+$/);
  });
});

describe('validateFrontmatter', () => {
  it('passes valid meta', () => {
    const result = validateFrontmatter({
      skill: 'crm', type: 'summary', layer: 'ops',
      updated: '2026-01-01', version: 1, generation_id: 1,
      content_hash: 'sha256:abc', generator: 'crm@1.0', tags: ['a'],
    });
    expect(result.valid).toBe(true);
  });

  it('fails on missing fields', () => {
    const result = validateFrontmatter({});
    expect(result.valid).toBe(false);
    expect(result.errors.length).toBeGreaterThan(0);
  });

  it('rejects invalid layer', () => {
    const result = validateFrontmatter({
      skill: 'x', type: 'summary', layer: 'bad',
      updated: 'now', version: 1, generation_id: 1,
      content_hash: 'sha256:abc', generator: 'x@1', tags: [],
    });
    expect(result.valid).toBe(false);
  });
});

describe('validateLayer', () => {
  it('passes matching path', () => {
    const r = validateLayer('/interchange/crm/ops/file.md', { layer: 'ops' });
    expect(r.valid).toBe(true);
  });
  it('fails mismatched path', () => {
    const r = validateLayer('/interchange/crm/state/file.md', { layer: 'ops' });
    expect(r.valid).toBe(false);
  });
});

describe('contentHash', () => {
  it('is deterministic', () => {
    expect(contentHash('hello')).toBe(contentHash('hello'));
  });
  it('starts with sha256:', () => {
    expect(contentHash('test')).toMatch(/^sha256:[a-f0-9]{64}$/);
  });
});

describe('serializeFrontmatter', () => {
  it('produces sorted keys', () => {
    const a = serializeFrontmatter({ z: 1, a: 2 });
    const b = serializeFrontmatter({ a: 2, z: 1 });
    expect(a).toBe(b);
  });

  it('is deterministic across calls', () => {
    const meta = { skill: 'crm', tags: ['a', 'b'], version: 1 };
    expect(serializeFrontmatter(meta)).toBe(serializeFrontmatter(meta));
  });
});

describe('serializeTable', () => {
  it('produces deterministic output', () => {
    const h = ['Name', 'Value'];
    const r = [['a', '1'], ['b', '2']];
    expect(serializeTable(h, r)).toBe(serializeTable(h, r));
  });

  it('formats correctly', () => {
    const table = serializeTable(['A', 'B'], [['x', 'y']]);
    expect(table).toContain('| A');
    expect(table).toContain('---');
    expect(table).toContain('| x');
  });
});

describe('isStale', () => {
  it('returns true for expired TTL', async () => {
    const fp = path.join(tmpDir, 'stale.md');
    const oldDate = new Date(Date.now() - 10000).toISOString();
    const yaml = `---\nupdated: ${oldDate}\nttl: 1\n---\nstale content`;
    atomicWrite(fp, yaml);
    expect(isStale(fp)).toBe(true);
  });

  it('returns false for fresh file', async () => {
    const fp = path.join(tmpDir, 'fresh.md');
    const now = new Date().toISOString();
    const yaml = `---\nupdated: ${now}\nttl: 9999\n---\nfresh`;
    atomicWrite(fp, yaml);
    expect(isStale(fp)).toBe(false);
  });
});

describe('reconcileDbToInterchange', () => {
  it('detects added, removed, changed, unchanged', () => {
    const db = { a: { hash: 'sha256:1' }, b: { hash: 'sha256:new' }, d: { hash: 'sha256:4' } };
    const files = [
      { id: 'a', content_hash: 'sha256:1' },
      { id: 'b', content_hash: 'sha256:old' },
      { id: 'c', content_hash: 'sha256:3' },
    ];
    const diff = reconcileDbToInterchange(db, files);
    expect(diff.added).toEqual(['d']);
    expect(diff.removed).toEqual(['c']);
    expect(diff.changed).toEqual(['b']);
    expect(diff.unchanged).toEqual(['a']);
  });
});

describe('CircuitBreaker', () => {
  it('opens after threshold failures', async () => {
    const cb = new CircuitBreaker({ threshold: 2, cooldownMs: 100000 });
    const fail = () => Promise.reject(new Error('fail'));
    await expect(cb.call(fail)).rejects.toThrow();
    await expect(cb.call(fail)).rejects.toThrow();
    expect(cb.state).toBe('OPEN');
  });

  it('serves stale data when open', async () => {
    const cb = new CircuitBreaker({ threshold: 1, cooldownMs: 100000 });
    await cb.call(() => Promise.resolve('fresh'));
    await cb.call(() => Promise.reject(new Error('fail')));
    expect(cb.state).toBe('OPEN');
    const result = await cb.call(() => Promise.reject(new Error('still down')));
    expect(result).toBe('fresh');
  });

  it('recovers after cooldown', async () => {
    const cb = new CircuitBreaker({ threshold: 1, cooldownMs: 1 });
    await expect(cb.call(() => Promise.reject(new Error('fail')))).rejects.toThrow();
    await new Promise(r => setTimeout(r, 10));
    const result = await cb.call(() => Promise.resolve('recovered'));
    expect(result).toBe('recovered');
    expect(cb.state).toBe('CLOSED');
  });
});

describe('helpers', () => {
  it('slugify', () => {
    expect(slugify('Hello World!')).toBe('hello-world');
    expect(slugify('  Acme Corp - Series B  ')).toBe('acme-corp-series-b');
  });

  it('formatCurrency', () => {
    expect(formatCurrency(1234.5)).toContain('1,234.50');
  });

  it('relativeTime', () => {
    const past = new Date(Date.now() - 3600000);
    expect(relativeTime(past)).toContain('ago');
  });
});


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: suspicious | SkillSpector: suspicious (MEDIUM)