Agents: real ClawBrainHub integration (list + pull brains → trading cards)
Backend: - brainhub.rs: BrainHubClient trait + HttpBrainHubClient (public discovery API, optional Bearer token) + FakeBrainHubClient. Lists the user's brains (CLAWBRAINHUB_OWNER, default "omar") + redclawsystems reference brains, and pulls a .brain JSON, parsing identity/skills/tools into card fields (tagline, skills, tools, capabilities). 2 parser unit tests. - Routes GET /v1/brainhub/brains + /v1/brainhub/pull (rate-limited); wired into AppState + main (HttpBrainHubClient) + test harness (FakeBrainHubClient). Mobile: - src/api/brainhub.ts (listBrains/pullBrain), draftStore.pendingAgentBrain. - app/pick-brain.tsx: pick a brain → pull → hand to the agent-card editor. - Agents "+ New agent" now opens the brain picker; demo.tsx adopts the pulled brain (name, tagline, skill bars, tools, capabilities) for the flip side. Verified live against clawbrainhub.com (omar's brains + reference brains list; general-assistant pull yields real skills). Token stays server-side/env only. Gate: mobile 28, backend 123. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
d30deaf857
commit
9765a08a5a
@@ -0,0 +1,112 @@
|
||||
// Pick an agent brain from ClawBrainHub (your account + reference brains), pull
|
||||
// it, and hand it to the agent-card editor to turn into a trading card.
|
||||
|
||||
import { Stack, useRouter } from "expo-router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ActivityIndicator, FlatList, Pressable, StyleSheet, Text, View } from "react-native";
|
||||
import { BrainSummary, listBrains, pullBrain } from "../src/api/brainhub";
|
||||
import { useDraftStore } from "../src/stores/draftStore";
|
||||
|
||||
export default function PickBrain() {
|
||||
const router = useRouter();
|
||||
const setPendingAgentBrain = useDraftStore((s) => s.setPendingAgentBrain);
|
||||
const [brains, setBrains] = useState<BrainSummary[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [pulling, setPulling] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
listBrains()
|
||||
.then((b) => alive && setBrains(b))
|
||||
.catch(() => alive && setError("Couldn't reach ClawBrainHub. Check your connection."))
|
||||
.finally(() => alive && setLoading(false));
|
||||
return () => {
|
||||
alive = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const choose = async (b: BrainSummary) => {
|
||||
const key = `${b.owner}/${b.name}`;
|
||||
setPulling(key);
|
||||
try {
|
||||
const brain = await pullBrain(b.owner, b.name, b.version);
|
||||
setPendingAgentBrain(brain);
|
||||
router.replace({ pathname: "/demo", params: { kind: "agent" } });
|
||||
} catch {
|
||||
setError("Couldn't pull that brain. Try another.");
|
||||
setPulling(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.root}>
|
||||
<Stack.Screen options={{ title: "Pick an agent brain", headerTitleAlign: "center" }} />
|
||||
{loading ? (
|
||||
<View style={styles.center}>
|
||||
<ActivityIndicator color="#ff3b30" />
|
||||
<Text style={styles.dim}>Loading brains…</Text>
|
||||
</View>
|
||||
) : error ? (
|
||||
<View style={styles.center}>
|
||||
<Text style={styles.error}>{error}</Text>
|
||||
</View>
|
||||
) : (
|
||||
<FlatList
|
||||
data={brains}
|
||||
keyExtractor={(b) => `${b.owner}/${b.name}`}
|
||||
contentContainerStyle={styles.list}
|
||||
ListHeaderComponent={
|
||||
<Text style={styles.hint}>Pull a brain from ClawBrainHub to make an agent card.</Text>
|
||||
}
|
||||
renderItem={({ item }) => {
|
||||
const key = `${item.owner}/${item.name}`;
|
||||
return (
|
||||
<Pressable
|
||||
style={styles.row}
|
||||
disabled={!!pulling}
|
||||
onPress={() => choose(item)}
|
||||
>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={styles.name} numberOfLines={1}>
|
||||
{item.name}
|
||||
</Text>
|
||||
<Text style={styles.owner} numberOfLines={1}>
|
||||
@{item.owner} · v{item.version}
|
||||
{item.badge ? ` · ${item.badge}` : ""}
|
||||
</Text>
|
||||
</View>
|
||||
{pulling === key ? (
|
||||
<ActivityIndicator color="#ff3b30" />
|
||||
) : (
|
||||
<Text style={styles.pull}>Pull →</Text>
|
||||
)}
|
||||
</Pressable>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
root: { flex: 1, backgroundColor: "#0a0a0c" },
|
||||
center: { flex: 1, alignItems: "center", justifyContent: "center", gap: 12, padding: 32 },
|
||||
dim: { color: "#9a9aa0" },
|
||||
error: { color: "#ff453a", textAlign: "center", fontSize: 15 },
|
||||
list: { padding: 16, gap: 10 },
|
||||
hint: { color: "#9a9aa0", fontSize: 14, marginBottom: 8 },
|
||||
row: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 12,
|
||||
backgroundColor: "#15151a",
|
||||
borderRadius: 14,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 16,
|
||||
},
|
||||
name: { color: "#f5f5f7", fontSize: 16, fontWeight: "700" },
|
||||
owner: { color: "#9a9aa0", fontSize: 13, marginTop: 2 },
|
||||
pull: { color: "#ff3b30", fontWeight: "700", fontSize: 14 },
|
||||
});
|
||||
Reference in New Issue
Block a user