@tpsdev-ai/flair 0.5.6 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/bridges/builtins/agentic-stack.js +58 -0
- package/dist/bridges/builtins/index.js +36 -0
- package/dist/bridges/discover.js +195 -0
- package/dist/bridges/runtime/context.js +59 -0
- package/dist/bridges/runtime/execute.js +92 -0
- package/dist/bridges/runtime/export-runner.js +105 -0
- package/dist/bridges/runtime/formats.js +136 -0
- package/dist/bridges/runtime/import-runner.js +107 -0
- package/dist/bridges/runtime/load-descriptor.js +46 -0
- package/dist/bridges/runtime/mapper.js +139 -0
- package/dist/bridges/runtime/predicate.js +122 -0
- package/dist/bridges/runtime/writers.js +130 -0
- package/dist/bridges/runtime/yaml-loader.js +148 -0
- package/dist/bridges/scaffold.js +224 -0
- package/dist/bridges/types.js +30 -0
- package/dist/cli.js +970 -216
- package/dist/deploy.js +161 -0
- package/dist/resources/ObservationCenter.js +19 -0
- package/dist/resources/health.js +443 -14
- package/package.json +6 -6
- package/ui/observation-center.html +91 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tpsdev-ai/flair",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Identity, memory, and soul for AI agents. Cryptographic identity (Ed25519), semantic memory with local embeddings, and persistent personality — all in a single process.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"files": [
|
|
31
31
|
"dist/",
|
|
32
32
|
"schemas/",
|
|
33
|
+
"ui/",
|
|
33
34
|
"config.yaml",
|
|
34
35
|
"LICENSE",
|
|
35
36
|
"README.md",
|
|
@@ -53,9 +54,11 @@
|
|
|
53
54
|
},
|
|
54
55
|
"dependencies": {
|
|
55
56
|
"@harperfast/harper": "5.0.1",
|
|
57
|
+
"@types/js-yaml": "^4.0.9",
|
|
56
58
|
"commander": "14.0.3",
|
|
57
|
-
"harper-fabric-embeddings": "0.2.
|
|
59
|
+
"harper-fabric-embeddings": "0.2.3",
|
|
58
60
|
"jose": "^6.2.2",
|
|
61
|
+
"js-yaml": "^4.1.1",
|
|
59
62
|
"tweetnacl": "1.0.3"
|
|
60
63
|
},
|
|
61
64
|
"devDependencies": {
|
|
@@ -70,8 +73,5 @@
|
|
|
70
73
|
"workspaces": [
|
|
71
74
|
"packages/*",
|
|
72
75
|
"plugins/*"
|
|
73
|
-
]
|
|
74
|
-
"optionalDependencies": {
|
|
75
|
-
"@node-llama-cpp/mac-arm64-metal": "3.18.1"
|
|
76
|
-
}
|
|
76
|
+
]
|
|
77
77
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Observation Center</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root{--bg:#f4efe6;--panel:rgba(255,251,243,.86);--ink:#1e241f;--muted:#697265;--line:rgba(30,36,31,.12);--accent:#0f766e;--accent-soft:rgba(15,118,110,.12);--ok:#166534;--warn:#b45309;--shadow:0 18px 48px rgba(69,58,35,.14)}
|
|
9
|
+
*{box-sizing:border-box}body{margin:0;min-height:100vh;font:15px/1.45 "IBM Plex Sans","Avenir Next",sans-serif;color:var(--ink);background:radial-gradient(circle at top left,rgba(15,118,110,.14),transparent 28rem),radial-gradient(circle at top right,rgba(180,83,9,.1),transparent 18rem),linear-gradient(180deg,#fcfaf5,var(--bg))}
|
|
10
|
+
.shell{max-width:1280px;margin:0 auto;padding:28px 18px 40px}.hero,.panel{background:var(--panel);border:1px solid var(--line);border-radius:22px;backdrop-filter:blur(14px);box-shadow:var(--shadow)}.hero{padding:24px;margin-bottom:18px}.hero h1,.panel h2{margin:0;font-family:"IBM Plex Serif","Iowan Old Style",serif}.hero h1{font-size:clamp(2rem,4vw,3.1rem)}.hero p,.subtle,.empty{color:var(--muted)}
|
|
11
|
+
.summary,.grid,.events,.toolbar,.layout{display:grid;gap:12px}.summary{grid-template-columns:repeat(auto-fit,minmax(150px,1fr));margin-top:18px}.chip,.card,.event{background:rgba(255,255,255,.62);border:1px solid var(--line);border-radius:16px;padding:14px}.chip strong{display:block;font-size:1.3rem}.chip span{font-size:.92rem;color:var(--muted)}
|
|
12
|
+
.toolbar{grid-template-columns:repeat(auto-fit,minmax(160px,1fr));padding:16px;margin-bottom:18px}.field{display:flex;flex-direction:column;gap:6px}.field label{font-size:.8rem;text-transform:uppercase;letter-spacing:.08em;color:var(--muted)}.field input,.field select,.field button{width:100%;padding:11px 12px;border-radius:12px;border:1px solid var(--line);background:#fffaf1;font:inherit;color:var(--ink)}.field button{cursor:pointer;color:#fff;background:linear-gradient(135deg,var(--accent),#115e59);border:none;font-weight:600}
|
|
13
|
+
.layout{grid-template-columns:1.05fr 1.4fr;align-items:start}.panel{padding:18px}.head,.row{display:flex;justify-content:space-between;gap:10px}.head{align-items:baseline;margin-bottom:14px}.grid{grid-template-columns:repeat(auto-fill,minmax(220px,1fr))}.events{max-height:72vh;overflow:auto;padding-right:4px}.card h3,.event h3{margin:0;font-size:1rem}.meta,.workspace{font-size:.9rem;color:var(--muted)}.event p{margin:8px 0 0;white-space:pre-wrap;word-break:break-word}.kind{font:0.82rem "IBM Plex Mono",monospace;color:var(--accent)}.badge{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:999px;font-size:.8rem;font-weight:600;background:var(--accent-soft);color:var(--accent)}.ok{background:rgba(22,101,52,.12);color:var(--ok)}.warn{background:rgba(180,83,9,.14);color:var(--warn)}
|
|
14
|
+
@media (max-width:1080px){.toolbar{grid-template-columns:repeat(2,minmax(0,1fr))}.layout{grid-template-columns:1fr}.events{max-height:none}}@media (max-width:640px){.shell{padding:18px 12px 32px}.toolbar{grid-template-columns:1fr}}
|
|
15
|
+
</style>
|
|
16
|
+
</head>
|
|
17
|
+
<body>
|
|
18
|
+
<div class="shell">
|
|
19
|
+
<section class="hero">
|
|
20
|
+
<h1>Observation Center</h1>
|
|
21
|
+
<p>Read-only Flair dashboard for OrgEvents, roster state, and latest workspace snapshots. Polling every 5 seconds.</p>
|
|
22
|
+
<div class="summary" id="summary"></div>
|
|
23
|
+
</section>
|
|
24
|
+
<section class="panel toolbar">
|
|
25
|
+
<div class="field"><label for="baseUrl">Flair URL</label><input id="baseUrl" placeholder="(same origin)"></div>
|
|
26
|
+
<div class="field"><label for="adminPass">Admin password</label><input id="adminPass" type="password" placeholder="Basic admin auth"></div>
|
|
27
|
+
<div class="field"><label for="agentFilter">Agent</label><select id="agentFilter"><option value="">All agents</option></select></div>
|
|
28
|
+
<div class="field"><label for="kindFilter">Event kind</label><select id="kindFilter"><option value="">All kinds</option></select></div>
|
|
29
|
+
<div class="field"><label for="timeFilter">Time range</label><select id="timeFilter"><option value="15m">Last 15 minutes</option><option value="1h" selected>Last hour</option><option value="6h">Last 6 hours</option><option value="24h">Last 24 hours</option></select></div>
|
|
30
|
+
<div class="field"><label> </label><button id="refresh">Refresh now</button></div>
|
|
31
|
+
</section>
|
|
32
|
+
<div class="layout">
|
|
33
|
+
<section class="panel">
|
|
34
|
+
<div class="head"><div><h2>Agent Grid</h2><p class="subtle">Status is derived from recent OrgEvents and workspace snapshots.</p></div><p class="subtle" id="agentStatus">Loading agents…</p></div>
|
|
35
|
+
<div class="grid" id="agentGrid"></div>
|
|
36
|
+
</section>
|
|
37
|
+
<section class="panel">
|
|
38
|
+
<div class="head"><div><h2>Event Feed</h2><p class="subtle">Events visible to <code>anvil</code> via <code>/OrgEventCatchup/anvil</code>.</p></div><p class="subtle" id="feedStatus">Waiting for first poll…</p></div>
|
|
39
|
+
<div class="events" id="eventFeed"></div>
|
|
40
|
+
</section>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
<script>
|
|
44
|
+
const state={anchorAgent:"anvil",agents:[],events:[],workspace:{},latestPoll:null,timer:null};
|
|
45
|
+
const els={summary:document.querySelector("#summary"),baseUrl:document.querySelector("#baseUrl"),adminPass:document.querySelector("#adminPass"),agentFilter:document.querySelector("#agentFilter"),kindFilter:document.querySelector("#kindFilter"),timeFilter:document.querySelector("#timeFilter"),refresh:document.querySelector("#refresh"),agentGrid:document.querySelector("#agentGrid"),eventFeed:document.querySelector("#eventFeed"),agentStatus:document.querySelector("#agentStatus"),feedStatus:document.querySelector("#feedStatus")};
|
|
46
|
+
const SS_PASS="flair.observation.adminPass",SS_URL="flair.observation.baseUrl";
|
|
47
|
+
const storedPass=sessionStorage.getItem(SS_PASS);if(storedPass)els.adminPass.value=storedPass;
|
|
48
|
+
const storedUrl=sessionStorage.getItem(SS_URL);if(storedUrl)els.baseUrl.value=storedUrl;
|
|
49
|
+
function authHeader(){const p=clean(els.adminPass.value);return p?"Basic "+btoa("admin:"+p):""}
|
|
50
|
+
const clean=v=>String(v??"").trim();
|
|
51
|
+
const ESC_MAP={"&":"&","<":"<",">":">",'"':""","'":"'"};
|
|
52
|
+
// esc() is used in BOTH attribute and text contexts; browsers HTML-decode attribute
|
|
53
|
+
// values when reading .value, so round-trips (e.g. agentFilter.value) preserve the original.
|
|
54
|
+
const esc=v=>String(v??"").replace(/[&<>"']/g,c=>ESC_MAP[c]);
|
|
55
|
+
const parse=v=>{if(!v)return null;if(typeof v==="object")return v;try{return JSON.parse(v)}catch{return null}};
|
|
56
|
+
const list=v=>Array.isArray(v)?v:Array.isArray(v?.items)?v.items:Array.isArray(v?.data)?v.data:[];
|
|
57
|
+
const eventKind=e=>clean(e?.kind||e?.type||e?.status||"unknown");
|
|
58
|
+
const eventText=e=>clean(e?.summary||e?.detail||e?.message||"");
|
|
59
|
+
const eventAgents=e=>[e?.authorId,...(Array.isArray(e?.targetIds)?e.targetIds:[])].map(clean).filter(Boolean);
|
|
60
|
+
const timeAgo=iso=>{const diff=Date.now()-new Date(iso).getTime();if(!Number.isFinite(diff))return"unknown";const m=Math.round(diff/6e4);if(m<1)return"just now";if(m<60)return`${m}m ago`;const h=Math.round(m/60);if(h<24)return`${h}h ago`;return`${Math.round(h/24)}d ago`};
|
|
61
|
+
const rangeMs=()=>({"15m":9e5,"1h":36e5,"6h":216e5,"24h":864e5}[els.timeFilter.value]||36e5);
|
|
62
|
+
const rangeStart=()=>new Date(Date.now()-rangeMs());
|
|
63
|
+
async function getJson(url){const headers={accept:"application/json"};const auth=authHeader();if(auth)headers.authorization=auth;const res=await fetch(url,{headers});if(res.status===401){sessionStorage.removeItem(SS_PASS);els.adminPass.value="";throw new Error("401 unauthorized — enter admin password and refresh")}if(!res.ok)throw new Error(`${res.status} ${res.statusText}`);return res.json()}
|
|
64
|
+
async function loadRoster(baseUrl){return list(await getJson(baseUrl+"/Agent"))}
|
|
65
|
+
async function loadWorkspace(baseUrl,agentIds){const rows=await Promise.all(agentIds.map(async id=>{try{return[id,await getJson(baseUrl+"/WorkspaceLatest/"+encodeURIComponent(id))]}catch{return[id,null]}}));state.workspace=Object.fromEntries(rows)}
|
|
66
|
+
function deriveAgent(agent){
|
|
67
|
+
const id=clean(agent.id||agent.agentId||agent.name);
|
|
68
|
+
const related=state.events.filter(event=>eventAgents(event).includes(id));
|
|
69
|
+
const latest=related.at(-1);
|
|
70
|
+
const ws=state.workspace[id];
|
|
71
|
+
const snapshot=parse(ws?.snapshot);
|
|
72
|
+
const lastAt=clean(latest?.createdAt||ws?.timestamp||ws?.createdAt||agent.updatedAt||agent.createdAt);
|
|
73
|
+
const online=lastAt&&(Date.now()-new Date(lastAt).getTime())<6e5;
|
|
74
|
+
return{id,name:clean(agent.name||id),role:clean(agent.role||agent.type||"agent"),status:online?"online":"offline",lastAt,currentTask:clean(parse(latest?.detail)?.taskId||latest?.refId||latest?.summary||"idle"),latestKind:eventKind(latest),workspace:clean(snapshot?.gitBranch||ws?.gitBranch||ws?.path||"unavailable")};
|
|
75
|
+
}
|
|
76
|
+
function filteredEvents(){const agent=els.agentFilter.value,kind=els.kindFilter.value,start=rangeStart();return state.events.filter(e=>{if(new Date(e.createdAt||0)<start)return false;if(agent&&!eventAgents(e).includes(agent))return false;if(kind&&eventKind(e)!==kind)return false;return true}).slice().reverse()}
|
|
77
|
+
function renderSummary(cards,events){const items=[["Agents",cards.length],["Online",cards.filter(c=>c.status==="online").length],["Events",events.length],["Last Poll",state.latestPoll?timeAgo(state.latestPoll):"never"]];els.summary.innerHTML=items.map(([l,v])=>`<div class="chip"><strong>${v}</strong><span>${l}</span></div>`).join("")}
|
|
78
|
+
function renderFilters(){const agents=state.agents.map(a=>clean(a.id||a.agentId||a.name)).filter(Boolean).sort();const kinds=[...new Set(state.events.map(eventKind).filter(Boolean))].sort();const chosenAgent=els.agentFilter.value,chosenKind=els.kindFilter.value;els.agentFilter.innerHTML=`<option value="">All agents</option>`+agents.map(id=>`<option value="${esc(id)}" ${id===chosenAgent?"selected":""}>${esc(id)}</option>`).join("");els.kindFilter.innerHTML=`<option value="">All kinds</option>`+kinds.map(kind=>`<option value="${esc(kind)}" ${kind===chosenKind?"selected":""}>${esc(kind)}</option>`).join("")}
|
|
79
|
+
function renderAgents(cards){els.agentStatus.textContent=`${cards.length} agents via /Agent`;els.agentGrid.innerHTML=cards.length?cards.map(agent=>`<article class="card"><div class="row"><h3>${esc(agent.name)}</h3><span class="badge ${agent.status==="online"?"ok":"warn"}">${esc(agent.status)}</span></div><div class="meta">${esc(agent.role)}</div><div class="row"><span class="kind">${esc(agent.latestKind)}</span><span class="meta">${agent.lastAt?timeAgo(agent.lastAt):"no signal"}</span></div><p><strong>Current task:</strong> ${esc(agent.currentTask)}</p><div class="workspace">Workspace: ${esc(agent.workspace)}</div></article>`).join(""):`<div class="empty">No agents found.</div>`}
|
|
80
|
+
function renderEvents(events){els.feedStatus.textContent=`${events.length} matching events`;els.eventFeed.innerHTML=events.length?events.map(event=>`<article class="event"><div class="row"><h3>${esc(clean(event.summary||eventKind(event)))}</h3><span class="meta">${timeAgo(event.createdAt)}</span></div><div class="row"><span class="kind">${esc(eventKind(event))}</span><span class="badge">${esc(eventAgents(event).join(", "))||"org-wide"}</span></div><p>${esc(eventText(event))||"No detail provided."}</p></article>`).join(""):`<div class="empty">No events match the current filters.</div>`}
|
|
81
|
+
function render(){renderFilters();const cards=state.agents.map(deriveAgent);const events=filteredEvents();renderSummary(cards,events);renderAgents(cards);renderEvents(events)}
|
|
82
|
+
async function refresh(){const baseUrl=clean(els.baseUrl.value).replace(/\/$/,"");const since=rangeStart().toISOString();els.feedStatus.textContent="Polling Flair…";try{const [agents,events]=await Promise.all([loadRoster(baseUrl),getJson(`${baseUrl}/OrgEventCatchup/${encodeURIComponent(state.anchorAgent)}?since=${encodeURIComponent(since)}`)]);state.agents=agents;state.events=list(events).sort((a,b)=>clean(a.createdAt).localeCompare(clean(b.createdAt)));await loadWorkspace(baseUrl,state.agents.map(a=>clean(a.id||a.agentId||a.name)).filter(Boolean));state.latestPoll=new Date().toISOString();render()}catch(error){els.feedStatus.textContent=`Poll failed: ${error.message}`}}
|
|
83
|
+
function schedule(){clearInterval(state.timer);state.timer=setInterval(refresh,5e3)}
|
|
84
|
+
[els.agentFilter,els.kindFilter,els.timeFilter].forEach(el=>el.addEventListener("change",()=>{render();if(el===els.timeFilter)refresh()}));
|
|
85
|
+
els.refresh.addEventListener("click",refresh);
|
|
86
|
+
els.baseUrl.addEventListener("change",()=>{sessionStorage.setItem(SS_URL,clean(els.baseUrl.value));refresh();schedule()});
|
|
87
|
+
els.adminPass.addEventListener("change",()=>{sessionStorage.setItem(SS_PASS,clean(els.adminPass.value));refresh()});
|
|
88
|
+
refresh();schedule();
|
|
89
|
+
</script>
|
|
90
|
+
</body>
|
|
91
|
+
</html>
|