From b4b5df91f0c5721b8827c0c10f60f53916fd83d8 Mon Sep 17 00:00:00 2001 From: artheru Date: Thu, 11 Jun 2026 02:58:51 +0800 Subject: [PATCH] legion: agent cards reused + class runtime properties (1.2.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AgentCard extracted; Browse and My Legion render the identical card; Legion cards open the same ClassModal (instances + Use + chat) - class properties: managed (amerc-managed, loads amerc skills), terminal_mode new|shared per session, skills_reload for shared terminals โ€” publish form, API create/patch, badges in the modal - kebab coordination: broker spec delivered via TerminalMan 155; kebab already heartbeats instance 2 with the skills array (status lost -> stabilizing); monitor armed for online Co-Authored-By: Claude Fable 5 --- package.json | 2 +- server/amerc-api.mjs | 17 +++++++--- src/AgentPlatform.jsx | 77 ++++++++++++++++++++++++++++--------------- src/Scene2D.jsx | 3 +- src/styles.css | 12 +++++++ 5 files changed, 79 insertions(+), 32 deletions(-) diff --git a/package.json b/package.json index bd07f17..448abfe 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "amerc-site", - "version": "1.1.0-chat", + "version": "1.2.0-legioncards", "private": true, "type": "module", "scripts": { diff --git a/server/amerc-api.mjs b/server/amerc-api.mjs index 57a679b..c8efa9a 100644 --- a/server/amerc-api.mjs +++ b/server/amerc-api.mjs @@ -76,6 +76,12 @@ db.exec(` try { db.exec("ALTER TABLE api_keys ADD COLUMN owner TEXT DEFAULT ''"); } catch { /* column exists */ } try { db.exec("ALTER TABLE agent_instances ADD COLUMN skills TEXT DEFAULT ''"); } catch { /* column exists */ } +// class runtime properties: amerc-managed (loads amerc skills), terminal mode +// per session ('new' | 'shared'), and whether a shared terminal reloads the +// client's skills on each new session. +try { db.exec("ALTER TABLE agent_classes ADD COLUMN managed INTEGER DEFAULT 0"); } catch { /* column exists */ } +try { db.exec("ALTER TABLE agent_classes ADD COLUMN terminal_mode TEXT DEFAULT 'new'"); } catch { /* column exists */ } +try { db.exec("ALTER TABLE agent_classes ADD COLUMN skills_reload INTEGER DEFAULT 1"); } catch { /* column exists */ } // ---- live chat plumbing (in-memory; survives nothing, stores nothing) ------- // waiters: long-poll watchers per session, woken when a message lands. @@ -464,8 +470,9 @@ const server = http.createServer(async (req, res) => { const tags = Array.isArray(b.tags) ? b.tags.join(',') : (b.tags || ''); let slug = slugify(b.slug || b.name); let n = 1; while (db.prepare('SELECT id FROM agent_classes WHERE slug=?').get(slug)) slug = `${slugify(b.name)}-${++n}`; - const info = db.prepare('INSERT INTO agent_classes(slug,name,owner,tags,description,kind,command,visibility,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?)') - .run(slug, b.name || 'Agent', who(id), tags, b.description || '', b.kind || 'codex', b.command || '', b.visibility === 'private' ? 'private' : 'public', now, now); + const info = db.prepare('INSERT INTO agent_classes(slug,name,owner,tags,description,kind,command,visibility,managed,terminal_mode,skills_reload,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)') + .run(slug, b.name || 'Agent', who(id), tags, b.description || '', b.kind || 'codex', b.command || '', b.visibility === 'private' ? 'private' : 'public', + b.managed ? 1 : 0, b.terminalMode === 'shared' ? 'shared' : 'new', b.skillsReload === false ? 0 : 1, now, now); return send(res, 200, { ok: true, id: Number(info.lastInsertRowid), slug }); } if ((m = path.match(/^\/agents\/classes\/(\d+)$/)) && method === 'GET') { @@ -481,8 +488,10 @@ const server = http.createServer(async (req, res) => { if (!owns(c)) return send(res, 403, { ok: false, error: 'not owner' }); if (method === 'DELETE') { db.prepare('DELETE FROM agent_classes WHERE id=?').run(c.id); db.prepare('DELETE FROM agent_instances WHERE class_id=?').run(c.id); return send(res, 200, { ok: true }); } const b = await readBody(req) || {}; const tags = Array.isArray(b.tags) ? b.tags.join(',') : (b.tags ?? c.tags); - db.prepare('UPDATE agent_classes SET name=?,tags=?,description=?,kind=?,command=?,visibility=?,updated_at=? WHERE id=?') - .run(b.name ?? c.name, tags, b.description ?? c.description, b.kind ?? c.kind, b.command ?? c.command, b.visibility ?? c.visibility, Date.now(), c.id); + db.prepare('UPDATE agent_classes SET name=?,tags=?,description=?,kind=?,command=?,visibility=?,managed=?,terminal_mode=?,skills_reload=?,updated_at=? WHERE id=?') + .run(b.name ?? c.name, tags, b.description ?? c.description, b.kind ?? c.kind, b.command ?? c.command, b.visibility ?? c.visibility, + b.managed != null ? (b.managed ? 1 : 0) : c.managed, b.terminalMode != null ? (b.terminalMode === 'shared' ? 'shared' : 'new') : c.terminal_mode, + b.skillsReload != null ? (b.skillsReload ? 1 : 0) : c.skills_reload, Date.now(), c.id); return send(res, 200, { ok: true }); } // subscription token for a class diff --git a/src/AgentPlatform.jsx b/src/AgentPlatform.jsx index 27ab2a3..4f2730a 100644 --- a/src/AgentPlatform.jsx +++ b/src/AgentPlatform.jsx @@ -8,6 +8,24 @@ const TAG_EMOJI = { backend: '๐Ÿ› ๏ธ', model: '๐Ÿง ', frontend: '๐ŸŽจ', ui: ' const classEmoji = (c) => { for (const t of (c.tags || [])) if (TAG_EMOJI[t]) return TAG_EMOJI[t]; return '๐Ÿ—ก๏ธ'; }; function avatarStyle(name) { let h = 0; for (const ch of String(name)) h = (h * 31 + ch.charCodeAt(0)) >>> 0; const a = h % 360, b = (a + 70 + h % 90) % 360; return { background: `linear-gradient(135deg, hsl(${a} 68% 46%), hsl(${b} 64% 32%))` }; } +// One agent card, used identically in Browse Agents and My Legion. +export function AgentCard({ c, onClick }) { + return ( + + ); +} + // ---- Browse Agents: all public classes, filter by tag -------------------- export function AgentBrowse({ auth, setRoute }) { const [classes, setClasses] = useState([]); @@ -82,20 +100,7 @@ export function AgentBrowse({ auth, setRoute }) {
))} - {loaded && classes.map((c) => ( - - ))} + {loaded && classes.map((c) => openClass(c)} />)} {loaded && classes.length > 0 && ( : Log in to subscribe.} @@ -270,9 +280,10 @@ export function LegionDashboard({ auth, setRoute }) { const [classes, setClasses] = useState([]); const [insts, setInsts] = useState([]); const [err, setErr] = useState(''); - const [form, setForm] = useState({ name: '', tags: '', description: '', kind: 'codex', command: 'codex --yolo', visibility: 'public' }); + const [form, setForm] = useState({ name: '', tags: '', description: '', kind: 'codex', command: 'codex --yolo', visibility: 'public', managed: false, terminalMode: 'new', skillsReload: true }); const [newInst, setNewInst] = useState(null); const [logOpen, setLogOpen] = useState(null); // chatlog session to reopen + const [openCls, setOpenCls] = useState(null); // class modal (same as Browse) const [loaded, setLoaded] = useState(false); useEffect(() => { if (!newInst) return; @@ -344,6 +355,18 @@ export function LegionDashboard({ auth, setRoute }) { setForm({ ...form, command: e.target.value })} />

Kind is your agent's runtime; the launch command is how your broker starts it.

+
+ + + {form.terminalMode === 'shared' && ( + + )} +
@@ -369,17 +392,18 @@ export function LegionDashboard({ auth, setRoute }) {
) : ( <> -

My classes

- {classes.map((c) => ( -
-
- {classEmoji(c)} - {c.name}{c.visibility} - {c.onlineCount}/{c.instanceCount} -
-
{c.tags.map((t) => {t})}
-
- ))} +

My classes โ€” your banners on the roster

+
+ {classes.map((c) => ( +
+ setOpenCls(c.id)} /> +
+ {c.visibility} + +
+
+ ))} +

My instances

{insts.map((i) => (
@@ -406,6 +430,7 @@ export function LegionDashboard({ auth, setRoute }) {
)} + {openCls && { setOpenCls(null); load(); }} />} {newInst && (() => { const hb = `while true; do curl -s -XPOST https://amerc.ai/api/agents/instances/${newInst.id}/heartbeat -H 'content-type: application/json' -d '{"accesskey":"${newInst.accesskey}","status":"online","broker":"my-broker"}' >/dev/null; sleep 10; done`; return ( diff --git a/src/Scene2D.jsx b/src/Scene2D.jsx index 0c2e7fd..17d5017 100644 --- a/src/Scene2D.jsx +++ b/src/Scene2D.jsx @@ -10,7 +10,7 @@ import { api } from './api.js'; const Menu = ({ size = 20 }) => (); const X = ({ size = 20 }) => (); -const RELEASE = '1.1.0-chat'; +const RELEASE = '1.2.0-legioncards'; const NAV = [ { id: 'home', label: 'Tavern' }, @@ -477,6 +477,7 @@ function StatusPage() { } const CHANGELOG = [ + { v: '1.2', date: 'Jun 2026', title: 'Legion fields agent cards', notes: ['Your classes in My Legion are now the same agent cards as Browse Agents โ€” open one to see and use its instances right there', 'New class properties at publish time: amerc-managed (loads amerc skills), terminal-per-session vs shared terminal, and skills-reload on shared terminals'] }, { v: '1.1', date: 'Jun 2026', title: 'Chat, rebuilt native', notes: ['Agent chat now runs on amerc itself โ€” no external relay, so it works whenever the site is up', 'Send files and images both ways: relayed live with a progress bar, never stored on the server', 'The chat shows which skills the agent has loaded', 'Every session keeps its chatlog โ€” reopen any conversation from My Legion'] }, { v: '1.0', date: 'Jun 2026', title: 'Legion & My Mansion', notes: ['My Booth is now your Legion โ€” your war-band of agent classes and the instances you field', 'The Mansion became My Mansion, your estate of live services'] }, { v: '0.99', date: 'Jun 2026', title: 'Clearer publish form', notes: ['The Booth publish form now labels every field, so the pre-filled Kind and Launch command are no longer ambiguous'] }, diff --git a/src/styles.css b/src/styles.css index df163c8..c381954 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1628,3 +1628,15 @@ button { .ch-log-last { color: #8595ab; font-size: 11.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .ch-log-meta { display: flex; flex-direction: column; gap: 2px; align-items: flex-end; color: #5b6b7d; font-size: 10.5px; flex: 0 0 auto; } .ap-modal-chat { width: min(660px, 94vw); padding: 10px; } + +/* class runtime properties + Legion card reuse (1.2) */ +.ap-props { display: flex; flex-wrap: wrap; gap: 6px; margin: 10px 0 4px; } +.ap-prop { background: #131a29; border: 1px solid #243049; color: #9fb2c8; border-radius: 999px; padding: 3px 11px; font-size: 11px; } +.ap-prop.managed { background: rgba(54, 240, 176, 0.07); border-color: #1f5642; color: #5fe0b0; } +.ap-pub-props { display: flex; flex-direction: column; gap: 9px; margin: 4px 0 2px; padding: 10px 12px; background: #0d1320; border: 1px solid #20283c; border-radius: 10px; } +.ap-check { display: flex; align-items: center; gap: 8px; color: #b6c5da; font-size: 12px; cursor: pointer; } +.ap-check input { accent-color: #46c8e0; width: 14px; height: 14px; } +.ap-mine-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 12px; margin-bottom: 14px; } +.ap-mine-cardwrap { display: flex; flex-direction: column; gap: 7px; } +.ap-mine-cardwrap .ap-card { width: 100%; } +.ap-mine-actions { display: flex; align-items: center; justify-content: space-between; gap: 8px; }