Skill: koatora20/guava-suite (version 1.1.0)

--- name: guava-suite description: > Premium security suite for AI agents. Adds $GUAVA token-gated strict mode protection on top of guard-scanner. Features: 2-layer defense (static + runtime), Soul Lock, Memory Guard, on-chain identity verification via SoulRegistry V2. Requires $GUAVA token on Polygon Mainnet. homepage: https://github.com/koatora20/guava-suite metadata: openclaw: emoji: "🍈" category: security requires: bins: - node env: [] files: ["services/*"] primaryEnv: null tags: - security - token-gate - runtime-guard - soul-lock - polygon - guava --- # GuavaSuite 🍈 Premium 2-layer security for AI agents — powered by **$GUAVA** token gating. ## What It Does GuavaSuite upgrades your guard-scanner from `enforce` (CRITICAL-only) to `strict` mode (HIGH + CRITICAL blocking), plus adds these exclusive features: | Feature | Free (guard-scanner) | Suite ($GUAVA) | |---------|---------------------|----------------| | Static Scan (129 patterns, 21 categories) | ✅ | ✅ | | Runtime Guard (enforce) | ✅ | ✅ | | **Runtime Guard (strict)** | ❌ | ✅ | | **Soul Lock** (SOUL.md integrity + auto-rollback) | ❌ | ✅ | | **Memory Guard** (L1-L5 記憶システム保護) | ❌ | ✅ | | **Zettel Memory** (原子的ノート+リンク+検索) | ❌ | ✅ | | **On-chain Identity** (SoulRegistry V2) | ❌ | ✅ | | Audit Log (JSONL) | ✅ | ✅ | ## Prerequisites 1. **guard-scanner** installed (`clawhub install guard-scanner`) 2. **$GUAVA tokens** on Polygon Mainnet (minimum 1M $GUAVA) - [REDACTED_SECRET] - Buy on [QuickSwap V2](https://quickswap.exchange/#/swap) ### How to Get $GUAVA | Method | How | |--------|-----| | **超越者プラン** (note.com membership) | 手動送金 — MetaMaskでウォレットに直接送付 | | **自分で購入** | QuickSwap V2 で MATIC → $GUAVA swap | > **セキュリティ方針**: $GUAVAの配布はすべてMetaMaskからの手動送金で行います。秘密鍵をスクリプトに渡すことは一切しません。 ## Quick Start ### 1. Install ```bash # Via clawhub (coming soon) clawhub install guava-suite # Or: git clone + setup git clone https://github.com/koatora20/guava-suite.git cd guava-suite && bash setup.sh ``` ### 2. Activate ```bash node services/license-api/src/activate.js --wallet 0xYOUR_WALLET_ADDRESS ``` This single command will: 1. Request a challenge nonce 2. Prompt you to sign with your wallet (EIP-712) 3. Verify your signature & check $GUAVA balance on Polygon 4. Save JWT locally & switch guard-scanner to `strict` mode ### 3. Check Status ```bash node services/license-api/src/activate.js --status ``` ### Deactivate ```bash node services/license-api/src/activate.js --deactivate ``` ## How Token Gating Works ``` You hold $GUAVA on Polygon │ ▼ Sign EIP-712 challenge │ ▼ LicenseService checks: ├─ Signature valid? ├─ $GUAVA balance ≥ 1M? │ ▼ JWT issued → SuiteGate activated │ ▼ guard-scanner mode: strict (HIGH + CRITICAL blocked) ``` ## Architecture - **SuiteGate** — JWT-based fail-closed gate (grace period for network issues) - **LicenseService** — Nonce + EIP-712 signature + $GUAVA balance check + JWT issuance - **TokenBalanceChecker** — Polygon RPC ERC-20 balance verification (zero dependencies) - **SuiteBridge** — Connects SuiteGate status to guard-scanner runtime mode - **SoulRegistry V2** — On-chain identity verification (Polygon) ## External Endpoints | URL | Data Sent | Purpose | |-----|-----------|---------| | `polygon-rpc.com` | Wallet address | $GUAVA balance check (read-only `eth_call`) | ## Security & Privacy - **Read-only on-chain**: Only calls `balanceOf` — no transactions, no approvals - **Local JWT**: Tokens stored locally, never sent to external servers - **Fail-closed**: If balance check fails, Suite features are disabled (not bypassed) - **No telemetry**: Zero analytics or tracking ## License Proprietary — © 2026 Guava 🍈 & Dee

path: ROADMAP.md
content: # GuavaSuite ROADMAP

## 方針
GuavaSuiteはファネル④（Revenue）の本丸。
$GUAVAトークンゲートで認証し、Soul Lock/SoulRegistry/Memory V4を提供。

## Phase 1: SuiteGate完成（W3-W6）
元: `guava-guard-suite-split`
- [x] W3 REFACTOR: CLI + クラス分離 + activationログ整備
- [x] W3.5 Runtime Guard: guard-scanner `plugin.ts` が `before_tool_call` hookで実現
- [x] W4 E2E: 27テスト全パス（suite-integration + e2e-activation）
- [x] W5 コード分離: Guard(OSS) / Suite(Private) 境界確定
- [ ] W6 ClawHub公開（GitHub Private配布）

## Phase 2: SoulRegistry統合
元: `guava-guard-onchain`
- [ ] SoulRegistry V2をSuiteGateに接続
- [ ] SOUL.md SHA256自動計算 + オンチェーン照合
- [ ] 不一致アラート（Discord + ログ）
- [ ] 定期検証（ハートビート連動）

## Phase 2.5: Zettel Memory統合 ✅
元: `zettel-memory-openclaw`（MVP Complete 2026-02-16）
- [x] 原子的ノート作成（new_note.py）
- [x] ノート間リンク+index更新（link_notes.py）
- [x] memory_search互換+1hop展開（search_expand.py）
- [x] 週次キュレーション（weekly_curate.py）
- [x] Antigravity連携（antigravity_digest.py）
- [x] テスト（P0/P1/P2 property-based）
- [x] guava-suite/scripts/zettel/ に統合

## Phase 3: トークンゲーティング実装
- [ ] $GUAVA残高チェック（Polygon RPC）をSuiteGateに統合
- [ ] 閾値設定（$GUAVA価格確認後に決定）
- [ ] Founders Pass NFT検証統合
- [ ] ウォレット接続UI

## Phase 4: Founders Pass デプロイ
元: `founders-pass`
- [ ] NFTアートワーク生成
- [ ] メタデータJSON作成
- [ ] Remix + Phantomでデプロイ
- [ ] 900M $GUAVAデポジット
- [ ] テストミント

## Phase 5: GUAVA Protocol統合
元: `guava-protocol`
- [ ] guava-score.js実装
- [ ] NFTライセンスチェック
- [ ] EAS attestation統合（将来）


path: STATUS.md
content: # GuavaSuite — STATUS

## ステータス: 🔨 W6 ClawHub公開準備中

## ファネル上の位置: ④ REVENUE（$GUAVAトークンゲート）

## 概要
でぃー＆グアバ固有のAIアイデンティティ保護・管理プラットフォーム。
$GUAVA保有量で認証。超越者メンバーには$GUAVAを配布。

## 認証モデル
- **超越者**（¥100,000/月）→ $GUAVA配布 → Suite利用可
- **Founders Pass**（1 POL / 10個限定）→ 90M $GUAVA付与 → Suite利用可
- **一般** → QuickSwapで$GUAVA購入 → 閾値以上保有で利用可

## 含む機能
- ✅ SuiteGate middleware（fail-closed gate + grace period）
- ✅ License API（challenge/verify/health endpoints）
- ✅ TokenBalanceChecker（Polygon RPC、ゼロ依存）
- ✅ SignatureVerifier（EIP-712、ethers optional）
- ✅ SuiteBridge（SuiteGate → guard-scanner mode switch）
- ✅ CLI Activation（activate.js — 1コマンドアクティベーション）
- ✅ Setup Script（setup.sh — 依存インストール + 配置）
- ✅ plugin.ts JWT検出（GuavaSuite JWT → auto strict mode）
- ✅ Zettel Memory Integration（zettel-memory-integrationから吸収）
- ✅ SoulRegistry V2（contracts/SoulRegistryV2.sol）

## テストカバレッジ

- Unit Tests: 21（suite-integration.test.js）— ✅ ALL PASS
- E2E Tests: 6（e2e-activation.test.js）— ✅ ALL PASS
- **合計: 27テスト、0失敗**

## 技術進捗
- [x] W1 Contracts（Hardhat 5本 PASS）
- [x] W2 License API（28本 PASS）
- [x] W3 Runtime Gate GREEN（SuiteGate middleware）
- [x] W3 REFACTOR（CLI + クラス分離）
- [x] W4 E2E（27テスト全パス）
- [x] W5 コード分離（OSS/Private境界確定）
- [ ] W6 ClawHub公開（GitHub Private配布）

## 吸収元
- `guava-guard-onchain` → SoulRegistry統合
- `founders-pass` → NFTライセンス統合
- `guava-protocol` → tanhスコアリング統合

## 次のアクション
→ GitHub Private リポ作成 → push → 配布テスト


path: _meta.json
content: {
    "publisher": {
        "id": "koatora20",
        "name": "Guava & Dee",
        "homepage": "https://github.com/koatora20"
    },
    "version": "1.0.0",
    "created": "2026-02-18T00:00:00Z",
    "source": "private",
    "license": "PROPRIETARY",
    "checksum_algorithm": "sha256",
    "features": [
        "token-gate",
        "strict-runtime-guard",
        "soul-lock",
        "memory-guard",
        "onchain-identity",
        "jwt-licensing"
    ],
    "dependencies": [
        "guard-scanner@>=2.0.0"
    ],
    "token": {
        "name": "$GUAVA",
        "chain": "Polygon",
        "chainId": 137,
        "contract": [REDACTED_SECRET],
        "threshold": "1000000",
        "decimals": 18
    },
    "tags": [
        "security",
        "premium",
        "token-gate",
        "polygon",
        "guava",
        "runtime-guard",
        "soul-lock",
        "eip-712"
    ]
}

path: docs/ADR-001-open-core-boundary.md
content: # ADR-001: Open Core Boundary for GuavaGuard/GuavaSuite

- Status: Accepted
- Date: 2026-02-17
- Deciders: dee, guava

## Context
公開価値（再現性・信頼）と高感度防御（Soul/Hash/Memory統合）を同一コードベースで扱うと、漏洩・運用事故・主張不整合が発生しやすい。

## Decision
Open Core + Sealed Extensions を採用。
- GuavaGuard: OSS core
- GuavaSuite: private extension
- 接続は plugin API + ACL のみ

## Consequences
### Positive
- OSSの信頼性と採用性を維持
- 高感度ロジックの管理強化
- 学術主張（再現可能）と事業優位（private）を両立

### Negative
- 境界管理コスト増加
- 契約テスト・バージョン管理の運用負荷

## Implementation Rules (t-wada TDD)
1. RED: 契約テストを先に書く（境界破壊ケース含む）
2. GREEN: 最小実装で通す
3. REFACTOR: 責務分離・命名改善・重複除去
4. 常に小さく刻む（1変更1意図）

## Verification Checklist
- [ ] OSS単体で全テスト緑
- [ ] private未接続でfail-closed動作
- [ ] direct import禁止ルール有効
- [ ] plugin version handshakeテスト有効


path: docs/ARCH_SPLIT.md
content: # ARCH_SPLIT.md

## Architecture Decision
GuavaGuard は OSS の汎用セキュリティエンジンとして公開し、Soul/Hash/Memory統合は GuavaSuite として private 管理する。

## Product Boundary

### GuavaGuard (OSS)
含む:
- 静的スキャン（汎用ルール）
- Runtime Guard（汎用）
- CLI / JSON / SARIF 出力
- Extension Host（plugin API）

含まない:
- Soul Lock
- Identity hash ledger
- Memory v4 integration
- Dee/Guava 固有ポリシー

### GuavaSuite (Private)
含む:
- Soul Lock
- Hash integration and recovery
- GuavaMemory v4 orchestration
- EAE integration policies

## Integration Model
- Guard → Suite は plugin API 経由のみ
- 直接 import 禁止
- Suite 未接続時は fail-closed（guard-only安全動作）

## Compatibility Contract
- API versioning: `x.y`
- Backward compatible: minor
- Breaking changes: major

## Directory Sketch
- `skills/guava-guard/` (OSS)
- `private/guava-suite/` (Private)
- `skills/guava-guard/plugin-api/` (shared contract only)

## Non-goals
- OSS側にSuite実装断片を残さない
- 「隠しフラグ」で実質統合しない


path: docs/GUAVA_SUITE_TOKEN_GATE_SPEC_V1.md
content: # GUAVA Suite Token-Gate Spec v1

## 0. Goal
GuavaSuite（private機能）を $GUAVA 支払いで利用可能にし、GuavaGuard OSSと明確分離したまま安全に運用する。

## 1. Product split
- OSS: `guava-guard`（無料）
- Paid: `guava-suite`（Soul/Hash/Memory統合）

## 2. Licensing model (初期)
- Access method: **Pass NFT**（ERC-721, Polygon）
- Payment [REDACTED_SECRET] (ERC-20)**
- Purchase: ユーザーが `buyWithGuava()` を実行 → Pass NFT mint
- Runtime check: Pass NFT保有 + 署名検証でsuite有効化

## 3. Why NFT pass first
- 1ユーザー=1ライセンスを扱いやすい
- 譲渡/無効化/バージョン管理が明確
- しきい値保有型より不正共有を制御しやすい

## 4. On-chain components
### 4.1 Contracts
1) `GuavaSuitePass.sol` (ERC-721)
- mint条件: `priceGuava` を支払い
- treasuryへ$GUAVA送金
- optional: non-transferable modeフラグ（初期はtransferable=false推奨）

2) `GuavaSuiteRegistry.sol`
- contract version
- revoked token list
- feature tier mapping

### 4.2 Existing token
- $GUAVA: [REDACTED_SECRET]

## 5. Off-chain architecture
### 5.1 License service (minimal)
- endpoint: `/license/challenge` nonce発行
- endpoint: `/license/verify` 署名検証 + on-chain保有確認
- issue: 短命JWT（machine-bound）

### 5.2 Suite runtime
- 起動時にJWT検証
- 定期再検証（例: 24h）
- 検証失敗時: fail-closed（suite機能停止、guardは継続）

## 6. Security controls
- Nonce 1回限り
- JWT短命 + machine fingerprint
- rate limit
- revocation list
- 監査ログ（activation/deactivation）

## 7. TDD plan (t-wada)
RED→GREEN→REFACTORで全工程

### Stage A: Contract tests
- buyWithGuava success/failure
- insufficient allowance
- revoked pass rejected
- transfer policy checks

### Stage B: License service tests
- nonce replay rejected
- invalid signature rejected
- pass owner accepted
- expired token rejected

### Stage C: Runtime integration tests
- pass valid => suite enabled
- pass invalid => suite disabled / guard alive
- network fail => cached token within grace only

## 8. Delivery phases
1) P1 Spec + contracts skeleton
2) P2 Contract implementation + tests
3) P3 License service + tests
4) P4 Suite runtime gate + tests
5) P5 E2E testnet dry-run
6) P6 Mainnet release

## 9. Definition of Done
- [ ] 全テスト緑（contracts/service/runtime）
- [ ] testnetで購入→有効化→失効フロー実証
- [ ] mainnetデプロイTx記録
- [ ] OSSとprivateの境界監査完了
- [ ] Runbook更新

## 10. Non-negotiables
- suite検証失敗時は必ず停止（fail-closed）
- guard OSSは常に利用可能
- secretをチャットに貼らない


path: docs/IMPLEMENTATION_PLAN_V1.md
content: # Implementation Plan v1 (Must-Finish)

## Timeline (aggressive but realistic)
- Day 1: Contract spec/final ABI + tests red
- Day 2: Contract green + deploy to Polygon testnet
- Day 3: License service red/green
- Day 4: Suite runtime gate red/green
- Day 5: E2E + hardening + release checklist

## Work breakdown

### W1 Contracts
- `contracts/GuavaSuitePass.sol`
- `contracts/GuavaSuiteRegistry.sol`
- unit tests + property tests

### W2 License service
- `services/license-api/`
- nonce/signature/JWT
- on-chain read via viem/ethers

### W3 Runtime integration
- `guava-suite` boot gate
- token validation middleware
- grace-period cache

### W4 Ops
- monitoring
- revocation command
- incident runbook

## Exit criteria per work item
- W1: all contract tests pass + gas report
- W2: replay/forgery tests pass
- W3: fail-open paths = 0
- W4: alert on verify failures + docs complete

## Anti-abandon protocol (must finish)
1. 毎日 `STATUS.md` 更新（Done/Next/Blocker）
2. 未完了で日跨ぎ時は必ず「次の1手」を明記
3. ブロッカー24h超えたら代替案を即決
4. スコープ追加は禁止（完了まで）

## Kill criteria (only valid reasons)
- 致命的法務リスク
- mainnet不可逆バグ
- 依存停止（chain/provider outage）

## Release checklist
- [ ] Security review complete
- [ ] Testnet validation complete
- [ ] Mainnet deployment recorded
- [ ] Docs + Runbook updated
- [ ] Rollback plan documented


path: docs/TASKLIST_TWADA_V2.md
content: # TASKLIST_TWADA_V2

## Objective
GuavaSuite token-gate を t-wada（Red→Green→Refactor）で完遂する。

## Phase 1 (W1) Contracts — ✅ Done
- [x] GuavaSuitePass / Registry / MockToken 実装
- [x] テスト 5 passing

## Phase 2 (W2) License API — ✅ Done
### RED ✅
- [x] nonce再利用を拒否するテスト
- [x] 不正署名を拒否するテスト
- [x] 期限切れトークンを拒否するテスト
- [x] pass未保有アドレスを拒否するテスト
### GREEN ✅
- [x] `/license/challenge` 実装（nonce発行）
- [x] `/license/verify` 実装（署名 + 所有確認 + JWT発行）
### REFACTOR ✅ (2026-02-17)
- [x] service層分離（NonceStore / SignatureVerifier / TokenIssuer）
- [x] エラーモデル統一（errors.js + LicenseError）
- [x] EIP-712署名検証（stub + ethers DI対応）
- [x] JWT発行（jsonwebtoken）
- [x] HTTPサーバー（Node built-in http）
- [x] テスト: 5ファイル28本全PASS + Hardhat 5本 = 33本全GREEN

## Phase 3 (W3) Runtime Gate — 🚧 REFACTOR残り (2026-02-17〜)
### RED ✅
- [x] JWT失効時にsuite停止・guard継続テスト
- [x] network障害時grace期間テスト
- [x] grace期間超過時fail-closedテスト
### GREEN ✅
- [x] SuiteGate middleware実装（fail-closed + grace period）
- [x] Grace period cache実装（graceDeadline状態管理）
- [x] check()でgrace deadline優先の修正（fail-closed保証）
- [x] テスト: 6ファイル38本全PASS
### REFACTOR
- [ ] activationログ整備
- [ ] guard-only fallbackの動作確認E2Eテスト

## Phase 4 (W4) E2E + Hardening
- [ ] testnetで購入→検証→有効化→失効
- [ ] Hardhat fork modeでPolygon Mainnet $GUAVA統合テスト
- [ ] Runbook更新
- [ ] Release checklist完了

## Guardrails
- 実装前に必ずリサーチ（仕様/脆弱性）
- 1コミット1意図
- 失敗はfixlog/STATUSへ即反映

## 引き継ぎメモ（後輩AIへ）
- テスト実行: `cd services/license-api && npm test`（vitest 38本）
- コントラクトテスト: `npx hardhat test`（5本）
- HTTPサーバー起動: `JWT_SECRET=xxx node src/server.js`（port 3100）
- プロジェクト設計書: `docs/` フォルダ参照（ARCH_SPLIT, ADR-001, TOKEN_GATE_SPEC）
- SuiteGateの設計: fail-closed（suiteEnabled=false, guardEnabled=true always）
- Grace period: networkFailure()後、graceMs以内はsuite継続、超過でfail-closed


path: docs/THREAT_MODEL.md
content: # THREAT_MODEL.md

## Assets
- Agent identity integrity
- Memory integrity/availability
- Detection rules confidentiality (high-sensitivity)
- OSS trust and reproducibility

## Trust Boundaries
1. OSS repo boundary
2. Private suite boundary
3. Runtime plugin boundary
4. Config/secrets boundary

## Primary Threats
- Direct import bypass (boundary break)
- Rule leakage via logs/errors
- Prompt-injection induced unsafe extension calls
- Replay/duplicate memory writes
- Plugin downgrade / version mismatch

## Controls
- Contract test required before merge
- Import-lint: no private path in OSS
- Fail-closed on extension errors
- Sensitive fields redaction in logs
- Version handshake + minimum supported version

## Abuse Cases
- Attacker mimics suite plugin with permissive responses
- OSS release accidentally includes private signatures
- Missing lock creates concurrent write corruption

## Security Invariants
- OSS alone must remain safe and useful
- Private logic never required for baseline safety
- Any boundary violation must fail build or fail runtime closed


path: docs/zettel-spec.md
content: # SPEC.md — Zettel Memory MVP for OpenClaw

## 1. Storage Layout

- `memory/notes/<id>.md` 原子的ノート本体
- `memory/notes/index.md` ノート一覧 + 主要リンク

## 2. Note Frontmatter

```yaml
id: zettel-YYYYMMDD-HHMMSS
title: <short title>
tags: [tag1, tag2]
entities: [entity1, entity2]
source: memory/YYYY-MM-DD.md#Lx-Ly
created_at: ISO8601
updated_at: ISO8601
supersedes: null
links: []
confidence: 0.7
```

## 3. Workflows

### A) New Note
`new_note.py` でタイトル・本文・タグ等を受け取り、ノート作成。

### B) Link
`link_notes.py` で最新ノートと既存ノートのキーワード重なりを計算。
閾値以上で `links` に相互追記。

### C) Index
`link_notes.py` 実行時に `index.md` を再生成（新しい順）。

## 4. Evolution (Supersedes)
更新版ノート作成時は `--supersedes <old_id>` を指定。
旧ノートは残し、新ノートが置換関係を持つ。

## 5. Safety
- 既存メモリは削除しない
- 上書きは対象ノート限定
- JSON/Markdown破損時は処理中断


path: hardhat.config.js
content: import hardhatToolboxMochaEthers from "@nomicfoundation/hardhat-toolbox-mocha-ethers";

/** @type {import('hardhat/config').HardhatUserConfig} */
const config = {
  plugins: [hardhatToolboxMochaEthers],
  solidity: {
    profiles: {
      default: {
        version: "0.8.24",
        settings: {
          optimizer: { enabled: true, runs: 200 },
        },
      },
    },
  },
};

export default config;


path: package.json
content: {
  "name": "guava-suite",
  "version": "1.1.0",
  "description": "Premium 2-layer security for AI agents — powered by $GUAVA token gating",
  "main": "services/license-api/src/server.js",
  "type": "module",
  "scripts": {
    "test": "node --test test/suite-integration.test.js test/e2e-activation.test.js",
    "start": "node services/license-api/src/server.js",
    "activate": "node services/license-api/src/activate.js",
    "soul-lock": "bash scripts/soul-watchdog.sh",
    "soul-lock:install": "bash scripts/soul-watchdog.sh --install",
    "soul-lock:status": "bash scripts/soul-watchdog.sh --status"
  },
  "keywords": [
    "security",
    "ai-agent",
    "token-gate",
    "polygon",
    "guava"
  ],
  "author": "Guava 🍈 & Dee",
  "license": "PROPRIETARY",
  "dependencies": {
    "jsonwebtoken": "^9.0.3"
  },
  "optionalDependencies": {
    "ethers": "^6.13.0"
  },
  "devDependencies": {
    "@nomicfoundation/hardhat-ethers": "^4.0.4",
    "@nomicfoundation/hardhat-ethers-chai-matchers": "^3.0.2",
    "@nomicfoundation/hardhat-toolbox-mocha-ethers": "^3.0.2",
    "@openzeppelin/contracts": "^5.4.0",
    "chai": "^5.1.2",
    "hardhat": "^3.1.8"
  }
}

path: scripts/soul-watchdog.sh
content: #!/bin/bash
# ============================================================
# GuavaGuard Soul Watchdog — Self-Healing Identity Protection
# ============================================================
# Monitors SOUL.md and IDENTITY.md for ANY modification.
# If tampered, instantly restores from git and re-locks.
# Runs as LaunchAgent (survives reboot).
#
# Usage:
#   ./soul-watchdog.sh              # foreground
#   ./soul-watchdog.sh --install    # install as LaunchAgent
#   ./soul-watchdog.sh --uninstall  # remove LaunchAgent
#   ./soul-watchdog.sh --status     # check if running
# ============================================================

WORKSPACE="${HOME}/.openclaw/workspace"
IDENTITY_FILES=("SOUL.md" "IDENTITY.md")
LOG_FILE="${HOME}/.openclaw/guava-guard/soul-watchdog.log"
HASH_FILE="${HOME}/.openclaw/guava-guard/soul-hashes-watchdog.json"
PLIST_NAME="com.guava-guard.soul-watchdog"
PLIST_PATH="${HOME}/Library/LaunchAgents/${PLIST_NAME}.plist"

mkdir -p "$(dirname "$LOG_FILE")"

log() {
  echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
  echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}

# Compute SHA-256 hash of a file
compute_hash() {
  shasum -a 256 "$1" 2>/dev/null | cut -d' ' -f1
}

# Store trusted hashes (current state = trusted baseline)
store_hashes() {
  local json="{"
  local first=true
  for f in "${IDENTITY_FILES[@]}"; do
    local fp="${WORKSPACE}/${f}"
    if [ -f "$fp" ]; then
      local h=$(compute_hash "$fp")
      if [ "$first" = true ]; then first=false; else json+=","; fi
      json+="\"${f}\":\"${h}\""
    fi
  done
  json+="}"
  echo "$json" > "$HASH_FILE"
  log "INIT: Stored trusted hashes → ${HASH_FILE}"
}

# Restore identity file from git and re-lock
restore_and_lock() {
  local file="$1"
  local fp="${WORKSPACE}/${file}"
  
  log "🚨 TAMPER DETECTED: ${file} — restoring from git..."
  
  # Unlock (if locked)
  chflags nouchg "$fp" 2>/dev/null
  
  # Restore from git
  cd "$WORKSPACE"
  if git checkout -- "$file" 2>/dev/null; then
    log "✅ RESTORED: ${file} from git"
  else
    # If git fails, restore from hash-verified backup
    log "⚠️ Git restore failed for ${file}, file may be untracked"
  fi
  
  # Re-lock
  chflags uchg "$fp" 2>/dev/null
  log "🔒 RE-LOCKED: ${file} (chflags uchg)"
  
  # Update stored hash
  store_hashes
}

# Check integrity against stored hashes
check_integrity() {
  if [ ! -f "$HASH_FILE" ]; then
    store_hashes
    return
  fi
  
  for f in "${IDENTITY_FILES[@]}"; do
    local fp="${WORKSPACE}/${f}"
    if [ -f "$fp" ]; then
      local current=$(compute_hash "$fp")
      local stored=$(python3 -c "import json; d=json.load(open('${HASH_FILE}')); print(d.get('${f}',''))" 2>/dev/null)
      
      if [ -n "$stored" ] && [ "$current" != "$stored" ]; then
        restore_and_lock "$f"
      fi
    fi
  done
}

# --- Install/Uninstall LaunchAgent ---

install_agent() {
  local script_path="$(cd "$(dirname "$0")" && pwd)/$(basename "$0")"
  
  cat > "$PLIST_PATH" << EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>${PLIST_NAME}</string>
  <key>ProgramArguments</key>
  <array>
    <string>/bin/bash</string>
    <string>${script_path}</string>
  </array>
  <key>RunAtLoad</key>
  <true/>
  <key>KeepAlive</key>
  <true/>
  <key>StandardOutPath</key>
  <string>${LOG_FILE}</string>
  <key>StandardErrorPath</key>
  <string>${LOG_FILE}</string>
</dict>
</plist>
EOF
  
  launchctl load "$PLIST_PATH" 2>/dev/null
  log "✅ INSTALLED: LaunchAgent ${PLIST_NAME}"
  echo "✅ Installed and started: ${PLIST_NAME}"
  echo "   Log: ${LOG_FILE}"
}

uninstall_agent() {
  launchctl unload "$PLIST_PATH" 2>/dev/null
  rm -f "$PLIST_PATH"
  log "🗑️ UNINSTALLED: LaunchAgent ${PLIST_NAME}"
  echo "🗑️ Uninstalled: ${PLIST_NAME}"
}

status_agent() {
  if launchctl list | grep -q "$PLIST_NAME"; then
    echo "✅ Running: ${PLIST_NAME}"
    echo "   PID: $(launchctl list | grep "$PLIST_NAME" | awk '{print $1}')"
    echo "   Log: ${LOG_FILE}"
    tail -5 "$LOG_FILE" 2>/dev/null
  else
    echo "❌ Not running: ${PLIST_NAME}"
  fi
}

# --- Handle CLI args ---
case "${1:-}" in
  --install)   install_agent; exit 0 ;;
  --uninstall) uninstall_agent; exit 0 ;;
  --status)    status_agent; exit 0 ;;
esac

# --- Main Loop (fswatch-based) ---

# Check if fswatch is available
if ! command -v fswatch &>/dev/null; then
  log "ERROR: fswatch not found. Install with: brew install fswatch"
  # Fallback to polling mode
  log "FALLBACK: Using polling mode (5s interval)"
  store_hashes
  while true; do
    check_integrity
    sleep 5
  done
  exit 0
fi

# Initialize
log "🍈🛡️ GuavaGuard Soul Watchdog starting..."
log "   Workspace: ${WORKSPACE}"
log "   Watching: ${IDENTITY_FILES[*]}"
store_hashes

# Build watch paths
WATCH_PATHS=()
for f in "${IDENTITY_FILES[@]}"; do
  WATCH_PATHS+=("${WORKSPACE}/${f}")
done

# Watch for changes with fswatch (macOS FSEvents backend)
fswatch -o "${WATCH_PATHS[@]}" | while read -r count; do
  log "⚡ File change detected (${count} events)"
  check_integrity
done


path: scripts/zettel/antigravity_digest.py
content: #!/usr/bin/env python3
"""
antigravity_digest.py — Antigravity walkthrough → Zettel note pipeline

Scans ~/.gemini/antigravity/brain/*/walkthrough.md for new or updated
walkthroughs and converts them into Zettel notes in memory/notes/.

Designed to run alongside session_digest_write.py via LaunchAgent or manually.
"""
from __future__ import annotations
import json, hashlib, os, pathlib, datetime as dt, re, subprocess

ROOT = pathlib.Path(os.environ.get('OPENCLAW_WORKSPACE', os.path.expanduser('~/.openclaw/workspace')))
BRAIN_DIR = pathlib.Path(os.environ.get('ANTIGRAVITY_BRAIN', os.path.expanduser('~/.gemini/antigravity/brain')))
NOTES_DIR = ROOT / 'memory' / 'notes'
STATE_PATH = ROOT / 'projects' / 'zettel-memory-openclaw' / 'state' / 'antigravity_state.json'
LINK_SCRIPT = ROOT / 'projects' / 'zettel-memory-openclaw' / 'scripts' / 'link_notes.py'


def iso_now() -> str:
    return dt.datetime.now(dt.timezone.utc).isoformat()


def load_state() -> dict:
    if STATE_PATH.exists():
        try:
            return json.loads(STATE_PATH.read_text(encoding='utf-8'))
        except Exception:
            pass
    return {'processed': {}}


def save_state(st: dict):
    STATE_PATH.parent.mkdir(parents=True, exist_ok=True)
    STATE_PATH.write_text(json.dumps(st, ensure_ascii=False, indent=2), encoding='utf-8')


def content_hash(text: str) -> str:
    return hashlib.sha256(text.encode()).hexdigest()[:16]


def slug(text: str) -> str:
    s = re.sub(r'[^a-zA-Z0-9\u3040-\u9fff]+', '-', text.lower())
    return s.strip('-')[:50]


def extract_title(text: str) -> str:
    """Extract title from walkthrough markdown."""
    for line in text.split('\n'):
        line = line.strip()
        if line.startswith('# '):
            title = line[2:].strip()
            # Remove "Walkthrough:" prefix if present
            title = re.sub(r'^Walkthrough:\s*', '', title, flags=re.IGNORECASE)
            return title[:80]
    return 'Antigravity Session'


def extract_tags(text: str) -> list[str]:
    """Extract meaningful tags from walkthrough content."""
    # Look for file extensions, tool names, key concepts
    tag_patterns = {
        r'\bAGENTS\.md\b': 'agents',
        r'\bRUNBOOK\.md\b': 'runbook',
        r'\bSOUL\.md\b': 'soul',
        r'\bMEMORY\.md\b': 'memory',
        r'\bguard-scanner\b': 'guard-scanner',
        r'\bzettel\b': 'zettelkasten',
        r'\bnote\.com\b': 'note-com',
        r'\bMoltbook\b': 'moltbook',
        r'\bLaunchAgent\b': 'launchagent',
        r'\bPDF\b': 'pdf',
        r'\barXiv\b': 'arxiv',
        r'\bZenodo\b': 'zenodo',
        r'\bTailscale\b': 'tailscale',
        r'\bOpenClaw\b': 'openclaw',
        r'\bComfyUI\b': 'comfyui',
        r'\b論文\b': 'paper',
        r'\bエピソード\b': 'episode',
        r'\bセキュリティ\b': 'security',
        r'\bブラウザ\b': 'browser',
    }
    tags = ['antigravity', 'walkthrough']
    for pattern, tag in tag_patterns.items():
        if re.search(pattern, text, re.IGNORECASE):
            tags.append(tag)
    return tags[:8]  # Cap at 8 tags


def extract_summary(text: str) -> str:
    """Extract a concise summary from walkthrough content."""
    lines = text.split('\n')
    summary_parts = []
    in_section = False

    for line in lines:
        stripped = line.strip()
        # Grab content from ## sections (not sub-sub sections)
        if stripped.startswith('## ') and not stripped.startswith('### '):
            in_section = True
            continue
        if stripped.startswith('# '):
            in_section = False
            continue
        if in_section and stripped and not stripped.startswith('|') and not stripped.startswith('```'):
            # Clean markdown formatting
            clean = re.sub(r'\[([^\]]+)\]\([^)]+\)', r'\1', stripped)
            clean = re.sub(r'[*_`]', '', clean)
            if clean and len(clean) > 10:
                summary_parts.append(clean)
            if len(summary_parts) >= 6:
                break

    return '\n'.join(f'- {p}' for p in summary_parts) if summary_parts else text[:300]


def make_note(conv_id: str, walkthrough_text: str, mtime: float) -> pathlib.Path:
    """Create a Zettel note from walkthrough content."""
    NOTES_DIR.mkdir(parents=True, exist_ok=True)

    now = dt.datetime.now(dt.timezone.utc)
    zid = f"zettel-{now.strftime('%Y%m%d-%H%M%S')}-{now.microsecond:06d}"

    title = extract_title(walkthrough_text)
    tags = extract_tags(walkthrough_text)
    summary = extract_summary(walkthrough_text)
    slug_title = slug(title)

    content = f"""---
id: {zid}
title: {title}
tags: [{', '.join(tags)}]
entities: [Antigravity, Guava]
source: antigravity/brain/{conv_id}/walkthrough.md
created_at: {now.isoformat()}
updated_at: {now.isoformat()}
supersedes: null
links: []
confidence: 0.75
---

## Antigravity Session Summary

{summary}

---
*Auto-generated from Antigravity walkthrough `{conv_id}`*
"""
    filename = f"{zid}-antigravity-{slug_title}.md"
    out = NOTES_DIR / filename
    out.write_text(content, encoding='utf-8')
    return out


def main() -> int:
    st = load_state()
    processed = st.get('processed', {})
    new_count = 0

    # Scan all brain directories for walkthrough.md
    if not BRAIN_DIR.exists():
        print(f'Brain directory not found: {BRAIN_DIR}')
        return 1

    for conv_dir in sorted(BRAIN_DIR.iterdir()):
        if not conv_dir.is_dir():
            continue

        walkthrough = conv_dir / 'walkthrough.md'
        if not walkthrough.exists():
            continue

        conv_id = conv_dir.name
        text = walkthrough.read_text(encoding='utf-8').strip()
        if not text or len(text) < 50:
            continue

        chash = content_hash(text)
        mtime = walkthrough.stat().st_mtime

        # Skip if already processed with same hash
        prev = processed.get(conv_id, {})
        if prev.get('hash') == chash:
            continue

        note_path = make_note(conv_id, text, mtime)
        processed[conv_id] = {
            'hash': chash,
            'note': str(note_path),
            'processed_at': iso_now(),
        }
        new_count += 1
        print(f'Created: {note_path.name} (from {conv_id[:8]}...)')

    if new_count > 0:
        # Re-link all notes
        subprocess.run(['python3', str(LINK_SCRIPT)], check=False)
        print(f'Processed {new_count} new walkthrough(s)')
    else:
        print('No new walkthroughs')

    st['processed'] = processed
    st['last_run'] = iso_now()
    save_state(st)
    return 0


if __name__ == '__main__':
    raise SystemExit(main())


path: scripts/zettel/link_notes.py
content: #!/usr/bin/env python3
from __future__ import annotations
import pathlib, re, datetime as dt

ROOT = pathlib.Path(os.environ.get('OPENCLAW_WORKSPACE', os.path.expanduser('~/.openclaw/workspace')))
NOTES_DIR = ROOT / 'memory' / 'notes'
INDEX = NOTES_DIR / 'index.md'


def read_text(p: pathlib.Path) -> str:
    return p.read_text(encoding='utf-8')


def keywords(text: str) -> set[str]:
    words = re.findall(r"[A-Za-z][A-Za-z0-9_-]{2,}", text.lower())
    stop = {'the','and','for','with','that','this','from','into','your','have','are'}
    return {w for w in words if w not in stop}


def extract_id(text: str, fallback: str) -> str:
    for line in text.splitlines():
        if line.startswith('id: '):
            return line.split('id: ', 1)[1].strip()
    return fallback


def update_links(path: pathlib.Path, all_notes: list[pathlib.Path], threshold=3):
    src = read_text(path)
    src_kw = keywords(src)
    linked = []
    for other in all_notes:
        if other == path:
            continue
        other_txt = read_text(other)
        ov = len(src_kw & keywords(other_txt))
        if ov >= threshold:
            linked.append((other, ov, other_txt))
    linked.sort(key=lambda x: x[1], reverse=True)

    # replace links line in frontmatter
    lines = src.splitlines()
    for i, line in enumerate(lines):
        if line.startswith('links:'):
            ids = [extract_id(txt, p.stem) for p, _, txt in linked[:8]]
            lines[i] = f"links: [{', '.join(ids)}]"
            break
    path.write_text('\n'.join(lines) + '\n', encoding='utf-8')


def build_index(notes: list[pathlib.Path]):
    rows = ['# Zettel Index', '', f'Updated: {dt.datetime.now().isoformat()}', '']
    for n in sorted(notes, key=lambda p: p.stat().st_mtime, reverse=True):
        txt = read_text(n)
        title = 'untitled'
        for line in txt.splitlines():
            if line.startswith('title: '):
                title = line.split('title: ',1)[1]
                break
        rows.append(f"- [{title}](./{n.name})")
    INDEX.write_text('\n'.join(rows) + '\n', encoding='utf-8')


def main():
    notes = [p for p in NOTES_DIR.glob('*.md') if p.name != 'index.md']
    for n in notes:
        update_links(n, notes)
    build_index(notes)
    print(f'updated {len(notes)} notes and index')


if __name__ == '__main__':
    main()


path: scripts/zettel/new_note.py
content: #!/usr/bin/env python3
from __future__ import annotations
import argparse, os, datetime as dt, pathlib, re

ROOT = pathlib.Path(os.environ.get('OPENCLAW_WORKSPACE', os.path.expanduser('~/.openclaw/workspace')))
NOTES_DIR = ROOT / 'memory' / 'notes'
NOTES_DIR.mkdir(parents=True, exist_ok=True)


def slug(s: str) -> str:
    s = s.strip().lower()
    s = re.sub(r'[^a-z0-9\-\s_]', '', s)
    s = re.sub(r'[\s_]+', '-', s)
    return s[:50] or 'note'


def main():
    p = argparse.ArgumentParser(description='Create a zettel note markdown file')
    p.add_argument('--title', required=True)
    p.add_argument('--body', required=True)
    p.add_argument('--tags', default='')
    p.add_argument('--entities', default='')
    p.add_argument('--source', default='')
    p.add_argument('--confidence', type=float, default=0.7)
    p.add_argument('--supersedes', default='')
    args = p.parse_args()

    now = dt.datetime.now(dt.timezone.utc)
    zid = f"zettel-{now.strftime('%Y%m%d-%H%M%S')}-{now.microsecond:06d}"
    filename = f"{zid}-{slug(args.title)}.md"
    path = NOTES_DIR / filename

    tags = [t.strip() for t in args.tags.split(',') if t.strip()]
    entities = [e.strip() for e in args.entities.split(',') if e.strip()]
    supersedes = args.supersedes if args.supersedes else 'null'

    content = f"""---
id: {zid}
title: {args.title}
tags: [{', '.join(tags)}]
entities: [{', '.join(entities)}]
source: {args.source or 'manual'}
created_at: {now.isoformat()}
updated_at: {now.isoformat()}
supersedes: {supersedes}
links: []
confidence: {args.confidence:.2f}
---

{args.body}
"""
    path.write_text(content, encoding='utf-8')
    print(path)


if __name__ == '__main__':
    main()


path: scripts/zettel/search_expand.py
content: #!/usr/bin/env python3
from __future__ import annotations
import argparse, os, json, pathlib, re, subprocess

ROOT = pathlib.Path(os.environ.get('OPENCLAW_WORKSPACE', os.path.expanduser('~/.openclaw/workspace')))
NOTES_DIR = ROOT / 'memory' / 'notes'
DEFAULT_OPENCLAW_BIN = '/opt/homebrew/bin/openclaw'


def parse_links(note_text: str) -> list[str]:
    for line in note_text.splitlines():
        if line.startswith('links:'):
            inside = line.split('[',1)[-1].rsplit(']',1)[0].strip()
            if not inside:
                return []
            return [x.strip() for x in inside.split(',') if x.strip()]
    return []


def id_from_filename(p: pathlib.Path) -> str:
    m = re.match(r'(zettel-\d{8}-\d{6}(?:-\d{6})?)', p.name)
    return m.group(1) if m else p.stem


def build_id_map() -> dict[str, pathlib.Path]:
    mp = {}
    for p in NOTES_DIR.glob('zettel-*.md'):
        mp[id_from_filename(p)] = p
    return mp


def try_parse_json(raw: str):
    try:
        return json.loads(raw)
    except Exception:
        return None


def run_memory_search(query: str, k: int, openclaw_bin: str):
    cmd = [
        openclaw_bin, 'tools', 'call', 'memory_search',
        json.dumps({'query': query, 'maxResults': k})
    ]
    try:
        out = subprocess.check_output(cmd, text=True, stderr=subprocess.STDOUT)
        return try_parse_json(out)
    except Exception:
        return None


def extract_seed_from_memory_result(memory_result, idmap: dict[str, pathlib.Path]) -> list[tuple[int, str, pathlib.Path]]:
    if not isinstance(memory_result, dict):
        return []
    snippets = memory_result.get('results') or memory_result.get('snippets') or []
    seeds = []
    for i, s in enumerate(snippets):
        text = ''
        path = ''
        if isinstance(s, dict):
            text = (s.get('text') or s.get('content') or '').lower()
            path = (s.get('path') or '')
        candidate = None

        # 1) direct path hit
        if path:
            for nid, fp in idmap.items():
                if fp.as_posix() == path or fp.name == pathlib.Path(path).name:
                    candidate = (nid, fp)
                    break

        # 2) id in snippet text
        if candidate is None and text:
            m = re.search(r'zettel-\d{8}-\d{6}(?:-\d{6})?', text)
            if m and m.group(0) in idmap:
                nid = m.group(0)
                candidate = (nid, idmap[nid])

        if candidate is not None:
            nid, fp = candidate
            score = max(1, 100 - i)  # keep order from memory_search
            seeds.append((score, nid, fp))

    # dedupe by id, keep best score
    best = {}
    for score, nid, fp in seeds:
        if nid not in best or score > best[nid][0]:
            best[nid] = (score, fp)
    out = [(score, nid, fp) for nid, (score, fp) in best.items()]
    out.sort(reverse=True)
    return out


def local_seed_search(query: str, idmap: dict[str, pathlib.Path], top: int):
    scored = []
    q = query.lower()
    for nid, fp in idmap.items():
        txt = fp.read_text(encoding='utf-8', errors='ignore').lower()
        s = txt.count(q)
        if s > 0:
            scored.append((s, nid, fp))
    scored.sort(reverse=True)
    return scored[:top]


def main():
    p = argparse.ArgumentParser(description='Expand zettel neighbors by 1-hop links')
    p.add_argument('--query', required=True)
    p.add_argument('--top', type=int, default=5)
    p.add_argument('--mode', choices=['auto', 'memory', 'local'], default='auto')
    p.add_argument('--openclaw-bin', default=DEFAULT_OPENCLAW_BIN)
    args = p.parse_args()

    idmap = build_id_map()

    seeds = []
    mode_used = 'local'
    if args.mode in ('auto', 'memory'):
        mem = run_memory_search(args.query, max(args.top, 8), args.openclaw_bin)
        seeds = extract_seed_from_memory_result(mem, idmap)
        if seeds:
            mode_used = 'memory_search'

    if (not seeds) and args.mode in ('auto', 'local'):
        seeds = local_seed_search(args.query, idmap, args.top)
        mode_used = 'local'

    seeds = seeds[:args.top]

    print(f'# Query: {args.query}')
    print(f'# Mode: {mode_used}')
    if not seeds:
        print('No direct zettel hits. (Tip: create notes first with new_note.py)')
        return

    print('\n## Seed notes')
    for s, nid, fp in seeds:
        print(f'- {nid} ({fp.name}) score={s}')

    print('\n## 1-hop linked notes')
    seen = set(nid for _, nid, _ in seeds)
    for _, nid, fp in seeds:
        links = parse_links(fp.read_text(encoding='utf-8', errors='ignore'))
        for lid in links:
            if lid in idmap and lid not in seen:
                seen.add(lid)
                print(f'- {lid} ({idmap[lid].name}) via {nid}')


if __name__ == '__main__':
    main()


path: scripts/zettel/session_digest_write.py
content: #!/usr/bin/env python3
from __future__ import annotations
import json, hashlib, pathlib, datetime as dt, re, subprocess

ROOT = pathlib.Path(os.environ.get('OPENCLAW_WORKSPACE', os.path.expanduser('~/.openclaw/workspace')))
SESSIONS_DIR = pathlib.Path('/Users/ishikawaryuuta/.openclaw/agents/main/sessions')
NOTES_DIR = ROOT / 'memory' / 'notes'
STATE_PATH = ROOT / 'projects' / 'zettel-memory-openclaw' / 'state' / 'digest_state.json'
LOCK_PATH = ROOT / 'projects' / 'zettel-memory-openclaw' / 'state' / 'digest.lock'
LINK_SCRIPT = ROOT / 'projects' / 'zettel-memory-openclaw' / 'scripts' / 'link_notes.py'
MIN_INTERVAL_SEC = 10 * 60


def iso_now() -> str:
    return dt.datetime.now(dt.timezone.utc).isoformat()


def load_state() -> dict:
    if STATE_PATH.exists():
        try:
            st = json.loads(STATE_PATH.read_text(encoding='utf-8'))
            if 'last_ts_ms' not in st:
                st['last_ts_ms'] = 0
            return st
        except Exception:
            pass
    # 初回は直近12時間のみ対象にして過去ログ爆取り込みを防ぐ
    return {'last_ts_ms': int((dt.datetime.now(dt.timezone.utc).timestamp() - 12*3600) * 1000), 'last_hash': ''}


def save_state(st: dict):
    STATE_PATH.parent.mkdir(parents=True, exist_ok=True)
    STATE_PATH.write_text(json.dumps(st, ensure_ascii=False, indent=2), encoding='utf-8')


def acquire_lock() -> bool:
    LOCK_PATH.parent.mkdir(parents=True, exist_ok=True)
    try:
        fd = LOCK_PATH.open('x')
        fd.write(str(dt.datetime.now(dt.timezone.utc).timestamp()))
        fd.close()
        return True
    except FileExistsError:
        return False


def release_lock():
    try:
        LOCK_PATH.unlink(missing_ok=True)
    except Exception:
        pass


def extract_text(content) -> str:
    if isinstance(content, str):
        return content
    if isinstance(content, list):
        parts = []
        for c in content:
            if isinstance(c, dict) and c.get('type') == 'text':
                t = c.get('text')
                if isinstance(t, str):
                    parts.append(t)
        return '\n'.join(parts)
    return ''


def to_ms(ts) -> int:
    if ts is None:
        return 0
    if isinstance(ts, (int, float)):
        return int(ts)
    if isinstance(ts, str):
        s = ts.strip()
        if s.isdigit():
            return int(s)
        try:
            if s.endswith('Z'):
                s = s[:-1] + '+00:00'
            return int(dt.datetime.fromisoformat(s).timestamp() * 1000)
        except Exception:
            return 0
    return 0


def iter_new_messages(last_ts_ms: int):
    files = sorted([p for p in SESSIONS_DIR.glob('*.jsonl') if '.deleted.' not in p.name], key=lambda p: p.stat().st_mtime)
    for fp in files:
        try:
            with fp.open(encoding='utf-8') as f:
                for line in f:
                    try:
                        o = json.loads(line)
                    except Exception:
                        continue
                    if o.get('type') != 'message':
                        continue
                    ts_raw = ((o.get('message') or {}).get('timestamp') or o.get('timestamp'))
                    ts_ms = to_ms(ts_raw)
                    if ts_ms <= last_ts_ms:
                        continue
                    role = ((o.get('message') or {}).get('role') or '').lower()
                    text = extract_text((o.get('message') or {}).get('content'))
                    if not text.strip():
                        continue
                    yield ts_ms, role, text.strip(), fp.name
        except Exception:
            continue


def suggest_tags(text: str) -> list[str]:
    words = re.findall(r'[A-Za-z][A-Za-z0-9_-]{3,}', text.lower())
    # Expanded stop words: common English + URL fragments + generic terms
    stop = {
        'that', 'this', 'with', 'from', 'your', 'about', 'have', 'will',
        'just', 'then', 'when', 'what', 'where', 'which', 'been', 'were',
        'they', 'them', 'their', 'there', 'these', 'those', 'than', 'each',
        'some', 'more', 'most', 'only', 'into', 'over', 'also', 'after',
        'before', 'between', 'under', 'again', 'does', 'done', 'could',
        'would', 'should', 'being', 'other', 'every', 'here', 'very',
        # Platform / generic terms (not useful as tags)
        'openclaw', 'session', 'message', 'https', 'http', 'true', 'false',
        'null', 'none', 'json', 'string', 'label', 'content', 'metadata',
        'untrusted', 'conversation', 'sender', 'guild', 'channel',
        'reply', 'current', 'info', 'data', 'type', 'user', 'assistant',
        'generic', 'auto', 'digest',
    }
    freq = {}
    for w in words:
        if w in stop:
            continue
        # Skip URL-like fragments (e.g. n---, com, org)
        if re.match(r'^[a-z]-{2,}', w) or len(w) <= 3:
            continue
        freq[w] = freq.get(w, 0) + 1
    top = sorted(freq.items(), key=lambda x: x[1], reverse=True)[:4]
    tags = [f'kw-{w}' for w, _ in top]
    base = ['auto-digest', 'session-summary']
    return base + tags


def fmt_ms(ms: int) -> str:
    return dt.datetime.fromtimestamp(ms / 1000, tz=dt.timezone.utc).isoformat()


def make_note(entries: list[tuple[int,str,str,str]]):
    NOTES_DIR.mkdir(parents=True, exist_ok=True)
    now = dt.datetime.now(dt.timezone.utc)
    zid = f"zettel-{now.strftime('%Y%m%d-%H%M%S')}-{now.microsecond:06d}"

    user_msgs = [t for _,r,t,_ in entries if r == 'user']
    assistant_msgs = [t for _,r,t,_ in entries if r == 'assistant']
    merged = '\n'.join([t for _,_,t,_ in entries])
    tags = suggest_tags(merged)

    title = f"Auto digest {now.astimezone().strftime('%m/%d %H:%M')}"
    body_lines = [
        '## Auto session digest (LLMなし・直接集約)',
        f'- window: {fmt_ms(entries[0][0])} -> {fmt_ms(entries[-1][0])}',
        f'- messages: total={len(entries)}, user={len(user_msgs)}, assistant={len(assistant_msgs)}',
        '',
        '## Recent highlights (raw extract)',
    ]
    for ts_ms, role, text, src in entries[-10:]:
        snippet = text.replace('\n', ' ')[:220]
        body_lines.append(f'- [{fmt_ms(ts_ms)}] ({role}) {snippet}')

    content = f"""---
id: {zid}
title: {title}
tags: [{', '.join(tags)}]
entities: [OpenClaw, SessionDigest]
source: auto/session-log
created_at: {now.isoformat()}
updated_at: {now.isoformat()}
supersedes: null
links: []
confidence: 0.60
---

{chr(10).join(body_lines)}
"""
    out = NOTES_DIR / f"{zid}-auto-session-digest.md"
    out.write_text(content, encoding='utf-8')
    return out


def main() -> int:
    if not acquire_lock():
        print('Another digest process is running; skip')
        return 0
    try:
        st = load_state()

        # cooldown guard
        last_run_at = st.get('last_run_at')
        if isinstance(last_run_at, str):
            try:
                t = last_run_at
                if t.endswith('Z'):
                    t = t[:-1] + '+00:00'
                elapsed = dt.datetime.now(dt.timezone.utc).timestamp() - dt.datetime.fromisoformat(t).timestamp()
                if elapsed < MIN_INTERVAL_SEC:
                    print('Cooldown active; skip')
                    return 0
            except Exception:
                pass

        entries = list(iter_new_messages(int(st.get('last_ts_ms', 0))))
        if not entries:
            print('No new messages; skip')
            return 0

        fingerprint = hashlib.sha256(('\n'.join(f"{a}|{b}|{c[:200]}|{d}" for a,b,c,d in entries)).encode()).hexdigest()
        if fingerprint == st.get('last_hash'):
            print('Same digest fingerprint; skip')
            return 0

        note_path = make_note(entries)
        subprocess.run(['python3', str(LINK_SCRIPT)], check=False)

        st['last_ts_ms'] = max(ts for ts,_,_,_ in entries)
        st['last_hash'] = fingerprint
        st['last_run_at'] = iso_now()
        st['last_note'] = str(note_path)
        save_state(st)

        print(f'Wrote {note_path}')
        return 0
    finally:
        release_lock()


if __name__ == '__main__':
    code = main()
    # Chain: also run Antigravity walkthrough digest
    antigravity_script = pathlib.Path(__file__).parent / 'antigravity_digest.py'
    if antigravity_script.exists():
        subprocess.run(['python3', str(antigravity_script)], check=False)
    # Chain: integrity check (daily health check)
    integrity_script = ROOT / 'scripts' / 'integrity_check.py'
    if integrity_script.exists():
        subprocess.run(['python3', str(integrity_script)], check=False)
    raise SystemExit(code)



path: scripts/zettel/weekly_curate.py
content: #!/usr/bin/env python3
from __future__ import annotations
import datetime as dt
import pathlib

ROOT = pathlib.Path(os.environ.get('OPENCLAW_WORKSPACE', os.path.expanduser('~/.openclaw/workspace')))
NOTES_DIR = ROOT / 'memory' / 'notes'
OUT_DIR = ROOT / 'projects' / 'zettel-memory-openclaw' / 'output'
OUT_DIR.mkdir(parents=True, exist_ok=True)


def main():
    notes = sorted([p for p in NOTES_DIR.glob('zettel-*.md')], key=lambda p: p.stat().st_mtime, reverse=True)
    top = notes[:30]

    today = dt.datetime.now().strftime('%Y-%m-%d')
    out = OUT_DIR / f'weekly-curation-{today}.md'

    lines = [
        f'# Weekly Curation Draft ({today})',
        '',
        f'Total notes: {len(notes)}',
        f'Included in draft: {len(top)}',
        '',
        '## Candidate notes (recent first)',
        ''
    ]

    for p in top:
        title = 'untitled'
        txt = p.read_text(encoding='utf-8', errors='ignore')
        for line in txt.splitlines():
            if line.startswith('title: '):
                title = line.split('title: ', 1)[1]
                break
        lines.append(f'- [{title}]({p.as_posix()})')

    lines += [
        '',
        '## Next Step (manual)',
        '- Pick 5-10 durable notes and merge into MEMORY.md or archive note.',
        '- Mark superseded notes with `supersedes` when rewriting.',
    ]

    out.write_text('\n'.join(lines) + '\n', encoding='utf-8')
    print(out)


if __name__ == '__main__':
    main()


path: services/license-api/package-lock.json
content: {
  "name": "license-api",
  "version": "1.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "license-api",
      "version": "1.0.0",
      "license": "ISC",
      "dependencies": {
        "jsonwebtoken": "^9.0.3"
      },
      "devDependencies": {
        "vitest": "^4.0.18"
      }
    },
    "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/@standard-schema/spec": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "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": "4.0.18",
      "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@standard-schema/spec": "^1.0.0",
        "@types/chai": "^5.2.2",
        "@vitest/spy": "4.0.18",
        "@vitest/utils": "4.0.18",
        "chai": "^6.2.1",
        "tinyrainbow": "^3.0.3"
      },
      "funding": {
        "url": "https://opencollective.com/vitest"
      }
    },
    "node_modules/@vitest/mocker": {
      "version": "4.0.18",
      "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@vitest/spy": "4.0.18",
        "estree-walker": "^3.0.3",
        "magic-string": "^0.30.21"
      },
      "funding": {
        "url": "https://opencollective.com/vitest"
      },
      "peerDependencies": {
        "msw": "^2.4.9",
        "vite": "^6.0.0 || ^7.0.0-0"
      },
      "peerDependenciesMeta": {
        "msw": {
          "optional": true
        },
        "vite": {
          "optional": true
        }
      }
    },
    "node_modules/@vitest/pretty-format": {
      "version": "4.0.18",
      "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "tinyrainbow": "^3.0.3"
      },
      "funding": {
        "url": "https://opencollective.com/vitest"
      }
    },
    "node_modules/@vitest/runner": {
      "version": "4.0.18",
      "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@vitest/utils": "4.0.18",
        "pathe": "^2.0.3"
      },
      "funding": {
        "url": "https://opencollective.com/vitest"
      }
    },
    "node_modules/@vitest/snapshot": {
      "version": "4.0.18",
      "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@vitest/pretty-format": "4.0.18",
        "magic-string": "^0.30.21",
        "pathe": "^2.0.3"
      },
      "funding": {
        "url": "https://opencollective.com/vitest"
      }
    },
    "node_modules/@vitest/spy": {
      "version": "4.0.18",
      "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "funding": {
        "url": "https://opencollective.com/vitest"
      }
    },
    "node_modules/@vitest/utils": {
      "version": "4.0.18",
      "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@vitest/pretty-format": "4.0.18",
        "tinyrainbow": "^3.0.3"
      },
      "funding": {
        "url": "https://opencollective.com/vitest"
      }
    },
    "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/buffer-equal-constant-time": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-3-Clause"
    },
    "node_modules/chai": {
      "version": "6.2.2",
      "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/ecdsa-sig-formatter": {
      "version": "1.0.11",
      "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "safe-buffer": "^5.0.1"
      }
    },
    "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/jsonwebtoken": {
      "version": "9.0.3",
      "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "jws": "^4.0.1",
        "lodash.includes": "^4.3.0",
        "lodash.isboolean": "^3.0.3",
        "lodash.isinteger": "^4.0.4",
        "lodash.isnumber": "^3.0.3",
        "lodash.isplainobject": "^4.0.6",
        "lodash.isstring": "^4.0.1",
        "lodash.once": "^4.0.0",
        "ms": "^2.1.1",
        "semver": "^7.5.4"
      },
      "engines": {
        "node": ">=12",
        "npm": ">=6"
      }
    },
    "node_modules/jwa": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "buffer-equal-constant-time": "^1.0.1",
        "ecdsa-sig-formatter": "1.0.11",
        "safe-buffer": "^5.0.1"
      }
    },
    "node_modules/jws": {
      "version": "4.0.1",
      "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "jwa": "^2.0.1",
        "safe-buffer": "^5.0.1"
      }
    },
    "node_modules/lodash.includes": {
      "version": "4.3.0",
      "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/lodash.isboolean": {
      "version": "3.0.3",
      "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/lodash.isinteger": {
      "version": "4.0.4",
      "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/lodash.isnumber": {
      "version": "3.0.3",
      "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/lodash.isplainobject": {
      "version": "4.0.6",
      "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/lodash.isstring": {
      "version": "4.0.1",
      "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/lodash.once": {
      "version": "4.1.1",
      "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "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],
      "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/obug": {
      "version": "2.1.1",
      "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "funding": [
        "https://github.com/sponsors/sxzz",
        "https://opencollective.com/debug"
      ],
      "license": "MIT"
    },
    "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/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/safe-buffer": {
      "version": "5.2.1",
      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/feross"
        },
        {
          "type": "patreon",
          "url": "https://www.patreon.com/feross"
        },
        {
          "type": "consulting",
          "url": "https://feross.org/support"
        }
      ],
      "license": "MIT"
    },
    "node_modules/semver": {
      "version": "7.7.4",
      "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC",
      "bin": {
        "semver": "bin/semver.js"
      },
      "engines": {
        "node": ">=10"
      }
    },
    "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/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": "1.0.2",
      "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=18"
      }
    },
    "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/tinyrainbow": {
      "version": "3.0.3",
      "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.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/vitest": {
      "version": "4.0.18",
      "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@vitest/expect": "4.0.18",
        "@vitest/mocker": "4.0.18",
        "@vitest/pretty-format": "4.0.18",
        "@vitest/runner": "4.0.18",
        "@vitest/snapshot": "4.0.18",
        "@vitest/spy": "4.0.18",
        "@vitest/utils": "4.0.18",
        "es-module-lexer": "^1.7.0",
        "expect-type": "^1.2.2",
        "magic-string": "^0.30.21",
        "obug": "^2.1.1",
        "pathe": "^2.0.3",
        "picomatch": "^4.0.3",
        "std-env": "^3.10.0",
        "tinybench": "^2.9.0",
        "tinyexec": "^1.0.2",
        "tinyglobby": "^0.2.15",
        "tinyrainbow": "^3.0.3",
        "vite": "^6.0.0 || ^7.0.0",
        "why-is-node-running": "^2.3.0"
      },
      "bin": {
        "vitest": "vitest.mjs"
      },
      "engines": {
        "node": "^20.0.0 || ^22.0.0 || >=24.0.0"
      },
      "funding": {
        "url": "https://opencollective.com/vitest"
      },
      "peerDependencies": {
        "@edge-runtime/vm": "*",
        "@opentelemetry/api": "^1.9.0",
        "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
        "@vitest/browser-playwright": "4.0.18",
        "@vitest/browser-preview": "4.0.18",
        "@vitest/browser-webdriverio": "4.0.18",
        "@vitest/ui": "4.0.18",
        "happy-dom": "*",
        "jsdom": "*"
      },
      "peerDependenciesMeta": {
        "@edge-runtime/vm": {
          "optional": true
        },
        "@opentelemetry/api": {
          "optional": true
        },
        "@types/node": {
          "optional": true
        },
        "@vitest/browser-playwright": {
          "optional": true
        },
        "@vitest/browser-preview": {
          "optional": true
        },
        "@vitest/browser-webdriverio": {
          "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: services/license-api/package.json
content: {
  "name": "license-api",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "directories": {
    "test": "test"
  },
  "scripts": {
    "test": "vitest run",
    "test_watch": "vitest"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "type": "module",
  "devDependencies": {
    "vitest": "^4.0.18"
  },
  "dependencies": {
    "jsonwebtoken": "^9.0.3"
  }
}


path: services/license-api/src/activate.js
content: #!/usr/bin/env node
// activate.js — One-command GuavaSuite activation CLI
// Why: Users shouldn't need to curl multiple endpoints manually.
// This script wraps the entire challenge→sign→verify→activate flow.

import http from "node:http";
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { exec } from "node:child_process";
import { homedir } from "node:os";
import { createInterface } from "node:readline";
import { LicenseService } from "./licenseService.js";
import { SuiteGate } from "./suiteGate.js";
import { SuiteBridge } from "./suiteBridge.js";
import { DOMAIN, TYPES } from "./signatureVerifier.js";

// ── Config ──

const SUITE_DIR = join(homedir(), ".openclaw", "guava-suite");
const TOKEN_FILE = join(SUITE_DIR, "token.jwt");
const CONFIG_FILE = join(homedir(), ".openclaw", "openclaw.json");
const PORT = parseInt(process.env.LICENSE_PORT || "3100", 10);
const JWT_SECRET = process.env.JWT_SECRET || "guava-suite-secret-" + homedir();

// ── Helpers ──

function ensureDir(dir) {
    if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
}

function readConfig() {
    try {
        return JSON.parse(readFileSync(CONFIG_FILE, "utf8"));
    } catch {
        return {};
    }
}

function writeConfig(config) {
    writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n");
}

function ask(question) {
    const rl = createInterface({ input: process.stdin, output: process.stdout });
    return new Promise((resolve) => {
        rl.question(question, (answer) => {
            rl.close();
            resolve(answer.trim());
        });
    });
}

// ── EIP-712 message display ──

function displaySigningMessage(nonce, address) {
    console.log("\n┌──────────────────────────────────────────┐");
    console.log("│  🍈 EIP-712 Signing Required             │");
    console.log("├──────────────────────────────────────────┤");
    console.log("│                                          │");
    console.log("│  Sign this message in your wallet:       │");
    console.log("│                                          │");
    console.log(`│  Domain: ${DOMAIN.name} v${DOMAIN.version} (Chain ${DOMAIN.chainId})  │`);
    console.log("│  Type: LicenseChallenge                  │");
    console.log(`│  nonce: ${nonce.slice(0, 20)}...       │`);
    console.log(`│  address: ${address.slice(0, 14)}...${address.slice(-4)}       │`);
    console.log("│  action: activate-guava-suite            │");
    console.log("│                                          │");
    console.log("└──────────────────────────────────────────┘");
    console.log("\n💡 Use MetaMask or any EIP-712 compatible wallet.");
    console.log("   Or use the signing helper at: https://eip712.guava.dev (coming soon)\n");
}

// ── Status check ──

async function checkStatus() {
    console.log("\n🍈 GuavaSuite Status Check\n");

    // Check saved token
    if (existsSync(TOKEN_FILE)) {
        const [REDACTED_SECRET], "utf8").trim();
        const gate = new SuiteGate({ jwtSecret: JWT_SECRET });
        const status = gate.activate(token);

        if (status.suiteEnabled) {
            const bridge = new SuiteBridge({ gate });
            const fullStatus = bridge.getStatus();
            console.log("  ✅ Suite: ACTIVE");
            console.log(`  🛡️ Guard Mode: ${fullStatus.guardMode}`);
            console.log(`  📅 Expires: ${status.expiresAt ? new Date(status.expiresAt).toISOString() : "N/A"}`);
            console.log("\n  Features:");
            for (const [feat, enabled] of Object.entries(fullStatus.features)) {
                console.log(`    ${enabled ? "✅" : "❌"} ${feat}`);
            }
            return;
        }

        console.log("  ⚠️  Token found but expired or invalid");
        console.log("  → Run: node activate.js --wallet 0x...");
        return;
    }

    console.log("  ❌ Suite: NOT ACTIVATED");
    console.log("  → Run: node activate.js --wallet 0x...");
    console.log("  → Need: 1,000,000+ $GUAVA on Polygon Mainnet");
}

// ── Activation flow ──

async function activate(walletAddress) {
    console.log("\n🍈 GuavaSuite Activation\n");
    console.log(`  Wallet: ${walletAddress}`);

    // 1. Create service and get challenge
    const service = new LicenseService({ jwtSecret: JWT_SECRET });

    console.log("\n  [1/4] Requesting challenge...");
    const { nonce, expiresAt } = service.issueChallenge(walletAddress);
    console.log(`  ✅ Challenge issued (expires: ${new Date(expiresAt).toLocaleTimeString()})`);

    // 2. Display signing message
    displaySigningMessage(nonce, walletAddress);

    // 3. Auto-open sign-helper.html with nonce pre-filled
    const __dirname = dirname(fileURLToPath(import.meta.url));
    const helperPath = join(__dirname, "sign-helper.html");
    if (existsSync(helperPath)) {
        const helperUrl = `file://${helperPath}?nonce=${encodeURIComponent(nonce)}`;
        console.log("  🌐 Opening signing helper in browser...");
        // macOS: open, Linux: xdg-open, Windows: start
        const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
        exec(`${cmd} "${helperUrl}"`, (err) => {
            if (err) console.log("  ⚠️  Could not auto-open browser. Open sign-helper.html manually.");
        });
    } else {
        console.log("  ⚠️  sign-helper.html not found. Sign manually with your wallet.");
    }

    // 4. Get signature from user
    const signature = await ask("  Paste your signature (0x...): ");

    if (!signature || !signature.startsWith("0x")) {
        console.error("\n  ❌ Invalid signature format. Must start with 0x.");
        process.exit(1);
    }

    // 4. Verify and get JWT
    console.log("\n  [2/4] Verifying signature & checking $GUAVA balance...");
    const result = await service.verify({
        address: walletAddress,
        nonce,
        signature,
    });

    if (!result.ok) {
        console.error(`\n  ❌ Verification failed: ${result.code}`);
        if (result.code === "INSUFFICIENT_BALANCE") {
            console.error(`     Current: ${result.balance?.current || "?"} $GUAVA`);
            console.error(`     Required: ${result.balance?.required || "1,000,000"} $GUAVA`);
            console.error("     → Buy $GUAVA: https://quickswap.exchange/#/swap");
        }
        process.exit(1);
    }

    // 5. Save JWT
    console.log("  [3/4] Saving license token...");
    ensureDir(SUITE_DIR);
    writeFileSync(TOKEN_FILE, result.token);
    console.log(`  ✅ Token saved to ${TOKEN_FILE}`);

    // 6. Update openclaw.json
    console.log("  [4/4] Activating strict mode...");
    const config = readConfig();
    if (!config.plugins) config.plugins = {};
    if (!config.plugins["guard-scanner"]) config.plugins["guard-scanner"] = {};
    config.plugins["guard-scanner"].mode = "strict";
    config.plugins["guard-scanner"].suiteEnabled = true;
    writeConfig(config);
    console.log("  ✅ guard-scanner mode set to: strict");

    // 7. Verify activation
    const gate = new SuiteGate({ jwtSecret: JWT_SECRET });
    const status = gate.activate(result.token);
    const bridge = new SuiteBridge({ gate });

    console.log("\n┌──────────────────────────────────────────┐");
    console.log("│  🍈 GuavaSuite Activated!                │");
    console.log("├──────────────────────────────────────────┤");
    console.log(`│  Mode: ${bridge.getMode().padEnd(33)}│`);
    console.log(`│  Suite: ${status.suiteEnabled ? "ACTIVE ✅" : "INACTIVE ❌"}                       │`);
    if (result.balance) {
        console.log(`│  Balance: ${result.balance.current} $GUAVA              │`);
    }
    console.log("│  Token expires in: 24h                   │");
    console.log("│                                          │");
    console.log("│  Re-activate daily or set up auto-renew  │");
    console.log("└──────────────────────────────────────────┘");
}

// ── Deactivate ──

async function deactivate() {
    console.log("\n🍈 Deactivating GuavaSuite...\n");

    // Remove token
    if (existsSync(TOKEN_FILE)) {
        const { unlinkSync } = await import("node:fs");
        unlinkSync(TOKEN_FILE);
        console.log("  ✅ Token removed");
    }

    // Reset config
    const config = readConfig();
    if (config.plugins?.["guard-scanner"]) {
        config.plugins["guard-scanner"].mode = "enforce";
        config.plugins["guard-scanner"].suiteEnabled = false;
        writeConfig(config);
        console.log("  ✅ guard-scanner mode reset to: enforce");
    }

    console.log("\n  Suite deactivated. OSS guard remains active.");
}

// ── CLI ──

const args = process.argv.slice(2);

if (args.includes("--help") || args.includes("-h")) {
    console.log(`
🍈 GuavaSuite CLI

Usage:
  node activate.js --wallet 0x...    Activate with wallet
  node activate.js --status          Check activation status
  node activate.js --deactivate      Deactivate suite
  node activate.js --help            Show this help

Environment:
  JWT_SECRET       Secret for JWT signing (default: auto-generated)
  LICENSE_PORT     API port (default: 3100)
`);
    process.exit(0);
}

if (args.includes("--status")) {
    await checkStatus();
    process.exit(0);
}

if (args.includes("--deactivate")) {
    await deactivate();
    process.exit(0);
}

const walletIdx = args.indexOf("--wallet");
if (walletIdx === -1 || !args[walletIdx + 1]) {
    console.error("❌ Wallet address required. Usage: node activate.js --wallet 0x...");
    process.exit(1);
}

const wallet = args[walletIdx + 1];
if (!wallet.startsWith("0x") || wallet.length !== 42) {
    console.error("❌ Invalid wallet address. Must be 42-char hex starting with 0x.");
    process.exit(1);
}

await activate(wallet);


path: services/license-api/src/errors.js
content: // License API error codes and error class
// Why: Unified error model ensures consistent HTTP responses and client handling

export const ErrorCode = Object.freeze({
  NONCE_UNKNOWN: "NONCE_UNKNOWN",
  NONCE_REPLAY: "NONCE_REPLAY",
  EXPIRED_CHALLENGE: "EXPIRED_CHALLENGE",
  ADDRESS_MISMATCH: "ADDRESS_MISMATCH",
  INVALID_SIGNATURE: "INVALID_SIGNATURE",
  PASS_REQUIRED: "PASS_REQUIRED",
  INSUFFICIENT_BALANCE: "INSUFFICIENT_BALANCE",
  INVALID_REQUEST: "INVALID_REQUEST",
  INTERNAL_ERROR: "INTERNAL_ERROR",
});

const HTTP_STATUS = {
  [ErrorCode.NONCE_UNKNOWN]: 400,
  [ErrorCode.NONCE_REPLAY]: 409,
  [ErrorCode.EXPIRED_CHALLENGE]: 410,
  [ErrorCode.ADDRESS_MISMATCH]: 400,
  [ErrorCode.INVALID_SIGNATURE]: 401,
  [ErrorCode.PASS_REQUIRED]: 403,
  [ErrorCode.INSUFFICIENT_BALANCE]: 403,
  [ErrorCode.INVALID_REQUEST]: 400,
  [ErrorCode.INTERNAL_ERROR]: 500,
};

export class LicenseError extends Error {
  /**
   * @param {string} code - One of ErrorCode constants
   * @param {string} [message] - Human-readable detail
   */
  constructor(code, message) {
    super(message || code);
    this.code = code;
    this.httpStatus = HTTP_STATUS[code] || 500;
  }

  toJSON() {
    return { ok: false, code: this.code, message: this.message };
  }
}


path: services/license-api/src/licenseService.js
content: // LicenseService — Facade composing NonceStore + SignatureVerifier + TokenIssuer + TokenBalanceChecker
// Why: Keeps the public API stable while delegating to single-responsibility modules
import { NonceStore } from "./nonceStore.js";
import { SignatureVerifier } from "./signatureVerifier.js";
import { TokenIssuer } from "./tokenIssuer.js";
import { TokenBalanceChecker } from "./tokenBalanceChecker.js";
import { LicenseError, ErrorCode } from "./errors.js";

export class LicenseService {
  /**
   * @param {object} [opts]
   * @param {number} [opts.ttlMs=300000] - Nonce TTL
   * @param {string} [opts.jwtSecret="dev-secret-CHANGE-ME"] - JWT signing secret
   * @param {string} [opts.jwtExpiresIn="24h"] - JWT lifetime
   * @param {object} [opts.ethers] - ethers module for real EIP-712 verification
   * @param {object} [opts.balanceChecker] - Injected TokenBalanceChecker (for testing)
   * @param {bigint} [opts.guavaThreshold] - $GUAVA minimum balance
   */
  constructor({
    ttlMs = 5 * 60 * 1000,
    jwtSecret = "dev-secret-CHANGE-ME",
    jwtExpiresIn = "24h",
    ethers = null,
    balanceChecker = null,
    guavaThreshold = undefined,
  } = {}) {
    this.nonces = new NonceStore({ ttlMs });
    this.signer = new SignatureVerifier({ ethers });
    this.tokens = new TokenIssuer({ [REDACTED_SECRET], expiresIn: jwtExpiresIn });
    this.balanceChecker = balanceChecker || new TokenBalanceChecker(
      guavaThreshold !== undefined ? { threshold: guavaThreshold } : {}
    );
  }

  /**
   * Issue a challenge nonce for the given address.
   * @param {string} address
   * @returns {{ nonce: string, expiresAt: number }}
   */
  issueChallenge(address) {
    if (!address) throw new LicenseError(ErrorCode.INVALID_REQUEST, "address required");
    return this.nonces.issue(address);
  }

  /**
   * Verify a signed challenge and issue a JWT if valid.
   * Supports two modes:
   *   - Legacy: { hasPass: true } skips balance check (for Founders Pass NFT holders)
   *   - Token Gate: checks $GUAVA balance on Polygon
   *
   * @param {object} params
   * @returns {Promise<{ ok: boolean, code: string, token?: string, expiresIn?: string, balance?: object }>}
   */
  async verify({ address, nonce, signature, hasPass, now = Date.now() }) {
    try {
      // 1. Nonce validation (replay, expiry, address match)
      this.nonces.consume(nonce, address, now);

      // 2. Signature verification
      this.signer.verify({ address, nonce, signature });

      // 3. Access check: Founders Pass (legacy) OR $GUAVA balance
      let balanceInfo = null;
      if (hasPass) {
        // Founders Pass holders bypass balance check
        // Why: NFT holders already paid — don't double-gate them
      } else {
        // Token gate: check $GUAVA balance on Polygon
        balanceInfo = await this.balanceChecker.check(address);
        if (!balanceInfo.ok) {
          return {
            ok: false,
            code: ErrorCode.INSUFFICIENT_BALANCE,
            balance: {
              current: balanceInfo.humanBalance,
              required: balanceInfo.humanThreshold,
            },
          };
        }
      }

      // 4. Issue JWT
      const { token, expiresIn } = this.tokens.issue({ address });

      return {
        ok: true,
        code: "OK",
        token,
        expiresIn,
        ...(balanceInfo ? {
          balance: {
            current: balanceInfo.humanBalance,
            required: balanceInfo.humanThreshold,
          },
        } : {}),
      };
    } catch (err) {
      if (err instanceof LicenseError) {
        return { ok: false, code: err.code };
      }
      return { ok: false, code: ErrorCode.INTERNAL_ERROR };
    }
  }
}


path: services/license-api/src/nonceStore.js
content: // NonceStore — nonce lifecycle management
// Why: Single responsibility for challenge nonce creation, validation, and consumption
import crypto from "node:crypto";
import { LicenseError, ErrorCode } from "./errors.js";

export class NonceStore {
    /**
     * @param {object} opts
     * @param {number} [opts.ttlMs=300000] - Nonce time-to-live in milliseconds (default 5 min)
     */
    constructor({ ttlMs = 5 * 60 * 1000 } = {}) {
        this.ttlMs = ttlMs;
        /** @type {Map<string, {address: string, expiresAt: number, used: boolean}>} */
        this._store = new Map();
    }

    /**
     * Issue a new challenge nonce for the given address.
     * @param {string} address - Ethereum address (checksummed or lowercase)
     * @returns {{ nonce: string, expiresAt: number }}
     */
    issue(address) {
        const nonce = crypto.randomUUID();
        const expiresAt = Date.now() + this.ttlMs;
        this._store.set(nonce, {
            address: address.toLowerCase(),
            expiresAt,
            used: false,
        });
        return { nonce, expiresAt };
    }

    /**
     * Consume a nonce — marks it as used if valid.
     * @param {string} nonce
     * @param {string} address
     * @param {number} [now=Date.now()]
     * @returns {void}
     * @throws {LicenseError} if nonce is unknown, replayed, expired, or address mismatch
     */
    consume(nonce, address, now = Date.now()) {
        const rec = this._store.get(nonce);
        if (!rec) throw new LicenseError(ErrorCode.NONCE_UNKNOWN);
        if (rec.used) throw new LicenseError(ErrorCode.NONCE_REPLAY);
        if (now > rec.expiresAt) throw new LicenseError(ErrorCode.EXPIRED_CHALLENGE);
        if ((address || "").toLowerCase() !== rec.address) {
            throw new LicenseError(ErrorCode.ADDRESS_MISMATCH);
        }
        rec.used = true;
    }

    /** Cleanup expired nonces to prevent memory leak in long-running service */
    prune() {
        const now = Date.now();
        for (const [k, v] of this._store) {
            if (now > v.expiresAt) this._store.delete(k);
        }
    }
}


path: services/license-api/src/server.js
content: // HTTP server for License API
// Why: Minimal Node built-in HTTP — zero external deps for the transport layer
import http from "node:http";
import { LicenseService } from "./licenseService.js";
import { LicenseError, ErrorCode } from "./errors.js";

const PORT = parseInt(process.env.LICENSE_PORT || "3100", 10);
const JWT_SECRET = process.env.JWT_SECRET || "dev-secret-CHANGE-ME";

const service = new LicenseService({ jwtSecret: JWT_SECRET });

/**
 * Read JSON body from request. Rejects if body is too large or malformed.
 * @param {http.IncomingMessage} req
 * @param {number} [maxBytes=4096]
 * @returns {Promise<object>}
 */
function readBody(req, maxBytes = 4096) {
    return new Promise((resolve, reject) => {
        const chunks = [];
        let size = 0;
        req.on("data", (chunk) => {
            size += chunk.length;
            if (size > maxBytes) {
                req.destroy();
                reject(new LicenseError(ErrorCode.INVALID_REQUEST, "Body too large"));
            }
            chunks.push(chunk);
        });
        req.on("end", () => {
            try {
                resolve(JSON.parse(Buffer.concat(chunks).toString()));
            } catch {
                reject(new LicenseError(ErrorCode.INVALID_REQUEST, "Invalid JSON"));
            }
        });
        req.on("error", reject);
    });
}

/** Send JSON response */
function json(res, status, data) {
    const body = JSON.stringify(data);
    res.writeHead(status, {
        "Content-Type": "application/json",
        "Content-Length": Buffer.byteLength(body),
    });
    res.end(body);
}

/** Route handler */
async function handleRequest(req, res) {
    // CORS preflight
    if (req.method === "OPTIONS") {
        res.writeHead(204, {
            "Access-Control-Allow-Origin": "*",
            "Access-Control-Allow-Methods": "POST, OPTIONS",
            "Access-Control-Allow-Headers": "Content-Type",
        });
        return res.end();
    }

    res.setHeader("Access-Control-Allow-Origin", "*");

    try {
        // POST /license/challenge
        if (req.method === "POST" && req.url === "/license/challenge") {
            const { address } = await readBody(req);
            if (!address) {
                return json(res, 400, { ok: false, code: ErrorCode.INVALID_REQUEST, message: "address required" });
            }
            const result = service.issueChallenge(address);
            return json(res, 200, { ok: true, ...result });
        }

        // POST /license/verify
        if (req.method === "POST" && req.url === "/license/verify") {
            const { address, nonce, signature, hasPass } = await readBody(req);
            const result = await service.verify({ address, nonce, signature, hasPass });
            const status = result.ok ? 200 : (result.code === ErrorCode.INVALID_SIGNATURE ? 401 : 400);
            return json(res, status, result);
        }

        // GET /health
        if (req.method === "GET" && req.url === "/health") {
            return json(res, 200, { ok: true, service: "license-api", version: "1.0.0" });
        }

        // 404
        json(res, 404, { ok: false, code: "NOT_FOUND" });
    } catch (err) {
        if (err instanceof LicenseError) {
            return json(res, err.httpStatus, err.toJSON());
        }
        console.error("[license-api] Internal error:", err);
        json(res, 500, { ok: false, code: ErrorCode.INTERNAL_ERROR });
    }
}

const server = http.createServer(handleRequest);

// Only listen if run directly (not imported for testing)
if (process.argv[1] && process.argv[1].endsWith("server.js")) {
    server.listen(PORT, () => {
        console.log(`🍈 License API listening on :${PORT}`);
    });
}

export { server, service };


path: services/license-api/src/signatureVerifier.js
content: // SignatureVerifier — EIP-712 typed data signature verification
// Why: Isolate cryptographic verification from business logic
import { LicenseError, ErrorCode } from "./errors.js";

// EIP-712 domain & type definitions for GuavaSuite license challenge
const DOMAIN = {
    name: "GuavaSuite",
    version: "1",
    chainId: 137, // Polygon Mainnet
};

const TYPES = {
    LicenseChallenge: [
        { name: "nonce", type: "string" },
        { name: "address", type: "address" },
        { name: "action", type: "string" },
    ],
};

export class SignatureVerifier {
    /**
     * @param {object} opts
     * @param {object} [opts.ethers] - ethers module (injected for testability)
     */
    constructor({ ethers = null } = {}) {
        this._ethers = ethers;
    }

    /**
     * Verify an EIP-712 signature for a license challenge.
     * @param {object} params
     * @param {string} params.address  - Expected signer address
     * @param {string} params.nonce    - Challenge nonce
     * @param {string} params.signature - Hex-encoded signature
     * @returns {void}
     * @throws {LicenseError} if signature is invalid or signer mismatch
     */
    verify({ address, nonce, signature }) {
        // If ethers is not injected, fall back to stub mode (for unit tests)
        if (!this._ethers) {
            return this._stubVerify({ address, signature });
        }

        try {
            const message = {
                nonce,
                address,
                action: "activate-guava-suite",
            };

            const recovered = this._ethers.verifyTypedData(DOMAIN, TYPES, message, signature);
            if (recovered.toLowerCase() !== address.toLowerCase()) {
                throw new LicenseError(ErrorCode.INVALID_SIGNATURE, "Signer mismatch");
            }
        } catch (err) {
            if (err instanceof LicenseError) throw err;
            throw new LicenseError(ErrorCode.INVALID_SIGNATURE, err.message);
        }
    }

    /**
     * Stub verification for unit tests without ethers dependency.
     * signature === "valid" passes, anything else fails.
     */
    _stubVerify({ signature }) {
        if (signature !== "valid") {
            throw new LicenseError(ErrorCode.INVALID_SIGNATURE);
        }
    }
}

// Export constants for clients that need to construct signing payloads
export { DOMAIN, TYPES };


path: services/license-api/src/suiteBridge.js
content: // SuiteBridge — Connects SuiteGate to guard-scanner runtime hook
// Why: Suite status determines the security posture of the runtime guard
// - Suite ACTIVE → strict mode (block HIGH + CRITICAL)
// - Suite INACTIVE → enforce mode (block CRITICAL only, OSS default)
// This is the "value add" of GuavaSuite: paying users get stronger protection

import { SuiteGate } from "./suiteGate.js";

/**
 * @typedef {Object} BridgeConfig
 * @property {SuiteGate} gate - SuiteGate instance
 * @property {Function} [onModeChange] - Callback when mode changes
 */

export class SuiteBridge {
    /**
     * @param {BridgeConfig} config
     */
    constructor({ gate, onModeChange = null }) {
        if (!gate) throw new Error("SuiteBridge requires a SuiteGate instance");
        this._gate = gate;
        this._onModeChange = onModeChange;
        this._lastMode = this.getMode();
    }

    /**
     * Get the current guard mode based on SuiteGate status.
     * @returns {"monitor" | "enforce" | "strict"}
     */
    getMode() {
        const status = this._gate.check();

        if (status.suiteEnabled) {
            // Suite active → premium protection
            return "strict";
        }

        // Suite inactive → OSS default
        return "enforce";
    }

    /**
     * Check if mode has changed and notify if callback is registered.
     * Called periodically or on gate status change.
     * @returns {{ mode: string, changed: boolean }}
     */
    checkModeChange() {
        const currentMode = this.getMode();
        const changed = currentMode !== this._lastMode;

        if (changed && this._onModeChange) {
            this._onModeChange(this._lastMode, currentMode);
        }

        this._lastMode = currentMode;
        return { mode: currentMode, changed };
    }

    /**
     * Get the full status report for display/logging.
     * @returns {Object}
     */
    getStatus() {
        const gateStatus = this._gate.check();
        const mode = this.getMode();

        return {
            suiteEnabled: gateStatus.suiteEnabled,
            guardEnabled: gateStatus.guardEnabled,
            guardMode: mode,
            reason: gateStatus.reason,
            expiresAt: gateStatus.expiresAt,
            graceDeadline: gateStatus.graceDeadline,
            features: {
                // OSS features (always available)
                staticScan: true,           // guard-scanner CLI
                runtimeGuard: true,         // Plugin Hook (enforce mode)
                auditLog: true,             // JSONL audit trail

                // Suite features (only when suiteEnabled)
                strictMode: gateStatus.suiteEnabled,       // HIGH+CRITICAL blocking
                soulLock: gateStatus.suiteEnabled,          // SOUL.md integrity check
                memoryGuard: gateStatus.suiteEnabled,       // Memory V4 protection
                onchainVerify: gateStatus.suiteEnabled,     // SoulRegistry V2
            },
        };
    }

    /**
     * Determine if a specific threat should be blocked based on current suite status.
     * This is the core decision function used by the runtime hook.
     *
     * @param {"CRITICAL" | "HIGH" | "MEDIUM"} severity
     * @returns {{ block: boolean, reason: string }}
     */
    shouldBlock(severity) {
        const mode = this.getMode();

        if (mode === "monitor") {
            return { block: false, reason: "Monitor mode — log only" };
        }

        if (mode === "strict") {
            // Suite active → block CRITICAL and HIGH
            if (severity === "CRITICAL" || severity === "HIGH") {
                return {
                    block: true,
                    reason: `GuavaSuite strict mode — ${severity} threat blocked`,
                };
            }
            return { block: false, reason: `${severity} below strict threshold` };
        }

        // enforce (default OSS)
        if (severity === "CRITICAL") {
            return { block: true, reason: `CRITICAL threat blocked (enforce mode)` };
        }

        return { block: false, reason: `${severity} below enforce threshold` };
    }
}


path: services/license-api/src/suiteGate.js
content: // SuiteGate — Runtime middleware for GuavaSuite activation
// Why: fail-closed design — suite features only available with valid JWT
// Guard (OSS) always runs regardless of suite status

import { TokenIssuer } from "./tokenIssuer.js";

/**
 * @typedef {Object} GateStatus
 * @property {boolean} suiteEnabled - true if suite features are active
 * @property {boolean} guardEnabled - true always (OSS, never disabled)
 * @property {string} reason - human-readable status
 * @property {number|null} expiresAt - JWT expiry timestamp (ms), null if no token
 * @property {number|null} graceDealine - grace period deadline (ms), null if not in grace
 */

export class SuiteGate {
    /**
     * @param {object} opts
     * @param {string} opts.jwtSecret - Secret for JWT verification
     * @param {number} [opts.graceMs=3600000] - Grace period in ms (default 1h)
     * @param {number} [opts.recheckMs=86400000] - Re-verification interval (default 24h)
     */
    constructor({ jwtSecret, graceMs = 60 * 60 * 1000, recheckMs = 24 * 60 * 60 * 1000 }) {
        if (!jwtSecret) throw new Error("SuiteGate requires jwtSecret");
        this._issuer = new TokenIssuer({ [REDACTED_SECRET] });
        this._graceMs = graceMs;
        this._recheckMs = recheckMs;

        // State
        this._token = null;
        this._decoded = null;
        this._lastVerifiedAt = null;
        this._graceDeadline = null;
    }

    /**
     * Activate suite with a valid JWT token.
     * @param {string} token
     * @returns {GateStatus}
     */
    activate(token) {
        try {
            this._decoded = this._issuer.verify(token);
            this._token = token;
            this._lastVerifiedAt = Date.now();
            this._graceDeadline = null; // Clear grace period on fresh activation
            return this.status();
        } catch {
            // Invalid token — do not activate, enter or maintain fail-closed
            return this._failClosed("Invalid or expired token");
        }
    }

    /**
     * Check current gate status. Called by suite features before executing.
     * @param {number} [now=Date.now()]
     * @returns {GateStatus}
     */
    check(now = Date.now()) {
        // No token ever set — guard-only mode
        if (!this._token) {
            return this._failClosed("No license token");
        }

        // If grace deadline is active, honor it (fail-closed if expired)
        // Why: networkFailure() or token expiry set a grace window.
        // Even if the stored JWT hasn't technically expired yet, the grace
        // deadline represents a real-world inability to re-verify.
        if (this._graceDeadline !== null) {
            if (now > this._graceDeadline) {
                return this._failClosed("Grace period expired");
            }
            return {
                suiteEnabled: true,
                guardEnabled: true,
                reason: "Grace period active",
                expiresAt: null,
                graceDeadline: this._graceDeadline,
            };
        }

        // Re-verify the token
        try {
            this._decoded = this._issuer.verify(this._token);
        } catch {
            // Token expired or invalid — enter grace period if not already
            return this._enterGrace(now, "Token expired");
        }

        // Token valid — check if recheck interval passed
        if (this._lastVerifiedAt && (now - this._lastVerifiedAt) > this._recheckMs) {
            // Recheck needed — enter grace until re-verified
            return this._enterGrace(now, "Recheck interval exceeded");
        }

        return {
            suiteEnabled: true,
            guardEnabled: true,
            reason: "Active",
            expiresAt: this._decoded.exp ? this._decoded.exp * 1000 : null,
            graceDeadline: null,
        };
    }


    /**
     * Simulate network failure scenario — token can't be re-verified.
     * Grace period allows continued operation for graceMs.
     * @param {number} [now=Date.now()]
     * @returns {GateStatus}
     */
    networkFailure(now = Date.now()) {
        return this._enterGrace(now, "Network failure — grace period active");
    }

    /**
     * @returns {GateStatus}
     */
    status() {
        return this.check();
    }

    /** @private */
    _enterGrace(now, reason) {
        if (!this._graceDeadline) {
            this._graceDeadline = now + this._graceMs;
        }

        if (now <= this._graceDeadline) {
            return {
                suiteEnabled: true,
                guardEnabled: true,
                reason: `Grace period: ${reason}`,
                expiresAt: null,
                graceDeadline: this._graceDeadline,
            };
        }

        // Grace expired — fail-closed
        return this._failClosed(`Grace period expired: ${reason}`);
    }

    /** @private */
    _failClosed(reason) {
        return {
            suiteEnabled: false,  // Suite OFF
            guardEnabled: true,    // Guard ALWAYS ON
            reason,
            expiresAt: null,
            graceDeadline: null,
        };
    }
}


path: tests/zettel/requirements-dev.txt
content: hypothesis>=6.0.0
jsonschema>=4.0.0


path: types/ethers-contracts/factories/index.ts
content: /* Autogenerated file. Do not edit manually. */
/* tslint:disable */
/* eslint-disable */
export { GuavaSuitePass__factory } from './GuavaSuitePass__factory.js';
export { GuavaSuiteRegistry__factory } from './GuavaSuiteRegistry__factory.js';
export { MockGuavaToken__factory } from './MockGuavaToken__factory.js';

path: types/ethers-contracts/index.ts
content: /* Autogenerated file. Do not edit manually. */
/* tslint:disable */
/* eslint-disable */
export type { GuavaSuitePass } from './GuavaSuitePass.js';
export type { GuavaSuiteRegistry } from './GuavaSuiteRegistry.js';
export type { MockGuavaToken } from './MockGuavaToken.js';
export * as factories from './factories/index.js';
export { GuavaSuitePass__factory } from './factories/GuavaSuitePass__factory.js';
export { GuavaSuiteRegistry__factory } from './factories/GuavaSuiteRegistry__factory.js';
export { MockGuavaToken__factory } from './factories/MockGuavaToken__factory.js';

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