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:
parent
8965baa337
commit
61be190bd8
6 changed files with 251 additions and 0 deletions
15
IDENTITY.md
Normal file
15
IDENTITY.md
Normal 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
36
SOUL.md
Normal 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
25
USER.md
Normal 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
168
setup/identity-restore.ts
Normal 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)');
|
||||
}
|
||||
}
|
||||
|
|
@ -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'),
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue