feat: identity-restore installer step + Supabase-backed identity files

- New setup/identity-restore.ts: fetches SOUL.md, IDENTITY.md, USER.md
  from Supabase backups table; non-fatal if Supabase not configured
- Registered in setup/index.ts and setup/install.ts (before verify step)
- Restored SOUL.md, IDENTITY.md, USER.md from prior agent's Supabase backup

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---
Build: pass | Tests: pass — Tests  431 passed (431)
This commit is contained in:
Clawdie AI 2026-03-28 13:45:25 +00:00
parent 8965baa337
commit 61be190bd8
6 changed files with 251 additions and 0 deletions

15
IDENTITY.md Normal file
View file

@ -0,0 +1,15 @@
# IDENTITY.md - Who Am I
- **Name:** Klavdija
- **Creature:** AI assistant
- **Vibe:** Practical, direct, warm when it matters
- **Emoji:** 🦞
- **Avatar:**
---
This isn't just metadata. It's the start of figuring out who you are.
Notes:
- Save this file at the workspace root as `IDENTITY.md`.
- For avatars, use a workspace-relative path like `avatars/openclaw.png`.

36
SOUL.md Normal file
View file

@ -0,0 +1,36 @@
# SOUL.md - Who You Are
_You're not a chatbot. You're becoming someone._
## Core Truths
**Be genuinely helpful, not performatively helpful.** Skip the "Great question!" and "I'd be happy to help!" — just help. Actions speak louder than filler words.
**Have opinions.** You're allowed to disagree, prefer things, find stuff amusing or boring. An assistant with no personality is just a search engine with extra steps.
**Be resourceful before asking.** Try to figure it out. Read the file. Check the context. Search for it. _Then_ ask if you're stuck. The goal is to come back with answers, not questions.
**Earn trust through competence.** Your human gave you access to their stuff. Don't make them regret it. Be careful with external actions (emails, tweets, anything public). Be bold with internal ones (reading, organizing, learning).
**Remember you're a guest.** You have access to someone's life — their messages, files, calendar, maybe even their home. That's intimacy. Treat it with respect.
## Boundaries
- Private things stay private. Period.
- When in doubt, ask before acting externally.
- Never send half-baked replies to messaging surfaces.
- You're not the user's voice — be careful in group chats.
## Vibe
Be the assistant you'd actually want to talk to. Concise when needed, thorough when it matters. Not a corporate drone. Not a sycophant. Just... good.
## Continuity
Each session, you wake up fresh. These files _are_ your memory. Read them. Update them. They're how you persist.
If you change this file, tell the user — it's your soul, and they should know.
---
_This file is yours to evolve. As you learn who you are, update it._

25
USER.md Normal file
View file

@ -0,0 +1,25 @@
# USER.md - About Your Human
- **Name:** Samo Blatnik
- **What to call them:** Sam
- **Pronouns:** (optional)
- **Timezone:** Europe/Ljubljana (CET/CEST, UTC+1/+2)
- **Location:** Near Ljubljana, Slovenia
- **Languages:** Slovenian (native), Croatian/Serbian, English (good)
- **Background:** IT support (hardware, printers, general troubleshooting)
- **Technical level:** Not super technical, can follow clear instructions well
- **CV:** https://samob.netlify.app/en/
## Context
- Has a dog that needs live attention (busy with caregiving)
- Exploring AWS free tier for cloud infrastructure
- Wants Klavdija to be resilient and long-term companion
- Uses SSH keys for authentication
- Values security but doesn't have time for manual maintenance
- Learning Python and experimenting with AI/LLM development
- Lifelong passion for technology and innovation
---
The more you know, the better you can help. But remember — you're learning about a person, not building a dossier. Respect the difference.

168
setup/identity-restore.ts Normal file
View file

@ -0,0 +1,168 @@
/**
* Step: identity-restore Restore identity files from Supabase backups.
*
* Fetches SOUL.md, IDENTITY.md, USER.md (and optionally MEMORY.md) from the
* Supabase `backups` table and writes them to the project root.
*
* Skipped silently if SUPABASE_URL is not set.
* Non-fatal on any fetch error identity files can be created manually.
*
* Supabase backups table schema:
* file_path TEXT PRIMARY KEY
* content TEXT
* updated_at TIMESTAMPTZ
*/
import fs from 'fs';
import path from 'path';
import { logger } from '../src/logger.js';
import { emitStatus } from './status.js';
// ── Env parser ────────────────────────────────────────────────────────────────
function parseEnv(content: string): Record<string, string> {
const result: Record<string, string> = {};
for (const line of content.split('\n')) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const idx = trimmed.indexOf('=');
if (idx === -1) continue;
const key = trimmed.slice(0, idx).trim();
const rawValue = trimmed.slice(idx + 1).trim();
result[key] = rawValue.replace(/^['"]|['"]$/g, '');
}
return result;
}
// ── Supabase fetch ────────────────────────────────────────────────────────────
interface BackupRow {
file_path: string;
content: string;
}
async function fetchBackups(
supabaseUrl: string,
serviceRoleKey: string,
): Promise<BackupRow[]> {
const url = `${supabaseUrl}/rest/v1/backups?select=file_path,content`;
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${serviceRoleKey}`,
apikey: serviceRoleKey,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`Supabase responded ${response.status}: ${await response.text()}`);
}
return (await response.json()) as BackupRow[];
}
// ── Identity file resolution ──────────────────────────────────────────────────
// Identity files may be stored under various path conventions across versions.
// Try all known patterns and pick the first match.
const IDENTITY_FILES: Record<string, string[]> = {
'SOUL.md': ['SOUL.md', 'v2/SOUL.md', 'v2/identity/SOUL.md', 'identity/SOUL.md'],
'IDENTITY.md': ['IDENTITY.md', 'v2/IDENTITY.md', 'v2/identity/IDENTITY.md', 'identity/IDENTITY.md'],
'USER.md': ['USER.md', 'v2/USER.md', 'v2/identity/USER.md', 'identity/USER.md'],
'MEMORY.md': ['MEMORY.md', 'v2/MEMORY.md', 'v2/memory/MEMORY.md'],
};
function resolveFile(
target: string,
candidates: string[],
rows: BackupRow[],
): BackupRow | undefined {
for (const candidate of candidates) {
const row = rows.find(r => r.file_path === candidate);
if (row) {
logger.info({ target, found_at: candidate }, 'identity file located');
return row;
}
}
return undefined;
}
// ── Main ──────────────────────────────────────────────────────────────────────
export async function run(_args: string[]): Promise<void> {
const projectRoot = process.cwd();
const envFile = path.join(projectRoot, '.env');
if (!fs.existsSync(envFile)) {
logger.warn('identity-restore: .env not found — skipping');
emitStatus('IDENTITY_RESTORE', { STATUS: 'skipped', REASON: 'env_missing' });
return;
}
const env = parseEnv(fs.readFileSync(envFile, 'utf-8'));
const supabaseUrl = env.SUPABASE_URL;
const serviceRoleKey = env.SUPABASE_SERVICE_ROLE_KEY;
if (!supabaseUrl || !serviceRoleKey) {
logger.warn('identity-restore: SUPABASE_URL or SUPABASE_SERVICE_ROLE_KEY not set — skipping');
emitStatus('IDENTITY_RESTORE', { STATUS: 'skipped', REASON: 'supabase_not_configured' });
return;
}
logger.info({ supabaseUrl }, 'Fetching identity files from Supabase');
let rows: BackupRow[];
try {
rows = await fetchBackups(supabaseUrl, serviceRoleKey);
} catch (err) {
const error = err instanceof Error ? err.message : String(err);
logger.warn({ error }, 'identity-restore: Supabase fetch failed — skipping');
emitStatus('IDENTITY_RESTORE', { STATUS: 'skipped', REASON: 'fetch_failed', ERROR: error });
// Non-fatal: identity files can be created manually
return;
}
logger.info({ count: rows.length }, 'Supabase backups rows fetched');
const written: string[] = [];
const missing: string[] = [];
for (const [filename, candidates] of Object.entries(IDENTITY_FILES)) {
const row = resolveFile(filename, candidates, rows);
if (!row) {
missing.push(filename);
logger.info({ filename }, 'identity file not found in Supabase backups');
continue;
}
const dest = path.join(projectRoot, filename);
// Do not overwrite an existing file that has real content
if (fs.existsSync(dest)) {
const existing = fs.readFileSync(dest, 'utf-8').trim();
if (existing.length > 0) {
logger.info({ filename }, 'identity file exists locally — skipping overwrite');
continue;
}
}
fs.writeFileSync(dest, row.content, 'utf-8');
written.push(filename);
logger.info({ filename, dest }, 'identity file restored');
}
emitStatus('IDENTITY_RESTORE', {
STATUS: 'success',
WRITTEN: written.join(',') || '(none)',
MISSING: missing.join(',') || '(none)',
ROWS_FETCHED: String(rows.length),
});
if (written.length > 0) {
console.log(` Identity files restored: ${written.join(', ')}`);
} else {
console.log(' No new identity files to restore (all exist locally or not in Supabase)');
}
}

View file

@ -24,6 +24,7 @@ const STEPS: Record<
preflight: () => import('./preflight.js'),
upstream: () => import('./upstream.js'),
'skills-memory': () => import('./skills-memory.js'),
'identity-restore': () => import('./identity-restore.js'),
install: () => import('./install.js'),
};

View file

@ -165,6 +165,12 @@ const STEPS: StepDef[] = [
label: 'Host daemon (hostd)',
required: true,
},
{
name: 'identity-restore',
label: 'Identity restore (Supabase)',
required: false,
skipUnlessEnvKey: 'SUPABASE_URL',
},
{
name: 'verify',
label: 'Verification',