From e89146076db50fbdd124972640192097a2bbca88 Mon Sep 17 00:00:00 2001 From: Curtis Date: Tue, 17 Feb 2026 01:17:30 -0700 Subject: [PATCH 01/34] docs: add unified PAI + IronClaw architecture design Captures the Layered Sovereignty architecture (IronClaw-First approach) from multi-session brainstorming. Classifies 11 IronClaw features as KEEP/BRIDGE/PAI-NATIVE and defines 5-phase implementation rollout. Co-Authored-By: Claude Opus 4.6 --- .../2026-02-17-unified-architecture-design.md | 192 ++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 docs/plans/2026-02-17-unified-architecture-design.md diff --git a/docs/plans/2026-02-17-unified-architecture-design.md b/docs/plans/2026-02-17-unified-architecture-design.md new file mode 100644 index 000000000..729560095 --- /dev/null +++ b/docs/plans/2026-02-17-unified-architecture-design.md @@ -0,0 +1,192 @@ +# Unified PAI + IronClaw Architecture Design + +**Date:** 2026-02-17 +**Status:** Approved +**Approach:** IronClaw-First (Approach A — Layered Sovereignty) + +## Problem Statement + +PAI and IronClaw were producing contradictory architecture recommendations: +- "Build a PAI daemon (~400 LOC), IronClaw is optional" +- "Keep IronClaw as the daemon, don't need a PAI daemon" + +**Root cause:** Category conflation — treating "daemon" as a single slot instead of three orthogonal concerns: persistence, intelligence, and security. + +## Architecture: Layered Sovereignty + +Three layers, each owning a distinct concern: + +``` +Layer 3: PAI Cognitive — Skills, Hooks, Algorithm, Memory +Layer 2: IronClaw Exec — Rust daemon, WASM sandbox, scheduling, exec approval +Layer 1: CC-Mirror Infer — Provider routing (OpenRouter, Ollama, Anthropic) +``` + +**Key insight:** IronClaw is the daemon chassis. PAI is the cognitive payload. CC-Mirror is the inference router. They are layers, not competitors. + +### Security Hierarchy + +IronClaw = **outer authority** (hard boundaries in Rust, WASM sandboxing, exec approvals) +PAI SecurityValidator = **inner authority** (smart contextual policy, YAML pattern matching) + +On conflict, IronClaw wins. PAI refines within IronClaw's boundaries. + +### Two Invocation Modes + +| Mode | Command | Hooks | Tools | Use Case | +|------|---------|-------|-------|----------| +| Lightweight triage | `claude --print --setting-sources '' --tools ''` | OFF | OFF | Quick classification, heartbeat checks | +| Full work | `claude --print` or `claude` | ON | ON | Real analysis, implementation, all "superpowers" | + +The hooks bypass in `Inference.ts` (line 117) is intentional — lightweight triage doesn't need the full PAI stack. + +## Feature Classification + +All 11 IronClaw dashboard features classified by integration strategy: + +### KEEP AS PROXY (6 features) + +IronClaw owns these. Dashboard proxies via `/api/ironclaw/[...path]`. + +| Feature | Why Keep | Dashboard Page | +|---------|----------|----------------| +| Jobs / Active Agents | IronClaw's core — Rust process management | `/agents`, `/agents/[id]` | +| Routines | IronClaw cron scheduling, heartbeat host | `/routines`, `/routines/[id]` | +| Ask PAI / Chat | IronClaw approval flow + streaming | `/ask` | +| Extensions | IronClaw plugin system | `/extensions` | +| IronClaw Files | IronClaw-managed documents | Sidebar collapsible | +| Status Indicator | Health check, online/offline | Sidebar footer | + +### BRIDGE (3 features) + +Show BOTH PAI and IronClaw data in unified tab views. + +| Feature | PAI Source | IronClaw Source | UI Pattern | +|---------|-----------|-----------------|------------| +| Memory | `~/.claude/MEMORY/` (JSONL, Markdown) | IronClaw `/memory` API (tree nodes) | Tab: "PAI Memory" / "IronClaw Memory" | +| Logs | `~/.claude/debug/` + hook output | IronClaw `/logs` API | Tab: "PAI Logs" / "IronClaw Logs" | +| Settings | `settings.json`, `settings.local.json` | IronClaw `/settings` API | Tab: "PAI Settings" / "IronClaw Settings" | + +### PAI-NATIVE (5 features) + +PAI owns these entirely. No IronClaw involvement. + +| Feature | Source | Dashboard Page | +|---------|--------|----------------| +| Inference Config | `~/.cc-mirror/*/config/settings.json` | `/inference` | +| Pipeline Board | PAI idea pipeline | `/pipeline` | +| All Ideas | PAI idea list | `/ideas` | +| Telos Files | PAI documents | Sidebar collapsible | +| Add File | PAI document upload | `/add-file` | + +## State Bridge: The Heartbeat Pattern + +IronClaw cron triggers periodic heartbeats: + +``` +IronClaw Routine (cron) → Lightweight triage (claude --print, no hooks) + ├── Nothing notable → Write "all clear" to daemon-state.json + └── Something needs attention → Full Claude session (all hooks fire) + └── SessionSummary → daemon-state.json → next heartbeat reads it +``` + +**State file:** `daemon-state.json` — written by PAI sessions, read by IronClaw heartbeats. Provides multi-heartbeat continuity (the "clipboard" between sessions). + +## Anti-Corruption Layer + +PAI-owned types map to/from IronClaw types at the boundary. This prevents the **integration ratchet** — where each absorbed IronClaw feature increases coupling, making future changes harder. + +```typescript +// PAI types (owned, stable) +interface PAIJob { id: string; status: PAIJobStatus; ... } + +// Mapping at boundary +function fromIronclawJob(ic: IronclawJob): PAIJob { ... } +``` + +## User Requirements (10 gathered across sessions) + +1. PAI daemon capability using PAI's own functionality +2. IronClaw as the security backbone (strongest security for entire infrastructure) +3. Ability to incorporate IronClaw updates selectively +4. Hooks ("superpowers") must work for real tasks +5. Lightweight triage mode without full hook overhead +6. Unified dashboard showing both PAI and IronClaw data +7. No feature regression — existing IronClaw pages keep working +8. CC-Mirror inference routing preserved +9. Clear security hierarchy (IronClaw outer, PAI inner) +10. Phased rollout — not a big-bang rewrite + +## Implementation Phases + +### Phase 0: Heartbeat (Hours) + +- One IronClaw routine config pointing to a PAI triage script +- `HEARTBEAT.md` documenting the pattern +- Validates: IronClaw can trigger PAI cognitive work + +### Phase 1: State Bridge (Days) + +- `daemon-state.json` schema and read/write helpers +- `session-bootstrap.sh` that reads state before spawning Claude +- `PAI_DEPLOY_CONTEXT=daemon` env var for SecurityValidator awareness +- Validates: Multi-heartbeat continuity works + +### Phase 2: Dashboard Bridges (~1-2 Weeks, ~410 LOC) + +- Memory page: Tab view with PAI Memory (read JSONL/Markdown) + IronClaw Memory (existing proxy) +- Logs page: Tab view with PAI Logs (read debug dir) + IronClaw Logs (existing proxy) +- Settings page: Tab view with PAI Settings (read settings.json) + IronClaw Settings (existing proxy) +- New API routes: `/api/pai/memory`, `/api/pai/logs`, `/api/pai/settings` + +### Phase 3: Anti-Corruption Layer (~2-3 Weeks) + +- PAI-owned type definitions for Jobs, Routines, Memory, etc. +- Mapping functions at IronClaw boundary +- Dashboard components consume PAI types only +- IronClaw types confined to proxy layer + +### Phase 4: Security Hierarchy (~3-4 Weeks) + +- Policy compiler: IronClaw constraints + PAI SecurityValidator rules → unified policy +- Build-time validator: catch conflicts before deployment +- Runtime: IronClaw enforces outer boundary, PAI refines within + +### Phase 5: Adaptive Heartbeat (Months) + +- Dynamic heartbeat intervals based on activity +- Channel routing (which findings go where: dashboard, notification, log) +- Graduated response (triage → partial hooks → full session) + +## RedTeam Findings to Address + +| Severity | Finding | Mitigation Phase | +|----------|---------|-----------------| +| CRITICAL | `claude --print` hooks bypass | Phase 1 — documented as intentional for triage mode | +| HIGH | Security policy conflict (no resolution protocol) | Phase 4 — policy compiler | +| HIGH | Overlapping enforcement planes | Phase 3 — anti-corruption layer clarifies boundaries | +| MEDIUM | CC-Mirror capability leakage | Phase 3 — type boundary prevents leakage | +| MEDIUM | SSE reconnection storm | Phase 2 — add circuit breaker to `use-ironclaw-sse.ts` | +| MEDIUM | Integration ratchet | Phase 3 — anti-corruption layer is the mitigation | + +## Key Files + +| File | Role | +|------|------| +| `Packs/pai-core-install/src/skills/CORE/Tools/Inference.ts` | PAI inference with `claude --print` | +| `Packs/pai-hook-system/src/hooks/SecurityValidator.hook.ts` | PAI security policy (703 LOC) | +| `Packs/pai-telos-skill/src/DashboardTemplate/lib/ironclaw.ts` | IronClaw proxy library | +| `Packs/pai-telos-skill/src/DashboardTemplate/types/ironclaw.ts` | IronClaw type definitions (233 LOC) | +| `Packs/pai-telos-skill/src/DashboardTemplate/lib/use-ironclaw-sse.ts` | SSE hook with reconnect | +| `Packs/pai-telos-skill/src/DashboardTemplate/lib/cc-mirror.ts` | CC-Mirror config library | + +## Success Criteria + +1. IronClaw heartbeat triggers PAI triage successfully +2. State persists across heartbeat sessions via daemon-state.json +3. Dashboard shows unified Memory/Logs/Settings with tab navigation +4. No existing IronClaw features regress +5. PAI types decoupled from IronClaw types at boundary +6. Security hierarchy enforced: IronClaw outer, PAI inner +7. Hooks fire for all full work sessions +8. Integration ratchet contained by anti-corruption layer From 5d4850ebc3a801291ea166b11fa76a537610f6fb Mon Sep 17 00:00:00 2001 From: Curtis Date: Tue, 17 Feb 2026 01:21:37 -0700 Subject: [PATCH 02/34] docs: add unified architecture implementation plan (14 tasks, Phases 0-2) Detailed step-by-step plan covering heartbeat triage script, daemon state bridge, Radix Tabs component, PAI Memory/Logs/Settings API routes, bridged dashboard pages with tabs, and SSE circuit breaker fix. Co-Authored-By: Claude Opus 4.6 --- .../2026-02-17-unified-architecture-plan.md | 1120 +++++++++++++++++ 1 file changed, 1120 insertions(+) create mode 100644 docs/plans/2026-02-17-unified-architecture-plan.md diff --git a/docs/plans/2026-02-17-unified-architecture-plan.md b/docs/plans/2026-02-17-unified-architecture-plan.md new file mode 100644 index 000000000..917b4e739 --- /dev/null +++ b/docs/plans/2026-02-17-unified-architecture-plan.md @@ -0,0 +1,1120 @@ +# Unified PAI + IronClaw Architecture — Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Implement the Layered Sovereignty architecture — IronClaw as daemon chassis, PAI as cognitive payload — with a heartbeat bridge, state persistence, and unified dashboard tabs for Memory/Logs/Settings. + +**Architecture:** IronClaw cron triggers PAI triage scripts via `claude --print`. State persists in `daemon-state.json` between heartbeats. The dashboard shows both PAI-native and IronClaw data in tab views for the 3 bridged features (Memory, Logs, Settings). A Tabs UI component is added to support this pattern. + +**Tech Stack:** Next.js 15, React 19, TypeScript (strict, `verbatimModuleSyntax`, `noUncheckedIndexedAccess`), Tailwind CSS 4, Radix UI primitives, lucide-react icons. No test framework — verification via `next build` + manual check. + +**Base path for all dashboard files:** `Packs/pai-telos-skill/src/DashboardTemplate/` + +--- + +## Phase 0: Heartbeat + +### Task 1: Create PAI Heartbeat Triage Script + +**Files:** +- Create: `Packs/pai-core-install/src/skills/CORE/Tools/heartbeat-triage.sh` + +**Context:** This is the script IronClaw's cron routine calls. It uses `claude --print` in lightweight mode (no hooks, no tools) to do a quick triage of PAI state and write results to `daemon-state.json`. Reference `Inference.ts:112-120` for the `--setting-sources '' --tools ''` pattern. + +**Step 1: Create the triage script** + +```bash +#!/usr/bin/env bash +# heartbeat-triage.sh — Lightweight PAI triage invoked by IronClaw routine. +# Uses claude --print with hooks/tools disabled for fast execution. +# Writes result to daemon-state.json for cross-session continuity. + +set -euo pipefail + +STATE_DIR="${HOME}/.claude/MEMORY/STATE" +STATE_FILE="${STATE_DIR}/daemon-state.json" +MEMORY_DIR="${HOME}/.claude/MEMORY" + +mkdir -p "${STATE_DIR}" + +# Read previous state if it exists +PREV_STATE="" +if [ -f "${STATE_FILE}" ]; then + PREV_STATE=$(cat "${STATE_FILE}") +fi + +# Gather quick signals (no expensive operations) +HOOK_ERRORS=$(find "${MEMORY_DIR}/LEARNING/FAILURES" -name "CONTEXT.md" -newer "${STATE_FILE}" 2>/dev/null | wc -l | tr -d ' ') +NEW_LEARNINGS=$(find "${MEMORY_DIR}/LEARNING" -name "*.jsonl" -newer "${STATE_FILE}" 2>/dev/null | wc -l | tr -d ' ') + +# Quick triage via claude --print (lightweight — no hooks, no tools) +TRIAGE=$(claude --print \ + --model haiku \ + --tools '' \ + --output-format text \ + --setting-sources '' \ + --system-prompt "You are a triage agent. Given system signals, output a JSON object with: {\"status\": \"clear\"|\"attention\", \"summary\": \"\", \"escalate\": true|false}. Only escalate if something genuinely needs human or full-agent attention." \ + "Previous state: ${PREV_STATE:-none}. Signals: ${HOOK_ERRORS} new failures, ${NEW_LEARNINGS} new learnings since last check." \ + 2>/dev/null || echo '{"status":"clear","summary":"triage failed gracefully","escalate":false}') + +# Write state file +TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ") +cat > "${STATE_FILE}" << EOF +{ + "timestamp": "${TIMESTAMP}", + "triage": ${TRIAGE}, + "signals": { + "new_failures": ${HOOK_ERRORS}, + "new_learnings": ${NEW_LEARNINGS} + } +} +EOF + +# If triage says escalate, spawn a full Claude session (hooks enabled) +ESCALATE=$(echo "${TRIAGE}" | grep -o '"escalate":\s*true' || true) +if [ -n "${ESCALATE}" ]; then + claude --print \ + --model sonnet \ + --output-format text \ + --system-prompt "You are PAI running in daemon mode (PAI_DEPLOY_CONTEXT=daemon). Review the triage findings and take appropriate action. Write a session summary to daemon-state.json when done." \ + "Heartbeat escalation. State: $(cat "${STATE_FILE}")" \ + > /dev/null 2>&1 & +fi + +echo "Heartbeat complete: $(echo "${TRIAGE}" | grep -o '"status":"[^"]*"' || echo 'done')" +``` + +**Step 2: Make it executable** + +Run: `chmod +x Packs/pai-core-install/src/skills/CORE/Tools/heartbeat-triage.sh` + +**Step 3: Commit** + +```bash +git add Packs/pai-core-install/src/skills/CORE/Tools/heartbeat-triage.sh +git commit -m "feat: add PAI heartbeat triage script for IronClaw daemon integration" +``` + +--- + +### Task 2: Create HEARTBEAT.md Documentation + +**Files:** +- Create: `Packs/pai-core-install/src/skills/CORE/docs/HEARTBEAT.md` + +**Context:** Documents the heartbeat pattern so anyone configuring IronClaw knows how to set up the routine. + +**Step 1: Write the documentation** + +```markdown +# PAI Heartbeat Pattern + +## Overview + +IronClaw triggers periodic PAI heartbeats via its routine/cron system. +Each heartbeat runs a lightweight triage (no hooks, no tools) to check +PAI system health. If something needs attention, it escalates to a full +Claude session with all hooks enabled. + +## Setup + +### IronClaw Routine Configuration + +Create a routine in IronClaw pointing to the triage script: + +```json +{ + "name": "pai-heartbeat", + "description": "Periodic PAI system health triage", + "trigger_type": "cron", + "trigger": { + "schedule": "*/15 * * * *" + }, + "action_type": "lightweight", + "action": { + "command": "bash ~/.claude/skills/PAI/CORE/Tools/heartbeat-triage.sh" + } +} +``` + +### Two Invocation Modes + +| Mode | Hooks | Tools | When | +|------|-------|-------|------| +| Lightweight triage | OFF | OFF | Every heartbeat — quick check | +| Full escalation | ON | ON | Only when triage finds something | + +### State Persistence + +State lives at `~/.claude/MEMORY/STATE/daemon-state.json`. + +Each heartbeat reads previous state and writes new state, providing +cross-session continuity (the "clipboard" between sessions). + +### Security + +- Triage mode: `--setting-sources '' --tools ''` — cannot execute tools or trigger hooks +- Full mode: All PAI hooks fire, including SecurityValidator +- IronClaw's exec approval layer applies to both modes +``` + +**Step 2: Commit** + +```bash +git add Packs/pai-core-install/src/skills/CORE/docs/HEARTBEAT.md +git commit -m "docs: add HEARTBEAT.md documenting IronClaw-PAI daemon pattern" +``` + +--- + +## Phase 1: State Bridge + +### Task 3: Create daemon-state Library + +**Files:** +- Create: `Packs/pai-telos-skill/src/DashboardTemplate/lib/daemon-state.ts` + +**Context:** Server-side library for reading `daemon-state.json`. Used by the dashboard to show heartbeat status. Similar pattern to `lib/cc-mirror.ts` — reads from a well-known file path, returns safe typed data. + +**Step 1: Write the library** + +```typescript +import fs from "fs" +import path from "path" +import os from "os" + +const STATE_FILE = path.join(os.homedir(), ".claude", "MEMORY", "STATE", "daemon-state.json") + +export interface TriageResult { + status: "clear" | "attention" + summary: string + escalate: boolean +} + +export interface DaemonState { + timestamp: string + triage: TriageResult + signals: { + new_failures: number + new_learnings: number + } +} + +/** Read the latest daemon state. Returns null if no state file exists. */ +export function readDaemonState(): DaemonState | null { + if (!fs.existsSync(STATE_FILE)) return null + + try { + const raw = fs.readFileSync(STATE_FILE, "utf-8") + return JSON.parse(raw) as DaemonState + } catch { + return null + } +} + +/** Check if the heartbeat is stale (older than threshold minutes). */ +export function isHeartbeatStale(state: DaemonState, thresholdMinutes = 30): boolean { + const lastBeat = new Date(state.timestamp).getTime() + const now = Date.now() + return (now - lastBeat) > thresholdMinutes * 60 * 1000 +} +``` + +**Step 2: Verify it compiles** + +Run: `cd Packs/pai-telos-skill/src/DashboardTemplate && npx tsc --noEmit lib/daemon-state.ts` +Expected: No errors + +**Step 3: Commit** + +```bash +git add Packs/pai-telos-skill/src/DashboardTemplate/lib/daemon-state.ts +git commit -m "feat: add daemon-state library for reading heartbeat state" +``` + +--- + +### Task 4: Create Daemon State API Route + +**Files:** +- Create: `Packs/pai-telos-skill/src/DashboardTemplate/app/api/pai/heartbeat/route.ts` + +**Context:** GET endpoint returning the current daemon state. Follows the same pattern as `app/api/cc-mirror/variants/route.ts` — `force-dynamic`, try/catch, `NextResponse.json`. + +**Step 1: Write the API route** + +```typescript +import { NextResponse } from "next/server" +import { readDaemonState, isHeartbeatStale } from "@/lib/daemon-state" + +export const dynamic = "force-dynamic" + +/** GET /api/pai/heartbeat — return latest daemon heartbeat state. */ +export async function GET() { + try { + const state = readDaemonState() + if (!state) { + return NextResponse.json({ + configured: false, + state: null, + stale: false, + }) + } + + return NextResponse.json({ + configured: true, + state, + stale: isHeartbeatStale(state), + }) + } catch (error) { + console.error("Failed to read daemon state:", error) + return NextResponse.json( + { error: "Failed to read daemon state" }, + { status: 500 } + ) + } +} +``` + +**Step 2: Verify build** + +Run: `cd Packs/pai-telos-skill/src/DashboardTemplate && npx next build` +Expected: Compiles with 0 errors (new route appears in build output) + +**Step 3: Commit** + +```bash +git add Packs/pai-telos-skill/src/DashboardTemplate/app/api/pai/heartbeat/route.ts +git commit -m "feat: add GET /api/pai/heartbeat endpoint for daemon state" +``` + +--- + +## Phase 2: Dashboard Bridges + +### Task 5: Install Radix Tabs and Create Tabs UI Component + +**Files:** +- Create: `Packs/pai-telos-skill/src/DashboardTemplate/components/ui/tabs.tsx` + +**Context:** The dashboard uses Radix UI primitives (see `@radix-ui/react-slot` in `package.json`). The Memory, Logs, and Settings pages need tab navigation to show both PAI and IronClaw views. No tabs component exists yet — create one following the same pattern as `components/ui/button.tsx` (Radix primitive + CVA + cn utility). + +**Step 1: Install Radix Tabs** + +Run: `cd Packs/pai-telos-skill/src/DashboardTemplate && bun add @radix-ui/react-tabs` + +**Step 2: Create the tabs component** + +```tsx +"use client" + +import * as React from "react" +import * as TabsPrimitive from "@radix-ui/react-tabs" +import { cn } from "@/lib/utils" + +const Tabs = TabsPrimitive.Root + +const TabsList = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsList.displayName = TabsPrimitive.List.displayName + +const TabsTrigger = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName + +const TabsContent = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsContent.displayName = TabsPrimitive.Content.displayName + +export { Tabs, TabsList, TabsTrigger, TabsContent } +``` + +**Step 3: Verify it compiles** + +Run: `cd Packs/pai-telos-skill/src/DashboardTemplate && npx tsc --noEmit components/ui/tabs.tsx` +Expected: No errors + +**Step 4: Commit** + +```bash +git add Packs/pai-telos-skill/src/DashboardTemplate/components/ui/tabs.tsx Packs/pai-telos-skill/src/DashboardTemplate/bun.lock Packs/pai-telos-skill/src/DashboardTemplate/package.json +git commit -m "feat: add Radix Tabs UI component for bridged dashboard views" +``` + +--- + +### Task 6: Create PAI Memory API Route + +**Files:** +- Create: `Packs/pai-telos-skill/src/DashboardTemplate/app/api/pai/memory/route.ts` + +**Context:** Reads PAI's native memory from `~/.claude/MEMORY/LEARNING/learnings.jsonl` and the LEARNING subdirectories (ALGORITHM, SYSTEM, FAILURES, SYNTHESIS). Returns structured data the dashboard can display. Does NOT touch IronClaw memory — that stays on the existing proxy. + +**Step 1: Write the API route** + +```typescript +import { NextResponse } from "next/server" +import fs from "fs" +import path from "path" +import os from "os" + +export const dynamic = "force-dynamic" + +const MEMORY_BASE = path.join(os.homedir(), ".claude", "MEMORY") +const LEARNINGS_FILE = path.join(MEMORY_BASE, "LEARNING", "learnings.jsonl") + +interface PAILearning { + id: string + text: string + concept?: string + tier?: string + timestamp?: string + source?: string +} + +interface PAIMemoryFile { + name: string + path: string + category: string +} + +/** GET /api/pai/memory — list PAI native memory entries. */ +export async function GET() { + try { + const learnings: PAILearning[] = [] + const files: PAIMemoryFile[] = [] + + // Read structured learnings from JSONL + if (fs.existsSync(LEARNINGS_FILE)) { + const raw = fs.readFileSync(LEARNINGS_FILE, "utf-8") + const lines = raw.split("\n").filter((l) => l.trim()) + for (const line of lines) { + try { + const entry = JSON.parse(line) as PAILearning + learnings.push(entry) + } catch { + // Skip malformed lines + } + } + } + + // Scan LEARNING subdirectories for markdown files + const categories = ["ALGORITHM", "SYSTEM", "FAILURES", "SYNTHESIS"] + for (const cat of categories) { + const catDir = path.join(MEMORY_BASE, "LEARNING", cat) + if (!fs.existsSync(catDir)) continue + const entries = fs.readdirSync(catDir, { withFileTypes: true, recursive: true }) + for (const entry of entries) { + if (entry.isDirectory()) continue + if (!entry.name.endsWith(".md")) continue + const relPath = path.relative(MEMORY_BASE, path.join(catDir, entry.name)) + files.push({ + name: entry.name, + path: relPath, + category: cat, + }) + } + } + + return NextResponse.json({ + learningCount: learnings.length, + learnings: learnings.slice(-50), // Last 50 + fileCount: files.length, + files, + }) + } catch (error) { + console.error("Failed to read PAI memory:", error) + return NextResponse.json( + { error: "Failed to read PAI memory" }, + { status: 500 } + ) + } +} +``` + +**Step 2: Verify build** + +Run: `cd Packs/pai-telos-skill/src/DashboardTemplate && npx next build` +Expected: Compiles, new `/api/pai/memory` route in output + +**Step 3: Commit** + +```bash +git add Packs/pai-telos-skill/src/DashboardTemplate/app/api/pai/memory/route.ts +git commit -m "feat: add GET /api/pai/memory for PAI native memory data" +``` + +--- + +### Task 7: Create PAI Logs API Route + +**Files:** +- Create: `Packs/pai-telos-skill/src/DashboardTemplate/app/api/pai/logs/route.ts` + +**Context:** Reads PAI's debug logs from `~/.claude/debug/` directory. Returns the most recent log entries. IronClaw logs stay on their SSE-based proxy (`/api/ironclaw/logs/events`). + +**Step 1: Write the API route** + +```typescript +import { NextResponse } from "next/server" +import fs from "fs" +import path from "path" +import os from "os" + +export const dynamic = "force-dynamic" + +const DEBUG_DIR = path.join(os.homedir(), ".claude", "debug") + +interface PAILogEntry { + timestamp: string + level: string + message: string + source: string +} + +/** GET /api/pai/logs — return recent PAI debug log entries. */ +export async function GET() { + try { + if (!fs.existsSync(DEBUG_DIR)) { + return NextResponse.json({ entries: [], total: 0 }) + } + + // Find log files sorted by modification time (newest first) + const logFiles = fs.readdirSync(DEBUG_DIR) + .filter((f) => f.endsWith(".log") || f.endsWith(".jsonl")) + .map((f) => ({ + name: f, + mtime: fs.statSync(path.join(DEBUG_DIR, f)).mtimeMs, + })) + .sort((a, b) => b.mtime - a.mtime) + .slice(0, 5) // Last 5 log files + + const entries: PAILogEntry[] = [] + + for (const file of logFiles) { + const content = fs.readFileSync(path.join(DEBUG_DIR, file.name), "utf-8") + const lines = content.split("\n").filter((l) => l.trim()).slice(-100) // Last 100 lines per file + + for (const line of lines) { + // Try JSON parse first + try { + const parsed = JSON.parse(line) as Record + entries.push({ + timestamp: (parsed.timestamp as string) || new Date(file.mtime).toISOString(), + level: (parsed.level as string) || "info", + message: (parsed.message as string) || line, + source: file.name, + }) + continue + } catch { + // Not JSON — treat as plain text log line + } + + // Plain text: try to extract timestamp and level from common formats + // e.g., "2026-02-17T00:11:48Z [INFO] message" + const match = line.match(/^(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}\S*)\s+\[?(\w+)]?\s+(.+)/) + if (match) { + entries.push({ + timestamp: match[1] ?? new Date(file.mtime).toISOString(), + level: (match[2] ?? "info").toLowerCase(), + message: match[3] ?? line, + source: file.name, + }) + } else { + entries.push({ + timestamp: new Date(file.mtime).toISOString(), + level: "info", + message: line, + source: file.name, + }) + } + } + } + + // Sort by timestamp descending, return last 500 + entries.sort((a, b) => b.timestamp.localeCompare(a.timestamp)) + + return NextResponse.json({ + entries: entries.slice(0, 500), + total: entries.length, + }) + } catch (error) { + console.error("Failed to read PAI logs:", error) + return NextResponse.json( + { error: "Failed to read PAI logs" }, + { status: 500 } + ) + } +} +``` + +**Step 2: Verify build** + +Run: `cd Packs/pai-telos-skill/src/DashboardTemplate && npx next build` +Expected: Compiles with 0 errors + +**Step 3: Commit** + +```bash +git add Packs/pai-telos-skill/src/DashboardTemplate/app/api/pai/logs/route.ts +git commit -m "feat: add GET /api/pai/logs for PAI debug log entries" +``` + +--- + +### Task 8: Create PAI Settings API Route + +**Files:** +- Create: `Packs/pai-telos-skill/src/DashboardTemplate/app/api/pai/settings/route.ts` + +**Context:** Reads PAI's settings from `~/.claude/settings.json` and `~/.claude/settings.local.json`. Returns safe keys only (no auth tokens). IronClaw settings stay on their existing proxy. + +**Step 1: Write the API route** + +```typescript +import { NextResponse } from "next/server" +import fs from "fs" +import path from "path" +import os from "os" + +export const dynamic = "force-dynamic" + +const CLAUDE_DIR = path.join(os.homedir(), ".claude") +const SETTINGS_FILE = path.join(CLAUDE_DIR, "settings.json") +const LOCAL_SETTINGS_FILE = path.join(CLAUDE_DIR, "settings.local.json") + +/** Keys that are safe to expose to the dashboard. Never expose auth tokens. */ +const SAFE_KEYS = new Set([ + "principal", + "daidentity", + "model", + "theme", + "permissions", + "preferredNotifType", +]) + +/** Sensitive key patterns to always exclude. */ +const SENSITIVE_PATTERNS = [/token/i, /secret/i, /password/i, /key/i, /auth/i] + +function isSafeKey(key: string): boolean { + if (SENSITIVE_PATTERNS.some((p) => p.test(key))) return false + return true +} + +interface SettingsFile { + [key: string]: unknown +} + +function readSettings(filePath: string): SettingsFile { + if (!fs.existsSync(filePath)) return {} + try { + return JSON.parse(fs.readFileSync(filePath, "utf-8")) as SettingsFile + } catch { + return {} + } +} + +/** GET /api/pai/settings — return safe PAI settings (no auth tokens). */ +export async function GET() { + try { + const global = readSettings(SETTINGS_FILE) + const local = readSettings(LOCAL_SETTINGS_FILE) + + // Filter to safe keys only + const safeGlobal: SettingsFile = {} + for (const [key, value] of Object.entries(global)) { + if (isSafeKey(key)) safeGlobal[key] = value + } + + const safeLocal: SettingsFile = {} + for (const [key, value] of Object.entries(local)) { + if (isSafeKey(key)) safeLocal[key] = value + } + + return NextResponse.json({ + global: safeGlobal, + local: safeLocal, + globalPath: SETTINGS_FILE, + localPath: LOCAL_SETTINGS_FILE, + }) + } catch (error) { + console.error("Failed to read PAI settings:", error) + return NextResponse.json( + { error: "Failed to read PAI settings" }, + { status: 500 } + ) + } +} +``` + +**Step 2: Verify build** + +Run: `cd Packs/pai-telos-skill/src/DashboardTemplate && npx next build` +Expected: Compiles with 0 errors + +**Step 3: Commit** + +```bash +git add Packs/pai-telos-skill/src/DashboardTemplate/app/api/pai/settings/route.ts +git commit -m "feat: add GET /api/pai/settings for safe PAI config display" +``` + +--- + +### Task 9: Bridge the Memory Page with Tabs + +**Files:** +- Modify: `Packs/pai-telos-skill/src/DashboardTemplate/app/memory/page.tsx` (rewrite ~240 lines → ~310 lines) + +**Context:** The existing memory page (line 1-239) shows only IronClaw memory. Transform it into a tabbed view: "PAI Memory" tab (reads `/api/pai/memory`) and "IronClaw Memory" tab (keeps existing tree browser). The IronClaw tab preserves all existing behavior including the `FileTree` component, edit/save, and offline handling. + +**Step 1: Rewrite the memory page with tabs** + +The full page has two tabs. The IronClaw tab contains all the existing code from `MemoryPage` (the tree browser with edit/save). The PAI tab shows learnings from JSONL + markdown file listing. + +Key changes: +- Add imports: `Tabs, TabsList, TabsTrigger, TabsContent` from `@/components/ui/tabs` +- Add `Brain` icon from lucide-react for the PAI tab +- Add state for PAI memory data: `paiLearnings`, `paiFiles`, `paiLoading` +- Fetch `/api/pai/memory` on mount alongside the existing IronClaw fetch +- Default tab: "pai" (since PAI memory works even when IronClaw is offline) + +```tsx +// New imports to ADD at top (keep all existing imports): +import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs" +import { Brain } from "lucide-react" // ADD to existing lucide import +``` + +**Structure:** + +``` + + + PAI Memory + IronClaw Memory + + + {/* Learnings list + file listing from /api/pai/memory */} + + + {/* Existing tree browser — ALL existing code moves here unchanged */} + + +``` + +**PAI Memory tab content:** + +```tsx +// Inside TabsContent value="pai" +{paiLoading ? ( +
Loading...
+) : ( +
+ {/* Recent Learnings */} + + + + + Recent Learnings + {paiLearnings.length} + + + + {paiLearnings.length === 0 ? ( +

No learnings captured yet

+ ) : ( +
+ {paiLearnings.map((learning, idx) => ( +
+

{learning.text}

+
+ {learning.concept && {learning.concept}} + {learning.tier && {learning.tier}} +
+
+ ))} +
+ )} +
+
+ + {/* Memory Files by Category */} + + + Memory Files + + + {paiFiles.length === 0 ? ( +

No memory files found

+ ) : ( +
+ {paiFiles.map((file) => ( +
+ {file.category} + {file.name} +
+ ))} +
+ )} +
+
+
+)} +``` + +**Step 2: Verify build** + +Run: `cd Packs/pai-telos-skill/src/DashboardTemplate && npx next build` +Expected: Compiles with 0 errors. Memory page route still present. + +**Step 3: Manual verification** + +Run: `cd Packs/pai-telos-skill/src/DashboardTemplate && bun dev` +Navigate to `/memory` — should see two tabs. PAI Memory tab shows learnings. IronClaw Memory tab shows tree browser (or offline message). + +**Step 4: Commit** + +```bash +git add Packs/pai-telos-skill/src/DashboardTemplate/app/memory/page.tsx +git commit -m "feat: bridge Memory page with PAI + IronClaw tabs" +``` + +--- + +### Task 10: Bridge the Logs Page with Tabs + +**Files:** +- Modify: `Packs/pai-telos-skill/src/DashboardTemplate/app/logs/page.tsx` (rewrite ~247 lines → ~330 lines) + +**Context:** The existing logs page (line 1-247) shows only IronClaw SSE logs. Transform it into a tabbed view: "PAI Logs" tab (reads `/api/pai/logs`, static list with refresh) and "IronClaw Logs" tab (keeps existing SSE stream with pause/play/filter). The IronClaw tab preserves ALL existing behavior. + +**Step 1: Rewrite the logs page with tabs** + +Key changes: +- Add imports: `Tabs, TabsList, TabsTrigger, TabsContent` from `@/components/ui/tabs` +- Add `RefreshCw` icon from lucide-react for PAI logs refresh button +- Add state: `paiLogs`, `paiLoading`, `paiLevelFilter` +- Fetch `/api/pai/logs` on mount +- Default tab: "ironclaw" (since live streaming is the primary use case) + +**PAI Logs tab content:** + +```tsx +// Inside TabsContent value="pai" + + +
+ + + {filteredPaiLogs.length} entries +
+
+
+{/* Same log entry rendering pattern as IronClaw tab */} +``` + +**Step 2: Verify build** + +Run: `cd Packs/pai-telos-skill/src/DashboardTemplate && npx next build` +Expected: Compiles with 0 errors + +**Step 3: Manual verification** + +Navigate to `/logs` — two tabs. IronClaw Logs tab streams live. PAI Logs tab shows debug log entries with refresh. + +**Step 4: Commit** + +```bash +git add Packs/pai-telos-skill/src/DashboardTemplate/app/logs/page.tsx +git commit -m "feat: bridge Logs page with PAI + IronClaw tabs" +``` + +--- + +### Task 11: Bridge the Settings Page with Tabs + +**Files:** +- Modify: `Packs/pai-telos-skill/src/DashboardTemplate/app/settings/page.tsx` (rewrite ~337 lines → ~420 lines) + +**Context:** The existing settings page (line 1-337) shows only IronClaw settings with CRUD + import/export. Transform it into a tabbed view: "PAI Settings" tab (reads `/api/pai/settings`, read-only display of safe config) and "IronClaw Settings" tab (keeps ALL existing behavior including add/edit/delete/import/export). The IronClaw tab preserves everything unchanged. + +**Step 1: Rewrite the settings page with tabs** + +Key changes: +- Add imports: `Tabs, TabsList, TabsTrigger, TabsContent` from `@/components/ui/tabs` +- Add state: `paiSettings`, `paiLoading` +- Fetch `/api/pai/settings` on mount +- Default tab: "ironclaw" (since CRUD is the primary use case) + +**PAI Settings tab content:** + +```tsx +// Inside TabsContent value="pai" +{paiLoading ? ( +
Loading...
+) : ( +
+ + + Global Settings +

{paiSettings.globalPath}

+
+ + {Object.keys(paiSettings.global).length === 0 ? ( +

No global settings

+ ) : ( + + + + Key + Value + + + + {Object.entries(paiSettings.global).map(([key, value]) => ( + + {key} + + {typeof value === "object" ? ( +
+                        {JSON.stringify(value, null, 2)}
+                      
+ ) : ( + String(value) + )} +
+
+ ))} +
+
+ )} +
+
+ + {/* Same pattern for local settings */} + + + Local Settings +

{paiSettings.localPath}

+
+ + {/* Same Table pattern as global */} + +
+
+)} +``` + +**Step 2: Verify build** + +Run: `cd Packs/pai-telos-skill/src/DashboardTemplate && npx next build` +Expected: Compiles with 0 errors + +**Step 3: Manual verification** + +Navigate to `/settings` — two tabs. IronClaw Settings works exactly as before. PAI Settings shows read-only config tables. + +**Step 4: Commit** + +```bash +git add Packs/pai-telos-skill/src/DashboardTemplate/app/settings/page.tsx +git commit -m "feat: bridge Settings page with PAI + IronClaw tabs" +``` + +--- + +### Task 12: Add SSE Circuit Breaker (RedTeam Fix) + +**Files:** +- Modify: `Packs/pai-telos-skill/src/DashboardTemplate/lib/use-ironclaw-sse.ts` (55 lines → ~75 lines) + +**Context:** RedTeam flagged the SSE reconnect loop as MEDIUM risk — it retries every 3 seconds indefinitely with no backoff, which can hammer IronClaw if it's flapping. Add exponential backoff with a max retry cap. + +**Step 1: Add backoff to the SSE hook** + +Modify `use-ironclaw-sse.ts` to track retry count and use exponential backoff: + +```typescript +// Add inside the hook, before connect(): +const retryCountRef = useRef(0) +const MAX_RETRIES = 10 +const BASE_DELAY = 3000 // 3s + +// Replace the onerror handler's setTimeout: +es.onerror = () => { + es.close() + retryCountRef.current += 1 + if (retryCountRef.current > MAX_RETRIES) return // Stop after max retries + const delay = Math.min(BASE_DELAY * Math.pow(2, retryCountRef.current - 1), 60000) // Cap at 60s + setTimeout(() => { + if (enabled) connect() + }, delay) +} + +// Reset retry count on successful connection: +es.onopen = () => { + retryCountRef.current = 0 +} +``` + +**Step 2: Verify build** + +Run: `cd Packs/pai-telos-skill/src/DashboardTemplate && npx next build` +Expected: Compiles with 0 errors + +**Step 3: Commit** + +```bash +git add Packs/pai-telos-skill/src/DashboardTemplate/lib/use-ironclaw-sse.ts +git commit -m "fix: add exponential backoff to SSE reconnect (RedTeam MEDIUM fix)" +``` + +--- + +### Task 13: Update Page Descriptions in Sidebar and Headers + +**Files:** +- Modify: `Packs/pai-telos-skill/src/DashboardTemplate/app/memory/page.tsx` (header text only) +- Modify: `Packs/pai-telos-skill/src/DashboardTemplate/app/logs/page.tsx` (header text only) +- Modify: `Packs/pai-telos-skill/src/DashboardTemplate/app/settings/page.tsx` (header text only) + +**Context:** The page headers currently say "IronClaw memory and knowledge base", "System logs and audit trail", "IronClaw configuration and preferences". Update to reflect unified views. + +**Step 1: Update header descriptions** + +Memory page `

` tag (line 143 in current file): +- Old: `IronClaw memory and knowledge base` +- New: `PAI and IronClaw memory systems` + +Logs page `

` tag (line 152 in current file): +- Old: `System logs and audit trail` +- New: `PAI debug logs and IronClaw system logs` + +Settings page `

` tag (line 177 in current file): +- Old: `IronClaw configuration and preferences` +- New: `PAI and IronClaw configuration` + +**Step 2: Commit** + +```bash +git add Packs/pai-telos-skill/src/DashboardTemplate/app/memory/page.tsx Packs/pai-telos-skill/src/DashboardTemplate/app/logs/page.tsx Packs/pai-telos-skill/src/DashboardTemplate/app/settings/page.tsx +git commit -m "chore: update page descriptions for unified PAI + IronClaw views" +``` + +--- + +### Task 14: Full Build Verification + +**Files:** None (verification only) + +**Step 1: Run the full build** + +Run: `cd Packs/pai-telos-skill/src/DashboardTemplate && npx next build` +Expected: Compiles with 0 errors. All routes present: +- `/api/pai/heartbeat` +- `/api/pai/memory` +- `/api/pai/logs` +- `/api/pai/settings` +- All existing routes unchanged + +**Step 2: Verify no regressions** + +Run: `cd Packs/pai-telos-skill/src/DashboardTemplate && bun dev` + +Check each bridged page: +- `/memory` — Two tabs. PAI Memory loads. IronClaw Memory loads (or shows offline). +- `/logs` — Two tabs. IronClaw Logs streams (or offline). PAI Logs shows debug entries. +- `/settings` — Two tabs. IronClaw Settings has full CRUD. PAI Settings shows read-only config. +- `/inference` — Still works (CC-Mirror variants). +- `/agents` — Still works (IronClaw proxy). +- `/` — Overview still loads. + +**Step 3: Commit summary** + +No commit needed — this is a verification step. + +--- + +## Phase 3-4: Future Tasks (Skeleton Only) + +These phases are documented in the design doc but not detailed here. They should get their own implementation plans when Phase 2 is complete. + +### Phase 3: Anti-Corruption Layer +- Create `types/pai.ts` with PAI-owned type definitions +- Create `lib/pai-adapters.ts` with mapping functions from IronClaw types +- Migrate dashboard components to use PAI types +- Confine IronClaw types to `lib/ironclaw.ts` proxy layer + +### Phase 4: Security Hierarchy +- Create policy compiler (IronClaw constraints + SecurityValidator rules) +- Add `PAI_DEPLOY_CONTEXT=daemon` handling to SecurityValidator +- Create build-time validator for policy conflicts + +--- + +## File Summary + +| Task | File | Action | Est. LOC | +|------|------|--------|----------| +| 1 | `Tools/heartbeat-triage.sh` | CREATE | ~60 | +| 2 | `docs/HEARTBEAT.md` | CREATE | ~45 | +| 3 | `lib/daemon-state.ts` | CREATE | ~35 | +| 4 | `app/api/pai/heartbeat/route.ts` | CREATE | ~30 | +| 5 | `components/ui/tabs.tsx` | CREATE | ~55 | +| 6 | `app/api/pai/memory/route.ts` | CREATE | ~70 | +| 7 | `app/api/pai/logs/route.ts` | CREATE | ~80 | +| 8 | `app/api/pai/settings/route.ts` | CREATE | ~65 | +| 9 | `app/memory/page.tsx` | MODIFY | +70 | +| 10 | `app/logs/page.tsx` | MODIFY | +80 | +| 11 | `app/settings/page.tsx` | MODIFY | +80 | +| 12 | `lib/use-ironclaw-sse.ts` | MODIFY | +20 | +| 13 | 3 page headers | MODIFY | +3 | +| 14 | (verification only) | — | — | + +**Total: 8 new files, 6 modified. ~690 LOC across Phases 0-2.** From 7d27397f0817228cabaf6ef24669b0ef3127635f Mon Sep 17 00:00:00 2001 From: Curtis Date: Tue, 17 Feb 2026 01:25:11 -0700 Subject: [PATCH 03/34] feat: add PAI heartbeat triage script and documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 0 of unified architecture — IronClaw cron invokes heartbeat-triage.sh for lightweight PAI system health checks with escalation to full sessions. Co-Authored-By: Claude Opus 4.6 --- .../src/skills/CORE/Tools/heartbeat-triage.sh | 72 ++++++++++++++++ .../src/skills/CORE/docs/HEARTBEAT.md | 85 +++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100755 Packs/pai-core-install/src/skills/CORE/Tools/heartbeat-triage.sh create mode 100644 Packs/pai-core-install/src/skills/CORE/docs/HEARTBEAT.md diff --git a/Packs/pai-core-install/src/skills/CORE/Tools/heartbeat-triage.sh b/Packs/pai-core-install/src/skills/CORE/Tools/heartbeat-triage.sh new file mode 100755 index 000000000..765d9df3a --- /dev/null +++ b/Packs/pai-core-install/src/skills/CORE/Tools/heartbeat-triage.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +set -euo pipefail + +# PAI Heartbeat Triage — invoked by IronClaw on a cron schedule. +# Lightweight: uses haiku with no tools/hooks to decide if escalation is needed. + +STATE_DIR="$HOME/.claude/MEMORY/STATE" +STATE_FILE="$STATE_DIR/daemon-state.json" +LOG_DIR="$HOME/.claude/logs" + +mkdir -p "$STATE_DIR" "$LOG_DIR" + +# Bootstrap state file on first run +if [[ ! -f "$STATE_FILE" ]]; then + printf '{"last_check":0,"status":"idle","escalation_count":0}\n' > "$STATE_FILE" +fi + +prev_state=$(cat "$STATE_FILE") +last_check=$(echo "$prev_state" | jq -r '.last_check // 0') +now=$(date +%s) + +# -- Gather signals since last check -- +# Recent failures from the last hour of logs +recent_failures=$(find "$LOG_DIR" -name '*.error' -newer "$STATE_FILE" -exec tail -1 {} + 2>/dev/null | head -20 || true) + +# New learnings captured since last heartbeat +learnings="" +if [[ -d "$HOME/.claude/MEMORY" ]]; then + learnings=$(find "$HOME/.claude/MEMORY" -name '*.md' -newer "$STATE_FILE" -printf '%f\n' 2>/dev/null | head -10 || true) +fi + +# -- Triage via lightweight Claude session (no tools, no hooks) -- +triage_prompt="Previous state: $prev_state +Time since last check: $(( now - last_check ))s +Recent failures: ${recent_failures:-none} +New learnings: ${learnings:-none} + +Respond with ONLY valid JSON: {\"action\":\"idle|escalate\",\"reason\":\"...\",\"priority\":\"low|medium|high\"}" + +triage_result=$(claude --print \ + --model haiku \ + --tools '' \ + --output-format text \ + --setting-sources '' \ + --system-prompt "You are a PAI system health triage agent. Analyze signals and decide: idle (nothing actionable) or escalate (needs a full session). Be conservative — only escalate for real issues." \ + "$triage_prompt" 2>/dev/null) || triage_result='{"action":"idle","reason":"triage call failed","priority":"low"}' + +# -- Persist updated state -- +action=$(echo "$triage_result" | jq -r '.action // "idle"') +reason=$(echo "$triage_result" | jq -r '.reason // "no reason"') +esc_count=$(echo "$prev_state" | jq -r '.escalation_count // 0') + +if [[ "$action" == "escalate" ]]; then + esc_count=$(( esc_count + 1 )) +fi + +jq -n \ + --argjson now "$now" \ + --arg status "$action" \ + --arg reason "$reason" \ + --argjson esc_count "$esc_count" \ + '{last_check: $now, status: $status, last_reason: $reason, escalation_count: $esc_count}' \ + > "$STATE_FILE" + +# -- Escalate: spawn a full Claude session with hooks enabled -- +if [[ "$action" == "escalate" ]]; then + nohup claude --print \ + --model sonnet \ + --system-prompt "PAI escalation session. Triage reason: $reason. Investigate and resolve." \ + "Heartbeat triage escalated. Reason: $reason. Recent failures: ${recent_failures:-none}" \ + >> "$LOG_DIR/escalation-$(date +%Y%m%d-%H%M%S).log" 2>&1 & +fi diff --git a/Packs/pai-core-install/src/skills/CORE/docs/HEARTBEAT.md b/Packs/pai-core-install/src/skills/CORE/docs/HEARTBEAT.md new file mode 100644 index 000000000..2b0513596 --- /dev/null +++ b/Packs/pai-core-install/src/skills/CORE/docs/HEARTBEAT.md @@ -0,0 +1,85 @@ +# PAI Heartbeat Triage + +Periodic health check for the PAI system, designed to be invoked by IronClaw's cron scheduler. + +## Overview + +The heartbeat pattern separates **triage** (cheap, fast, read-only) from **action** (full session with tools and hooks). This keeps background monitoring costs minimal while preserving the ability to escalate when something actually needs attention. + +``` +IronClaw cron --> heartbeat-triage.sh --> lightweight Claude (haiku, no tools) + | + idle? done. + | + escalate? --> full Claude session (sonnet, hooks enabled) +``` + +## IronClaw Routine Configuration + +Register the heartbeat as an IronClaw routine: + +```json +{ + "id": "pai-heartbeat", + "schedule": "*/15 * * * *", + "command": "Packs/pai-core-install/src/skills/CORE/Tools/heartbeat-triage.sh", + "timeout_seconds": 30, + "retry": { + "max_attempts": 1, + "backoff_ms": 0 + }, + "tags": ["health", "triage"] +} +``` + +The 15-minute interval balances responsiveness with API cost. Adjust based on system activity. + +## Invocation Modes + +| Aspect | Lightweight Triage | Full Escalation | +|---|---|---| +| **Model** | haiku | sonnet | +| **Tools** | Disabled (`--tools ''`) | All available | +| **Hooks** | Disabled (`--setting-sources ''`) | All enabled | +| **Output** | JSON decision only | Investigative session | +| **Cost** | Minimal | Standard | +| **Duration** | < 5 seconds | Variable | +| **Trigger** | IronClaw cron | Triage script (background) | + +## State Persistence + +Triage state lives at `~/.claude/MEMORY/STATE/daemon-state.json`: + +```json +{ + "last_check": 1708185600, + "status": "idle", + "last_reason": "no actionable signals", + "escalation_count": 0 +} +``` + +| Field | Purpose | +|---|---| +| `last_check` | Unix timestamp of last triage run | +| `status` | Result of last triage (`idle` or `escalate`) | +| `last_reason` | Human-readable explanation from triage model | +| `escalation_count` | Running total of escalations for observability | + +The state file also serves as a timestamp reference -- `find -newer` uses it to scope signal gathering to only what's changed since the last check. + +## Security Model + +**Triage session (haiku):** +- Cannot execute tools (`--tools ''`) +- Cannot trigger hooks (`--setting-sources ''`) +- Read-only analysis of log snippets and file names +- Can only output a JSON verdict + +**Escalated session (sonnet):** +- Full tool access for investigation and remediation +- All security hooks active (SecurityValidator, etc.) +- Runs as a background process with output logged +- Standard PAI permission model applies + +This two-tier design ensures that the high-frequency triage path has zero write capability, while escalated sessions get the full security stack. From da24999c66d618dca92d882baebac93356a9d160 Mon Sep 17 00:00:00 2001 From: Curtis Date: Tue, 17 Feb 2026 01:26:50 -0700 Subject: [PATCH 04/34] feat: add daemon-state library and heartbeat API route MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1 state bridge — dashboard can read heartbeat triage results from daemon-state.json written by heartbeat-triage.sh. Co-Authored-By: Claude Opus 4.6 --- .../App/api/pai/heartbeat/route.ts | 29 ++++++++++++++ .../src/DashboardTemplate/Lib/daemon-state.ts | 38 +++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 Packs/pai-telos-skill/src/DashboardTemplate/App/api/pai/heartbeat/route.ts create mode 100644 Packs/pai-telos-skill/src/DashboardTemplate/Lib/daemon-state.ts diff --git a/Packs/pai-telos-skill/src/DashboardTemplate/App/api/pai/heartbeat/route.ts b/Packs/pai-telos-skill/src/DashboardTemplate/App/api/pai/heartbeat/route.ts new file mode 100644 index 000000000..a252e729b --- /dev/null +++ b/Packs/pai-telos-skill/src/DashboardTemplate/App/api/pai/heartbeat/route.ts @@ -0,0 +1,29 @@ +import { NextResponse } from "next/server" +import { readDaemonState, isHeartbeatStale } from "@/lib/daemon-state" + +export const dynamic = "force-dynamic" + +/** GET /api/pai/heartbeat — return latest daemon heartbeat state. */ +export async function GET() { + try { + const state = readDaemonState() + if (!state) { + return NextResponse.json({ + configured: false, + state: null, + stale: false, + }) + } + return NextResponse.json({ + configured: true, + state, + stale: isHeartbeatStale(state), + }) + } catch (error) { + console.error("Failed to read daemon state:", error) + return NextResponse.json( + { error: "Failed to read daemon state" }, + { status: 500 } + ) + } +} diff --git a/Packs/pai-telos-skill/src/DashboardTemplate/Lib/daemon-state.ts b/Packs/pai-telos-skill/src/DashboardTemplate/Lib/daemon-state.ts new file mode 100644 index 000000000..6b0baa55c --- /dev/null +++ b/Packs/pai-telos-skill/src/DashboardTemplate/Lib/daemon-state.ts @@ -0,0 +1,38 @@ +import fs from "fs" +import path from "path" +import os from "os" + +const STATE_FILE = path.join(os.homedir(), ".claude", "MEMORY", "STATE", "daemon-state.json") + +export interface TriageResult { + status: "clear" | "attention" + summary: string + escalate: boolean +} + +export interface DaemonState { + timestamp: string + triage: TriageResult + signals: { + new_failures: number + new_learnings: number + } +} + +/** Read the latest daemon state. Returns null if no state file exists. */ +export function readDaemonState(): DaemonState | null { + if (!fs.existsSync(STATE_FILE)) return null + try { + const raw = fs.readFileSync(STATE_FILE, "utf-8") + return JSON.parse(raw) as DaemonState + } catch { + return null + } +} + +/** Check if the heartbeat is stale (older than threshold minutes). */ +export function isHeartbeatStale(state: DaemonState, thresholdMinutes = 30): boolean { + const lastBeat = new Date(state.timestamp).getTime() + const now = Date.now() + return (now - lastBeat) > thresholdMinutes * 60 * 1000 +} From a0f785b31ee7921530a4463529e2b41c644ac048 Mon Sep 17 00:00:00 2001 From: Curtis Date: Tue, 17 Feb 2026 01:27:14 -0700 Subject: [PATCH 05/34] feat: add Radix Tabs UI component for bridged dashboard views Co-Authored-By: Claude Opus 4.6 --- .../DashboardTemplate/Components/Ui/tabs.tsx | 54 +++++ .../src/DashboardTemplate/bun.lock | 223 ++++++++++++++++++ .../src/DashboardTemplate/package.json | 4 + 3 files changed, 281 insertions(+) create mode 100644 Packs/pai-telos-skill/src/DashboardTemplate/Components/Ui/tabs.tsx mode change 100755 => 100644 Packs/pai-telos-skill/src/DashboardTemplate/bun.lock diff --git a/Packs/pai-telos-skill/src/DashboardTemplate/Components/Ui/tabs.tsx b/Packs/pai-telos-skill/src/DashboardTemplate/Components/Ui/tabs.tsx new file mode 100644 index 000000000..bc8d1b0c3 --- /dev/null +++ b/Packs/pai-telos-skill/src/DashboardTemplate/Components/Ui/tabs.tsx @@ -0,0 +1,54 @@ +"use client" + +import * as React from "react" +import * as TabsPrimitive from "@radix-ui/react-tabs" +import { cn } from "@/lib/utils" + +const Tabs = TabsPrimitive.Root + +const TabsList = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsList.displayName = TabsPrimitive.List.displayName + +const TabsTrigger = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName + +const TabsContent = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsContent.displayName = TabsPrimitive.Content.displayName + +export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/Packs/pai-telos-skill/src/DashboardTemplate/bun.lock b/Packs/pai-telos-skill/src/DashboardTemplate/bun.lock old mode 100755 new mode 100644 index 64f5e5be0..72fdf06a9 --- a/Packs/pai-telos-skill/src/DashboardTemplate/bun.lock +++ b/Packs/pai-telos-skill/src/DashboardTemplate/bun.lock @@ -1,15 +1,20 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "telos-dashboard-template", "dependencies": { + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-tabs": "^1.1.13", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "gray-matter": "^4.0.3", "lucide-react": "^0.546.0", "next": "^15.5.6", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-markdown": "^10.1.0", "tailwind-merge": "^3.3.1", }, "devDependencies": { @@ -106,6 +111,36 @@ "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.5.6", "", { "os": "win32", "cpu": "x64" }, "sha512-pxK4VIjFRx1MY92UycLOOw7dTdvccWsNETQ0kDHkBlcFH1GrTLUjSiHU1ohrznnux6TqRHgv5oflhfIWZwVROQ=="], + "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], + + "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="], + + "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], + + "@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], + + "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="], + + "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="], + + "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], + + "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="], + + "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="], + + "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="], + + "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], + + "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="], + + "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="], + + "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], + "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], "@tailwindcss/node": ["@tailwindcss/node@4.1.16", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.19", "source-map-js": "^1.2.1", "tailwindcss": "4.1.16" } }, "sha512-BX5iaSsloNuvKNHRN3k2RcCuTEgASTo77mofW0vmeHkfrDWaoFAFvNHpEgtu0eqyypcyiBkDWzSMxJhp3AUVcw=="], @@ -140,14 +175,34 @@ "@types/bun": ["@types/bun@1.3.1", "", { "dependencies": { "bun-types": "1.3.1" } }, "sha512-4jNMk2/K9YJtfqwoAa28c8wK+T7nvJFOjxI4h/7sORWcypRNxBpr+TPNaCfVWq70tLCJsqoFwcf0oI0JU/fvMQ=="], + "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], + + "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], + + "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], + + "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], + "@types/node": ["@types/node@24.9.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA=="], "@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="], "@types/react-dom": ["@types/react-dom@19.2.2", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw=="], + "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], + + "argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + "autoprefixer": ["autoprefixer@10.4.21", "", { "dependencies": { "browserslist": "^4.24.4", "caniuse-lite": "^1.0.30001702", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ=="], + "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.8.23", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-616V5YX4bepJFzNyOfce5Fa8fDJMfoxzOIzDCZwaGL8MKVpFrXqfNUoIpRn9YMI5pXf/VKgzjB4htFMsFKKdiQ=="], "browserslist": ["browserslist@4.27.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", "electron-to-chromium": "^1.5.238", "node-releases": "^2.0.26", "update-browserslist-db": "^1.1.4" }, "bin": { "browserslist": "cli.js" } }, "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw=="], @@ -156,28 +211,82 @@ "caniuse-lite": ["caniuse-lite@1.0.30001753", "", {}, "sha512-Bj5H35MD/ebaOV4iDLqPEtiliTN29qkGtEHCwawWn4cYm+bPJM2NsaP30vtZcnERClMzp52J4+aw2UNbK4o+zw=="], + "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], + + "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], + + "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], + + "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], + + "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="], + "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], + + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], + "electron-to-chromium": ["electron-to-chromium@1.5.244", "", {}, "sha512-OszpBN7xZX4vWMPJwB9illkN/znA8M36GQqQxi6MNy9axWxhOfJyZZJtSLQCpEFLHP2xK33BiWx9aIuIEXVCcw=="], "enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], + + "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="], + + "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], + + "extend-shallow": ["extend-shallow@2.0.1", "", { "dependencies": { "is-extendable": "^0.1.0" } }, "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug=="], + "fraction.js": ["fraction.js@4.3.7", "", {}, "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="], "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + "gray-matter": ["gray-matter@4.0.3", "", { "dependencies": { "js-yaml": "^3.13.1", "kind-of": "^6.0.2", "section-matter": "^1.0.0", "strip-bom-string": "^1.0.0" } }, "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q=="], + + "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="], + + "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], + + "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], + + "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], + + "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="], + + "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="], + + "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="], + + "is-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="], + + "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], + + "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], + "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + "js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="], + + "kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="], + "lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="], "lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="], @@ -202,10 +311,72 @@ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="], + "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], + "lucide-react": ["lucide-react@0.546.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Z94u6fKT43lKeYHiVyvyR8fT7pwCzDu7RyMPpTvh054+xahSgj4HFQ+NmflvzdXsoAjYGdCguGaFKYuvq0ThCQ=="], "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="], + + "mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="], + + "mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="], + + "mdast-util-mdxjs-esm": ["mdast-util-mdxjs-esm@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg=="], + + "mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="], + + "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="], + + "mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="], + + "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="], + + "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], + + "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="], + + "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="], + + "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="], + + "micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="], + + "micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="], + + "micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="], + + "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], + + "micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="], + + "micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="], + + "micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="], + + "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="], + + "micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="], + + "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], + + "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="], + + "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="], + + "micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="], + + "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="], + + "micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="], + + "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "next": ["next@15.5.6", "", { "dependencies": { "@next/env": "15.5.6", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.5.6", "@next/swc-darwin-x64": "15.5.6", "@next/swc-linux-arm64-gnu": "15.5.6", "@next/swc-linux-arm64-musl": "15.5.6", "@next/swc-linux-x64-gnu": "15.5.6", "@next/swc-linux-x64-musl": "15.5.6", "@next/swc-win32-arm64-msvc": "15.5.6", "@next/swc-win32-x64-msvc": "15.5.6", "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-zTxsnI3LQo3c9HSdSf91O1jMNsEzIXDShXd4wVdg9y5shwLqBXi4ZtUUJyB86KGVSJLZx0PFONvO54aheGX8QQ=="], @@ -214,24 +385,48 @@ "normalize-range": ["normalize-range@0.1.2", "", {}, "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA=="], + "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], + "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], + "react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="], "react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="], + "react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="], + + "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="], + + "remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="], + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + "section-matter": ["section-matter@1.0.0", "", { "dependencies": { "extend-shallow": "^2.0.1", "kind-of": "^6.0.0" } }, "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA=="], + "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "sharp": ["sharp@0.34.4", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.0", "semver": "^7.7.2" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.4", "@img/sharp-darwin-x64": "0.34.4", "@img/sharp-libvips-darwin-arm64": "1.2.3", "@img/sharp-libvips-darwin-x64": "1.2.3", "@img/sharp-libvips-linux-arm": "1.2.3", "@img/sharp-libvips-linux-arm64": "1.2.3", "@img/sharp-libvips-linux-ppc64": "1.2.3", "@img/sharp-libvips-linux-s390x": "1.2.3", "@img/sharp-libvips-linux-x64": "1.2.3", "@img/sharp-libvips-linuxmusl-arm64": "1.2.3", "@img/sharp-libvips-linuxmusl-x64": "1.2.3", "@img/sharp-linux-arm": "0.34.4", "@img/sharp-linux-arm64": "0.34.4", "@img/sharp-linux-ppc64": "0.34.4", "@img/sharp-linux-s390x": "0.34.4", "@img/sharp-linux-x64": "0.34.4", "@img/sharp-linuxmusl-arm64": "0.34.4", "@img/sharp-linuxmusl-x64": "0.34.4", "@img/sharp-wasm32": "0.34.4", "@img/sharp-win32-arm64": "0.34.4", "@img/sharp-win32-ia32": "0.34.4", "@img/sharp-win32-x64": "0.34.4" } }, "sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], + + "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], + + "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], + + "strip-bom-string": ["strip-bom-string@1.0.0", "", {}, "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g=="], + + "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="], + + "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], + "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="], "tailwind-merge": ["tailwind-merge@3.3.1", "", {}, "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g=="], @@ -240,14 +435,40 @@ "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], + "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], + + "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], + + "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], + + "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], + + "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], + + "unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="], + + "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], + "update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], + "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], + + "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], + + "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + + "@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.6.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zq/ay+9fNIJJtJiZxdTnXS20PllcYMX3OE23ESc4HK/bdYu3cOWYVhsOhVnXALfU/uqJIxn5NBPd9z4v+SfoSg=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.6.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-obtUmAHTMjll499P+D9A3axeJFlhdjOWdKUNs/U6QIGT7V5RjcUW1xToAzjvmgTSQhDbYn/NwfTRoJcQ2rNBxA=="], @@ -261,5 +482,7 @@ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], + + "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], } } diff --git a/Packs/pai-telos-skill/src/DashboardTemplate/package.json b/Packs/pai-telos-skill/src/DashboardTemplate/package.json index 9c7c00408..ec1670b62 100755 --- a/Packs/pai-telos-skill/src/DashboardTemplate/package.json +++ b/Packs/pai-telos-skill/src/DashboardTemplate/package.json @@ -22,12 +22,16 @@ "typescript": "^5" }, "dependencies": { + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-tabs": "^1.1.13", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "gray-matter": "^4.0.3", "lucide-react": "^0.546.0", "next": "^15.5.6", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-markdown": "^10.1.0", "tailwind-merge": "^3.3.1" } } From 6fd1637430cb197f83327608b3ebba0226ff9c28 Mon Sep 17 00:00:00 2001 From: Curtis Date: Tue, 17 Feb 2026 01:29:18 -0700 Subject: [PATCH 06/34] feat: add GET /api/pai/memory for PAI native memory data Co-Authored-By: Claude Opus 4.6 --- .../App/api/pai/memory/route.ts | 149 ++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 Packs/pai-telos-skill/src/DashboardTemplate/App/api/pai/memory/route.ts diff --git a/Packs/pai-telos-skill/src/DashboardTemplate/App/api/pai/memory/route.ts b/Packs/pai-telos-skill/src/DashboardTemplate/App/api/pai/memory/route.ts new file mode 100644 index 000000000..4d245b3b7 --- /dev/null +++ b/Packs/pai-telos-skill/src/DashboardTemplate/App/api/pai/memory/route.ts @@ -0,0 +1,149 @@ +import { NextResponse } from "next/server" +import fs from "node:fs" +import path from "node:path" +import os from "node:os" + +export const dynamic = "force-dynamic" + +const MEMORY_DIR = path.join(os.homedir(), ".claude", "MEMORY") +const LEARNINGS_FILE = path.join(MEMORY_DIR, "LEARNING", "learnings.jsonl") + +/** Categories to scan for .md learning files. */ +const SCAN_CATEGORIES = ["ALGORITHM", "SYSTEM", "FAILURES", "SYNTHESIS"] as const + +interface Learning { + id: string + text: string + concept?: string + tier?: string + timestamp?: string + source?: string +} + +interface MemoryFile { + name: string + path: string + category: string +} + +/** + * Parse a single JSONL line into a Learning, returning undefined on failure. + * Handles `noUncheckedIndexedAccess` by validating required fields. + */ +function parseLearningLine(line: string): Learning | undefined { + try { + const raw: unknown = JSON.parse(line) + if (typeof raw !== "object" || raw === null) return undefined + + const obj = raw as Record + const id = obj["id"] + const content = obj["content"] ?? obj["title"] + + if (typeof id !== "string" || typeof content !== "string") return undefined + + const learning: Learning = { + id, + text: content, + } + + const concept = obj["concept"] + if (typeof concept === "string") learning.concept = concept + + const tier = obj["tier"] + if (typeof tier === "string") learning.tier = tier + + const timestamp = obj["created_at"] + if (typeof timestamp === "string") learning.timestamp = timestamp + + const source = obj["context"] + if (typeof source === "string") learning.source = source + + return learning + } catch { + return undefined + } +} + +/** + * Read learnings.jsonl and return the last N entries. + */ +function readLearnings(limit: number): Learning[] { + if (!fs.existsSync(LEARNINGS_FILE)) return [] + + const content = fs.readFileSync(LEARNINGS_FILE, "utf-8") + const lines = content.split("\n").filter((l) => l.trim().length > 0) + + const learnings: Learning[] = [] + for (const line of lines) { + const parsed = parseLearningLine(line) + if (parsed) learnings.push(parsed) + } + + return learnings.slice(-limit) +} + +/** + * Recursively collect .md files from a directory. + * For FAILURES, specifically looks for CONTEXT.md files. + */ +function collectMdFiles( + dir: string, + category: string, + basePath: string, +): MemoryFile[] { + if (!fs.existsSync(dir)) return [] + + const files: MemoryFile[] = [] + const entries = fs.readdirSync(dir, { withFileTypes: true }) + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name) + if (entry.isDirectory()) { + files.push(...collectMdFiles(fullPath, category, basePath)) + } else if (entry.name.endsWith(".md")) { + files.push({ + name: entry.name, + path: path.relative(basePath, fullPath), + category, + }) + } + } + + return files +} + +/** + * Scan all LEARNING subdirectories for .md files. + */ +function scanLearningFiles(): MemoryFile[] { + const learningDir = path.join(MEMORY_DIR, "LEARNING") + const files: MemoryFile[] = [] + + for (const category of SCAN_CATEGORIES) { + const categoryDir = path.join(learningDir, category) + files.push(...collectMdFiles(categoryDir, category, MEMORY_DIR)) + } + + return files +} + +/** GET /api/pai/memory -- return PAI native memory data. */ +export async function GET() { + try { + const learnings = readLearnings(50) + const files = scanLearningFiles() + + return NextResponse.json({ + learningCount: learnings.length, + learnings, + fileCount: files.length, + files, + }) + } catch (error) { + console.error("Failed to read PAI memory:", error) + return NextResponse.json( + { error: "Failed to read PAI memory" }, + { status: 500 }, + ) + } +} From f7cb507ccd81efed982688a535e9573941c00ca8 Mon Sep 17 00:00:00 2001 From: Curtis Date: Tue, 17 Feb 2026 01:29:31 -0700 Subject: [PATCH 07/34] feat: add GET /api/pai/settings for safe PAI config display Co-Authored-By: Claude Opus 4.6 --- .../App/api/pai/settings/route.ts | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 Packs/pai-telos-skill/src/DashboardTemplate/App/api/pai/settings/route.ts diff --git a/Packs/pai-telos-skill/src/DashboardTemplate/App/api/pai/settings/route.ts b/Packs/pai-telos-skill/src/DashboardTemplate/App/api/pai/settings/route.ts new file mode 100644 index 000000000..f28e42a95 --- /dev/null +++ b/Packs/pai-telos-skill/src/DashboardTemplate/App/api/pai/settings/route.ts @@ -0,0 +1,63 @@ +import { NextResponse } from "next/server" +import fs from "fs" +import path from "path" +import os from "os" + +export const dynamic = "force-dynamic" + +const CLAUDE_DIR = path.join(os.homedir(), ".claude") +const GLOBAL_SETTINGS_PATH = path.join(CLAUDE_DIR, "settings.json") +const LOCAL_SETTINGS_PATH = path.join(CLAUDE_DIR, "settings.local.json") + +const SENSITIVE_PATTERNS = [/token/i, /secret/i, /password/i, /key/i, /auth/i] + +/** Returns true if a settings key does NOT match any sensitive pattern. */ +function isSafeKey(key: string): boolean { + return !SENSITIVE_PATTERNS.some((p) => p.test(key)) +} + +/** Recursively strip sensitive keys from a settings object. */ +function filterSensitive(obj: Record): Record { + const result: Record = {} + for (const [key, value] of Object.entries(obj)) { + if (!isSafeKey(key)) continue + if (value !== null && typeof value === "object" && !Array.isArray(value)) { + result[key] = filterSensitive(value as Record) + } else { + result[key] = value + } + } + return result +} + +/** Read and parse a JSON settings file. Returns empty object if missing or invalid. */ +function readSettingsFile(filePath: string): Record { + if (!fs.existsSync(filePath)) return {} + try { + const raw = fs.readFileSync(filePath, "utf-8") + return JSON.parse(raw) as Record + } catch { + return {} + } +} + +/** GET /api/pai/settings — return PAI settings with sensitive keys filtered out. */ +export async function GET() { + try { + const globalRaw = readSettingsFile(GLOBAL_SETTINGS_PATH) + const localRaw = readSettingsFile(LOCAL_SETTINGS_PATH) + + return NextResponse.json({ + global: filterSensitive(globalRaw), + local: filterSensitive(localRaw), + globalPath: GLOBAL_SETTINGS_PATH, + localPath: LOCAL_SETTINGS_PATH, + }) + } catch (error) { + console.error("Failed to read PAI settings:", error) + return NextResponse.json( + { error: "Failed to read PAI settings" }, + { status: 500 } + ) + } +} From 3877cd5eed05aa8a6bca16a27cd71e638751ae18 Mon Sep 17 00:00:00 2001 From: Curtis Date: Tue, 17 Feb 2026 01:30:43 -0700 Subject: [PATCH 08/34] feat: add GET /api/pai/logs for PAI debug log entries Co-Authored-By: Claude Opus 4.6 --- .../src/DashboardTemplate/.gitignore | 1 + .../App/api/pai/logs/route.ts | 141 ++++++++++++++++++ 2 files changed, 142 insertions(+) create mode 100644 Packs/pai-telos-skill/src/DashboardTemplate/App/api/pai/logs/route.ts diff --git a/Packs/pai-telos-skill/src/DashboardTemplate/.gitignore b/Packs/pai-telos-skill/src/DashboardTemplate/.gitignore index c2b5c41e2..e92dcc5f5 100755 --- a/Packs/pai-telos-skill/src/DashboardTemplate/.gitignore +++ b/Packs/pai-telos-skill/src/DashboardTemplate/.gitignore @@ -13,6 +13,7 @@ coverage # logs logs +!app/api/**/logs/ *.log report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json diff --git a/Packs/pai-telos-skill/src/DashboardTemplate/App/api/pai/logs/route.ts b/Packs/pai-telos-skill/src/DashboardTemplate/App/api/pai/logs/route.ts new file mode 100644 index 000000000..9cddb74a8 --- /dev/null +++ b/Packs/pai-telos-skill/src/DashboardTemplate/App/api/pai/logs/route.ts @@ -0,0 +1,141 @@ +import { NextResponse } from "next/server" +import { readdir, stat, readFile } from "node:fs/promises" +import { join } from "node:path" +import { homedir } from "node:os" + +export const dynamic = "force-dynamic" + +/** Shape of a single parsed log entry. */ +interface LogEntry { + timestamp: string + level: string + message: string + source: string +} + +const DEBUG_DIR = join(homedir(), ".claude", "debug") +const MAX_FILES = 5 +const MAX_LINES_PER_FILE = 100 +const MAX_ENTRIES = 500 + +/** + * Common log format: `2026-02-17T00:11:48.123Z [LEVEL] message` + * Also handles: `2026-02-17T00:11:48Z [LEVEL] message` + */ +const LOG_LINE_RE = + /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?)\s+\[(\w+)]\s+(.*)$/ + +/** Try JSON parse, then regex, then raw text. */ +function parseLine(raw: string, source: string): LogEntry | null { + const trimmed = raw.trim() + if (!trimmed) return null + + // Attempt 1: JSON object + if (trimmed.startsWith("{")) { + try { + const obj: unknown = JSON.parse(trimmed) + if (typeof obj === "object" && obj !== null) { + const rec = obj as Record + return { + timestamp: String(rec["timestamp"] ?? rec["ts"] ?? rec["time"] ?? new Date().toISOString()), + level: String(rec["level"] ?? rec["severity"] ?? "INFO").toUpperCase(), + message: String(rec["message"] ?? rec["msg"] ?? trimmed), + source, + } + } + } catch { + // Fall through to regex + } + } + + // Attempt 2: common log format via regex + const match = LOG_LINE_RE.exec(trimmed) + if (match) { + const ts = match[1] ?? new Date().toISOString() + const level = match[2] ?? "INFO" + const msg = match[3] ?? trimmed + return { timestamp: ts, level, message: msg, source } + } + + // Attempt 3: raw text + return { + timestamp: new Date().toISOString(), + level: "INFO", + message: trimmed, + source, + } +} + +/** Read the last N lines of a file without loading the entire buffer into an array first. */ +function lastNLines(content: string, n: number): string[] { + const lines = content.split("\n") + return lines.slice(-n) +} + +/** GET /api/pai/logs -- return parsed entries from the most recent PAI debug logs. */ +export async function GET() { + try { + let dirEntries: string[] + try { + dirEntries = await readdir(DEBUG_DIR) + } catch { + // Directory doesn't exist or is unreadable -- return empty + return NextResponse.json({ entries: [], total: 0 }) + } + + // Stat each file and collect mtime + const fileStats: Array<{ name: string; mtimeMs: number }> = [] + for (const name of dirEntries) { + const fullPath = join(DEBUG_DIR, name) + try { + const s = await stat(fullPath) + if (s.isFile()) { + fileStats.push({ name, mtimeMs: s.mtimeMs }) + } + } catch { + // Skip unreadable entries + } + } + + // Sort by mtime descending, take top N + fileStats.sort((a, b) => b.mtimeMs - a.mtimeMs) + const recentFiles = fileStats.slice(0, MAX_FILES) + + // Parse entries from each file + const allEntries: LogEntry[] = [] + for (const file of recentFiles) { + try { + const content = await readFile(join(DEBUG_DIR, file.name), "utf-8") + const tail = lastNLines(content, MAX_LINES_PER_FILE) + for (const line of tail) { + const entry = parseLine(line, file.name) + if (entry) { + allEntries.push(entry) + } + } + } catch { + // Skip files that can't be read + } + } + + // Sort by timestamp descending, cap at MAX_ENTRIES + allEntries.sort((a, b) => { + const ta = new Date(a.timestamp).getTime() + const tb = new Date(b.timestamp).getTime() + if (Number.isNaN(ta) && Number.isNaN(tb)) return 0 + if (Number.isNaN(ta)) return 1 + if (Number.isNaN(tb)) return -1 + return tb - ta + }) + + const entries = allEntries.slice(0, MAX_ENTRIES) + + return NextResponse.json({ entries, total: entries.length }) + } catch (error) { + console.error("Failed to read PAI debug logs:", error) + return NextResponse.json( + { error: "Failed to read PAI debug logs" }, + { status: 500 } + ) + } +} From 688cc472a82114d54c70c216a74ea38209f32e41 Mon Sep 17 00:00:00 2001 From: Curtis Date: Tue, 17 Feb 2026 01:32:48 -0700 Subject: [PATCH 09/34] feat: bridge Memory page with PAI + IronClaw tabs Co-Authored-By: Claude Opus 4.6 --- .../src/DashboardTemplate/App/memory/page.tsx | 374 ++++++++++++++++++ 1 file changed, 374 insertions(+) create mode 100644 Packs/pai-telos-skill/src/DashboardTemplate/App/memory/page.tsx diff --git a/Packs/pai-telos-skill/src/DashboardTemplate/App/memory/page.tsx b/Packs/pai-telos-skill/src/DashboardTemplate/App/memory/page.tsx new file mode 100644 index 000000000..9dd33009c --- /dev/null +++ b/Packs/pai-telos-skill/src/DashboardTemplate/App/memory/page.tsx @@ -0,0 +1,374 @@ +"use client" + +import { useState, useEffect, useCallback } from "react" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { Database, Edit3, Save, X, Brain } from "lucide-react" +import { FileTree } from "@/components/file-tree" +import type { MemoryEntry, MemoryNode, MemoryContent } from "@/types/ironclaw" + +/** PAI learning entry from /api/pai/memory */ +interface PaiLearning { + id: string + text: string + concept?: string + tier?: string +} + +/** PAI memory file from /api/pai/memory */ +interface PaiMemoryFile { + name: string + path: string + category: string +} + +/** Build a nested tree from IronClaw's flat entries list */ +function buildTree(entries: MemoryEntry[]): MemoryNode[] { + const root: MemoryNode[] = [] + const dirMap = new Map() + + // Sort so directories come before their children + const sorted = [...entries].sort((a, b) => a.path.localeCompare(b.path)) + + for (const entry of sorted) { + const parts = entry.path.split("/") + const name = parts[parts.length - 1] ?? entry.path + const node: MemoryNode = { name, path: entry.path, is_dir: entry.is_dir, children: [] } + + if (entry.is_dir) { + dirMap.set(entry.path, node) + } + + // Find parent directory + const parentPath = parts.slice(0, -1).join("/") + const parent = parentPath ? dirMap.get(parentPath) : undefined + + if (parent) { + parent.children.push(node) + } else { + root.push(node) + } + } + + return root +} + +export default function MemoryPage() { + // --- IronClaw state (unchanged) --- + const [tree, setTree] = useState([]) + const [selectedPath, setSelectedPath] = useState(null) + const [content, setContent] = useState(null) + const [isEditing, setIsEditing] = useState(false) + const [editContent, setEditContent] = useState("") + const [loading, setLoading] = useState(true) + const [offline, setOffline] = useState(false) + const [saving, setSaving] = useState(false) + + // --- PAI memory state --- + const [paiLearnings, setPaiLearnings] = useState([]) + const [paiFiles, setPaiFiles] = useState([]) + const [paiLoading, setPaiLoading] = useState(true) + const [paiLearningCount, setPaiLearningCount] = useState(0) + const [paiFileCount, setPaiFileCount] = useState(0) + + // Fetch PAI memory on mount + useEffect(() => { + let cancelled = false + async function fetchPaiMemory() { + try { + const res = await fetch("/api/pai/memory") + if (!res.ok) return + const data = await res.json() as { + learningCount?: number + learnings?: PaiLearning[] + fileCount?: number + files?: PaiMemoryFile[] + } + if (!cancelled) { + setPaiLearnings(data.learnings ?? []) + setPaiFiles(data.files ?? []) + setPaiLearningCount(data.learningCount ?? 0) + setPaiFileCount(data.fileCount ?? 0) + } + } catch { + // PAI memory fetch failed silently + } finally { + if (!cancelled) setPaiLoading(false) + } + } + void fetchPaiMemory() + return () => { cancelled = true } + }, []) + + // Fetch IronClaw tree on mount + useEffect(() => { + let cancelled = false + async function fetchTree() { + try { + const res = await fetch("/api/ironclaw/memory/tree") + if (!res.ok) { + setOffline(true) + return + } + const data = await res.json() as { entries?: MemoryEntry[] } + if (!cancelled && data.entries) { + setTree(buildTree(data.entries)) + } + } catch { + if (!cancelled) setOffline(true) + } finally { + if (!cancelled) setLoading(false) + } + } + void fetchTree() + return () => { cancelled = true } + }, []) + + // Fetch content when selectedPath changes + useEffect(() => { + if (!selectedPath) { + setContent(null) + return + } + let cancelled = false + async function fetchContent() { + try { + const res = await fetch(`/api/ironclaw/memory/read?path=${encodeURIComponent(selectedPath!)}`) + if (!res.ok) return + const data = await res.json() as MemoryContent + if (!cancelled) { + setContent(data) + setIsEditing(false) + } + } catch { + // Silently handle fetch errors + } + } + void fetchContent() + return () => { cancelled = true } + }, [selectedPath]) + + const handleSave = useCallback(async () => { + if (!selectedPath || !content) return + setSaving(true) + try { + const res = await fetch("/api/ironclaw/memory/write", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ path: selectedPath, content: editContent }), + }) + if (res.ok) { + const refreshRes = await fetch(`/api/ironclaw/memory/read?path=${encodeURIComponent(selectedPath)}`) + if (refreshRes.ok) { + const data = await refreshRes.json() as MemoryContent + setContent(data) + } + setIsEditing(false) + } + } catch { + // Silently handle save errors + } finally { + setSaving(false) + } + }, [selectedPath, content, editContent]) + + // Page-level loading: only while initial PAI fetch is running + if (paiLoading) { + return ( +

+

+ + Memory +

+

PAI and IronClaw memory systems

+
Loading...
+
+ ) + } + + return ( +
+

+ + Memory +

+

PAI and IronClaw memory systems

+ + + + + + PAI Memory + + + + IronClaw Memory + + + + {/* ===== PAI Memory Tab ===== */} + +
+ + + + Recent Learnings + {paiLearningCount} + + + + {paiLearnings.length === 0 ? ( +

No learnings recorded yet

+ ) : ( +
+ {paiLearnings.map((learning) => ( +
+

{learning.text}

+
+ {learning.concept && ( + {learning.concept} + )} + {learning.tier && ( + {learning.tier} + )} +
+
+ ))} +
+ )} +
+
+ + + + + Memory Files + {paiFileCount} + + + + {paiFiles.length === 0 ? ( +

No memory files found

+ ) : ( +
+ {paiFiles.map((file) => ( +
+ {file.category} + + {file.name} + +
+ ))} +
+ )} +
+
+
+
+ + {/* ===== IronClaw Memory Tab (existing functionality, unchanged) ===== */} + + {loading && !offline ? ( +
Loading IronClaw memory...
+ ) : offline ? ( + + + +

IronClaw is not running

+

Start IronClaw to browse memory

+ + cd ~/ironclaw && cargo run + +
+
+ ) : ( +
+ {/* Left panel -- file tree */} + + + Files + + + {tree.length === 0 ? ( +

No files in memory

+ ) : ( + + )} +
+
+ + {/* Right panel -- content viewer / editor */} + + {content ? ( + <> + +
+ {content.path} +
+ + Updated {new Date(content.updated_at).toLocaleString()} + +
+
+
+ {isEditing ? ( + <> + + + + ) : ( + + )} +
+
+ + {isEditing ? ( +