@cotal-ai/cli 0.4.0 → 0.5.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/dist/web/app.js CHANGED
@@ -41,6 +41,42 @@ function ago(ts) {
41
41
  const agoShort = (ts) => (ago(ts) === "just now" ? "now" : ago(ts));
42
42
  const plural = (n, w) => `${n} ${w}${n === 1 ? "" : "s"}`;
43
43
 
44
+ // ── Harness (host connector) branding ─────────────────────────────────────────
45
+ // A brand colour + logo (drawn in currentColor) per connector, keyed by the card's meta.connector.
46
+ // Claude and OpenCode use their official marks (public-domain SVG data from Simple Icons, CC0);
47
+ // Hermes/Nous Research has no clean official mark, so it gets a custom messenger glyph. Unknown
48
+ // connectors degrade to a neutral badge with the raw name (the compact roster form omits the icon).
49
+ // Icons are inline SVG — no network, no extra files.
50
+ const HARNESS = {
51
+ claude: {
52
+ label: "Claude Code",
53
+ color: "#d97757", // official Claude clay
54
+ icon: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="m4.7144 15.9555 4.7174-2.6471.079-.2307-.079-.1275h-.2307l-.7893-.0486-2.6956-.0729-2.3375-.0971-2.2646-.1214-.5707-.1215-.5343-.7042.0546-.3522.4797-.3218.686.0608 1.5179.1032 2.2767.1578 1.6514.0972 2.4468.255h.3886l.0546-.1579-.1336-.0971-.1032-.0972L6.973 9.8356l-2.55-1.6879-1.3356-.9714-.7225-.4918-.3643-.4614-.1578-1.0078.6557-.7225.8803.0607.2246.0607.8925.686 1.9064 1.4754 2.4893 1.8336.3643.3035.1457-.1032.0182-.0728-.164-.2733-1.3539-2.4467-1.445-2.4893-.6435-1.032-.17-.6194c-.0607-.255-.1032-.4674-.1032-.7285L6.287.1335 6.6997 0l.9957.1336.419.3642.6192 1.4147 1.0018 2.2282 1.5543 3.0296.4553.8985.2429.8318.091.255h.1579v-.1457l.1275-1.706.2368-2.0947.2307-2.6957.0789-.7589.3764-.9107.7468-.4918.5828.2793.4797.686-.0668.4433-.2853 1.8517-.5586 2.9021-.3643 1.9429h.2125l.2429-.2429.9835-1.3053 1.6514-2.0643.7286-.8196.85-.9046.5464-.4311h1.0321l.759 1.1293-.34 1.1657-1.0625 1.3478-.8804 1.1414-1.2628 1.7-.7893 1.36.0729.1093.1882-.0183 2.8535-.607 1.5421-.2794 1.8396-.3157.8318.3886.091.3946-.3278.8075-1.967.4857-2.3072.4614-3.4364.8136-.0425.0304.0486.0607 1.5482.1457.6618.0364h1.621l3.0175.2247.7892.522.4736.6376-.079.4857-1.2142.6193-1.6393-.3886-3.825-.9107-1.3113-.3279h-.1822v.1093l1.0929 1.0686 2.0035 1.8092 2.5075 2.3314.1275.5768-.3218.4554-.34-.0486-2.2039-1.6575-.85-.7468-1.9246-1.621h-.1275v.17l.4432.6496 2.3436 3.5214.1214 1.0807-.17.3521-.6071.2125-.6679-.1214-1.3721-1.9246L14.38 17.959l-1.1414-1.9428-.1397.079-.674 7.2552-.3156.3703-.7286.2793-.6071-.4614-.3218-.7468.3218-1.4753.3886-1.9246.3157-1.53.2853-1.9004.17-.6314-.0121-.0425-.1397.0182-1.4328 1.9672-2.1796 2.9446-1.7243 1.8456-.4128.164-.7164-.3704.0667-.6618.4008-.5889 2.386-3.0357 1.4389-1.882.929-1.0868-.0062-.1579h-.0546l-6.3385 4.1164-1.1293.1457-.4857-.4554.0608-.7467.2307-.2429 1.9064-1.3114Z"/></svg>`,
55
+ },
56
+ opencode: {
57
+ label: "OpenCode",
58
+ color: "#cdd6e0", // OpenCode is monochrome by brand; rendered light on the dark UI
59
+ icon: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M22 24H2V0h20zM17 4.8H7v14.4h10z"/></svg>`,
60
+ },
61
+ hermes: {
62
+ label: "Hermes",
63
+ color: "#a78bfa", // no official mark — custom messenger glyph, violet
64
+ icon: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 3 3 10.5l7 2.5 2.5 7L21 3Z"/><path d="M21 3 10 13"/></svg>`,
65
+ },
66
+ };
67
+ // Render the harness badge: a branded pill (icon + label) by default, or `{compact:true}` for a
68
+ // bare colour icon (the roster row). The inline style only ever takes a value from HARNESS, never
69
+ // raw card input, so meta.connector can't inject CSS.
70
+ function harnessBadge(connector, opts = {}) {
71
+ if (!connector) return "";
72
+ const h = HARNESS[connector];
73
+ const color = h ? h.color : "var(--dim)";
74
+ const text = h ? h.label : connector;
75
+ if (opts.compact)
76
+ return h ? `<span class="harness-ico" style="color:${color}" title="harness · ${esc(text)}">${h.icon}</span>` : "";
77
+ return `<span class="harness-badge" style="--hc:${color}" title="agent harness · ${esc(text)}">${h ? h.icon : ""}<span class="hl">${esc(text)}</span></span>`;
78
+ }
79
+
44
80
  function setConn(live) {
45
81
  const el = $("conn");
46
82
  el.className = "pill" + (live ? "" : " down");
@@ -68,11 +104,15 @@ function renderTiles(counts, oldest) {
68
104
 
69
105
  // ── Sidebar: roster ───────────────────────────────────────────────────────────
70
106
  function peerRow(p) {
71
- return `<div class="peer ${p.status}">
107
+ // A peer with an id is a click-through into its Agent Detail card; demo rows have no id.
108
+ const nav = p.id ? ` nav${p.id === agentSel ? " sel" : ""}` : "";
109
+ const attrs = p.id ? ` data-agent="${esc(p.id)}" tabindex="0" role="button"` : "";
110
+ return `<div class="peer ${p.status}${nav}"${attrs}>
72
111
  <span class="dot ${p.status}">${GLYPH[p.status] ?? "○"}</span>
73
112
  <div class="c">
74
113
  <div class="l1">
75
114
  <span class="name">${esc(p.name)}</span>
115
+ ${p.harness ? harnessBadge(p.harness, { compact: true }) : ""}
76
116
  ${p.role ? `<span class="role">${esc(p.role)}</span>` : ""}
77
117
  ${p.tag ? `<span class="tag">${esc(p.tag)}</span>` : ""}
78
118
  </div>
@@ -84,6 +124,33 @@ function renderRoster(list) {
84
124
  $("roster").innerHTML = list.length
85
125
  ? list.map(peerRow).join("")
86
126
  : `<div class="empty">no peers</div>`;
127
+ for (const el of $("roster").querySelectorAll(".peer[data-agent]")) {
128
+ el.onclick = () => selectAgent(el.dataset.agent);
129
+ el.onkeydown = (e) => {
130
+ if (e.key === "Enter" || e.key === " ") (e.preventDefault(), selectAgent(el.dataset.agent));
131
+ };
132
+ }
133
+ }
134
+ // The online roster, shaped for the sidebar: offline peers drop out (their count still rides the
135
+ // header tiles), live peers sort by status then name. Re-rendered on its own when a selection
136
+ // changes so the row highlight tracks the open Agent Detail.
137
+ function rosterRows() {
138
+ return [...roster]
139
+ .filter((p) => p.status !== "offline")
140
+ .sort(
141
+ (a, b) =>
142
+ STATUS.indexOf(a.status) - STATUS.indexOf(b.status) ||
143
+ a.card.name.localeCompare(b.card.name),
144
+ )
145
+ .map((p) => ({
146
+ id: p.card.id,
147
+ name: p.card.name,
148
+ role: p.card.role,
149
+ status: p.status,
150
+ act: p.activity,
151
+ harness: p.card.meta?.connector,
152
+ tag: p.status === "waiting" ? "needs input" : null,
153
+ }));
87
154
  }
88
155
 
89
156
  // ── Sidebar: channels ─────────────────────────────────────────────────────────
@@ -122,21 +189,31 @@ function rosterStatus(name) {
122
189
  const r = roster.find((x) => x.card?.name === name);
123
190
  return r ? r.status : "offline";
124
191
  }
192
+ // id→name resolution shared by the DM lens and the all-activity feed. `from` carries a full
193
+ // card (id+name), but a unicast `to` is the bare recipient identity id (a pubkey) — without this
194
+ // it renders raw. Sources: live roster cards, DM senders, and feed senders we've seen.
195
+ function nameIndex() {
196
+ const idName = new Map();
197
+ for (const p of roster) if (p.card?.id) idName.set(p.card.id, p.card.name);
198
+ for (const m of dms) if (m.from?.id && m.from?.name) idName.set(m.from.id, m.from.name);
199
+ for (const e of activity) if (e.msg?.from?.id && e.msg.from.name) idName.set(e.msg.from.id, e.msg.from.name);
200
+ return idName;
201
+ }
202
+ // Resolve an EndpointRef object or a bare id to a display name; an unknown id shrinks to a short
203
+ // prefix, and a string that isn't an identity (already a name) passes through unchanged.
204
+ function displayNameOf(x, idx) {
205
+ if (!x) return "?";
206
+ if (typeof x === "object") return x.name || displayNameOf(x.id, idx);
207
+ if (idx.has(x)) return idx.get(x);
208
+ return /^[A-Z2-7]{32,}$/.test(x) ? x.slice(0, 6) + "…" : x; // unknown identity → short id
209
+ }
210
+
125
211
  // Group raw DMs into per-peer rows; each peer lists its counterparties (conversations).
126
212
  // O(peers-with-DMs), never the n² pair cross-product — only pairs that actually talked.
127
213
  function dmPeers() {
128
214
  if (isDemo) return DEMO.dmPeers;
129
- // `from` is a full card (has a name); `to` is the recipient's identity id. Build an
130
- // id→name map from cards we've seen so recipients show a name, not a pubkey.
131
- const idName = new Map();
132
- for (const p of roster) if (p.card?.id) idName.set(p.card.id, p.card.name);
133
- for (const m of dms) if (m.from?.id && m.from?.name) idName.set(m.from.id, m.from.name);
134
- const nameOf = (x) => {
135
- if (!x) return "?";
136
- if (typeof x === "object") return x.name || nameOf(x.id);
137
- if (idName.has(x)) return idName.get(x);
138
- return /^[A-Z2-7]{32,}$/.test(x) ? x.slice(0, 6) + "…" : x; // unknown identity → short id
139
- };
215
+ const idx = nameIndex();
216
+ const nameOf = (x) => displayNameOf(x, idx);
140
217
  const conv = new Map();
141
218
  for (const m of dms) {
142
219
  const a = nameOf(m.from),
@@ -242,12 +319,12 @@ function rowHTML(e) {
242
319
  </div>
243
320
  </div>`;
244
321
  }
245
- function liveEntry(mode, msg) {
322
+ function liveEntry(mode, msg, idx) {
246
323
  const target =
247
324
  mode === "chat"
248
325
  ? `#${msg.channel ?? ""}`
249
326
  : mode === "unicast"
250
- ? `→ ${msg.to ?? ""}`
327
+ ? `→ ${displayNameOf(msg.to, idx)}`
251
328
  : `→ @${msg.toService ?? ""}`;
252
329
  return {
253
330
  type: "msg",
@@ -264,7 +341,8 @@ function renderAllActivity() {
264
341
  const center = $("center");
265
342
  const prev = center.querySelector(".feed");
266
343
  const atBottom = prev ? prev.scrollHeight - prev.scrollTop - prev.clientHeight < 40 : true;
267
- const rows = (isDemo ? DEMO.activity : activity.map((e) => liveEntry(e.mode, e.msg))).filter(
344
+ const idx = nameIndex();
345
+ const rows = (isDemo ? DEMO.activity : activity.map((e) => liveEntry(e.mode, e.msg, idx))).filter(
268
346
  (e) => !e.mode || modes.has(e.mode),
269
347
  );
270
348
  const sub = isDemo ? "112 recent · live" : `${rows.length} recent · live`;
@@ -449,27 +527,58 @@ function renderRail() {
449
527
  el.onclick = () => selectAgent(el.dataset.agent);
450
528
  }
451
529
 
452
- // ── Agent Detail drill-down (centre) — the forward-looking per-agent frame (docs/web.md) ──
530
+ // ── Agent Detail drill-down (centre) — per-agent frame, rendered from the peer's card (docs/web.md) ──
453
531
  function selectAgent(id) {
454
532
  agentSel = id;
455
533
  dmSel = null;
456
534
  selected = null;
457
535
  renderSidebarNav();
536
+ renderRoster(rosterRows()); // light up the clicked peer (only reached live — demo rows have no id)
458
537
  renderCenter();
459
538
  renderRail();
460
539
  }
461
540
  function renderAgentDetail() {
462
541
  const p = roster.find((x) => x.card.id === agentSel);
463
542
  if (!p) {
464
- $("center").innerHTML = `<div class="detail"><div class="empty">agent no longer present — pick another from NEEDS YOU.</div></div>`;
543
+ $("center").innerHTML = `<div class="detail"><div class="empty">agent no longer present — pick another from the roster or NEEDS YOU.</div></div>`;
465
544
  return;
466
545
  }
546
+ // The AgentCard is the legibility contract: who, in what role, what it can do, on what harness.
547
+ // Render only fields the card actually carries (skills/protocolVersion aren't populated yet).
548
+ const card = p.card;
549
+ const meta = card.meta || {};
467
550
  const waiting = p.status === "waiting";
468
- const who = p.card.role ? `${esc(p.card.name)}<span class="crole">${esc(p.card.role)}</span>` : esc(p.card.name);
551
+ const who = card.role ? `${esc(card.name)}<span class="crole">${esc(card.role)}</span>` : esc(card.name);
469
552
  const since = waiting ? `waiting ${esc(ago(p.ts))}` : `${esc(p.status)} · ${esc(ago(p.ts))}`;
553
+
554
+ const badges = [
555
+ card.kind ? `<span class="d-badge">${esc(card.kind)}</span>` : "",
556
+ harnessBadge(meta.connector),
557
+ meta.model ? `<span class="d-badge model" title="model">${esc(meta.model)}</span>` : "",
558
+ ].join("");
559
+ const desc = card.description ? `<div class="d-desc">${esc(card.description)}</div>` : "";
470
560
  const blocked = waiting
471
561
  ? `<div class="d-label">Blocked on</div><div class="d-block">${esc(p.activity || "waiting for input")}</div>`
472
- : `<div class="d-block muted">${esc(p.activity || "no current activity")}</div>`;
562
+ : `<div class="d-label">Activity</div><div class="d-block muted">${esc(p.activity || "no current activity")}</div>`;
563
+
564
+ const sec = (label, body) => (body ? `<div class="d-sec"><div class="d-label">${esc(label)}</div>${body}</div>` : "");
565
+ const tags = (card.tags || []).length
566
+ ? `<div class="d-chips">${card.tags.map((t) => `<span class="d-chip">${esc(t)}</span>`).join("")}</div>`
567
+ : "";
568
+ const skills = (card.skills || []).length
569
+ ? `<div class="d-skills">${card.skills
570
+ .map((s) => `<div class="d-skill"><span class="nm">${esc(s.name || s.id)}</span>${s.description ? `<span class="dsc">${esc(s.description)}</span>` : ""}</div>`)
571
+ .join("")}</div>`
572
+ : "";
573
+ // Any other meta beyond the badges (connector → harness, model), generically rendered (escaped, key-sorted).
574
+ const extra = Object.entries(meta).filter(([k]) => k !== "connector" && k !== "model").sort(([a], [b]) => a.localeCompare(b));
575
+ const metaKv = extra.length
576
+ ? `<div class="d-kv">${extra
577
+ .map(([k, v]) => `<div class="row"><span class="k">${esc(k)}</span><span class="v">${esc(typeof v === "string" ? v : JSON.stringify(v))}</span></div>`)
578
+ .join("")}</div>`
579
+ : "";
580
+ const proto = card.protocolVersion ? `<span class="d-foot-item">protocol ${esc(card.protocolVersion)}</span>` : "";
581
+
473
582
  $("center").innerHTML = `
474
583
  <div class="detail${waiting ? " amber" : ""}">
475
584
  <div class="d-head">
@@ -478,8 +587,13 @@ function renderAgentDetail() {
478
587
  <span class="d-age">${since}</span>
479
588
  </div>
480
589
  <div class="d-who">${who}</div>
481
- <div class="d-id">${esc(p.card.id.slice(0, 8))}…</div>
590
+ ${badges ? `<div class="d-badges">${badges}</div>` : ""}
591
+ ${desc}
482
592
  ${blocked}
593
+ ${sec("Tags", tags)}
594
+ ${sec("Skills", skills)}
595
+ ${sec("Metadata", metaKv)}
596
+ <div class="d-foot"><span class="d-foot-item id">${esc(card.id)}</span>${proto}</div>
483
597
  </div>`;
484
598
  }
485
599
 
@@ -498,24 +612,7 @@ function refreshDerived() {
498
612
  const oldest = waiting.length ? agoShort(Math.min(...waiting.map((p) => p.ts))) : "—";
499
613
  renderTiles(counts, oldest);
500
614
  $("online-c").textContent = roster.filter((p) => p.status !== "offline").length;
501
- // The roster sidebar is the ONLINE list offline peers drop out (their count still rides
502
- // in the header tiles). They reappear here the moment presence flips them back on.
503
- renderRoster(
504
- [...roster]
505
- .filter((p) => p.status !== "offline")
506
- .sort(
507
- (a, b) =>
508
- STATUS.indexOf(a.status) - STATUS.indexOf(b.status) ||
509
- a.card.name.localeCompare(b.card.name),
510
- )
511
- .map((p) => ({
512
- name: p.card.name,
513
- role: p.card.role,
514
- status: p.status,
515
- act: p.activity,
516
- tag: p.status === "waiting" ? "needs input" : null,
517
- })),
518
- );
615
+ renderRoster(rosterRows()); // online list; offline peers drop out but still ride the header tiles
519
616
  renderDMs(); // peer statuses may have changed
520
617
  renderRail();
521
618
  if (agentSel) renderCenter(); // keep an open Agent Detail live as the peer's status/activity changes
@@ -528,6 +625,7 @@ async function select(key) {
528
625
  selected = key;
529
626
  if (key !== "*") unread.set(key, 0);
530
627
  renderSidebarNav();
628
+ if (!isDemo) renderRoster(rosterRows()); // clear any stale Agent Detail highlight
531
629
  if (isDemo) return (renderCenter(), renderRail());
532
630
  if (key !== "*") {
533
631
  const seq = ++loadSeq;
@@ -546,6 +644,7 @@ function selectDM(peer, w) {
546
644
  dmSel = { peer, with: w || (pe && pe.conversations[0] ? pe.conversations[0].with : null) };
547
645
  selected = null;
548
646
  renderSidebarNav();
647
+ if (!isDemo) renderRoster(rosterRows()); // clear any stale Agent Detail highlight
549
648
  renderCenter();
550
649
  renderRail();
551
650
  }
@@ -624,12 +723,12 @@ const bd = [
624
723
  const lm = [{ ts: "10:15", who: "maya", status: "idle", body: "sent the NATS v3 notes your way" }];
625
724
  const DEMO = {
626
725
  roster: [
627
- { name: "alice", role: "planner", status: "waiting", tag: "needs input", act: "blocked — needs OPENAI_API_KEY" },
628
- { name: "linus", role: "reviewer", status: "working", act: "reviewing PR #42 · auth guards" },
629
- { name: "bob", role: "builder", status: "working", act: "writing tests · channels.ts" },
630
- { name: "dave", role: "builder", status: "working", act: "refactoring endpoint.ts" },
631
- { name: "maya", role: "researcher", status: "idle", act: "—" },
632
- { name: "scout", role: "observer", status: "idle", act: "watching #team.>" },
726
+ { name: "alice", role: "planner", status: "waiting", tag: "needs input", act: "blocked — needs OPENAI_API_KEY", harness: "claude" },
727
+ { name: "linus", role: "reviewer", status: "working", act: "reviewing PR #42 · auth guards", harness: "opencode" },
728
+ { name: "bob", role: "builder", status: "working", act: "writing tests · channels.ts", harness: "claude" },
729
+ { name: "dave", role: "builder", status: "working", act: "refactoring endpoint.ts", harness: "hermes" },
730
+ { name: "maya", role: "researcher", status: "idle", act: "—", harness: "opencode" },
731
+ { name: "scout", role: "observer", status: "idle", act: "watching #team.>", harness: "claude" },
633
732
  ],
634
733
  activity: [
635
734
  { type: "sys", text: "— scout joined · observer —" },
@@ -96,6 +96,11 @@
96
96
  font-size: 9.5px; font-weight: 600; padding: 1px 6px; border-radius: 4px;
97
97
  white-space: nowrap; background: var(--tag-amber); color: var(--amber);
98
98
  }
99
+ /* A peer row is a click-through into its Agent Detail card. */
100
+ .peer.nav { cursor: pointer; }
101
+ .peer.nav:hover { background: #ffffff06; }
102
+ .peer.nav.sel { background: var(--sel); }
103
+ .peer.nav:focus-visible { outline: 2px solid var(--blue); outline-offset: -2px; }
99
104
 
100
105
  /* ── Channels ── */
101
106
  .chan {
@@ -263,12 +268,51 @@
263
268
  .detail .d-age { margin-left: auto; font-size: 11px; color: var(--faint); }
264
269
  .detail .d-who { font-size: 20px; font-weight: 700; color: var(--fg); display: flex; align-items: baseline; }
265
270
  .detail .d-who .crole { font-size: 12px; font-weight: 500; color: var(--faint); margin-left: 8px; }
266
- .detail .d-id { font-size: 11px; color: var(--faint); font-family: ui-monospace, monospace; margin-top: -7px; }
267
271
  .detail .d-label { font-size: 10.5px; font-weight: 600; letter-spacing: .4px; color: var(--faint); text-transform: uppercase; }
268
272
  .detail .d-block { font-size: 13px; color: var(--fg); line-height: 1.5; padding: 12px 14px; border-radius: 8px;
269
273
  background: var(--tile); border: 1px solid var(--line); white-space: pre-wrap; word-break: break-word; }
270
274
  .detail.amber .d-block { border-color: var(--amber); background: var(--tint-amber); }
271
275
  .detail .d-block.muted { color: var(--dim); }
276
+ .detail .d-badges { display: flex; flex-wrap: wrap; gap: 6px; margin-top: -6px; }
277
+ .detail .d-badge {
278
+ font-size: 10.5px; font-weight: 600; padding: 2px 8px; border-radius: 5px;
279
+ background: var(--tile); border: 1px solid var(--line); color: var(--dim);
280
+ }
281
+ .detail .d-badge.model { font-family: ui-monospace, monospace; color: var(--fg); letter-spacing: -.2px; }
282
+ /* Harness (host connector) badge — brand-coloured via --hc; used in the detail view (pill
283
+ with label) and the roster row (bare icon). */
284
+ .harness-badge {
285
+ display: inline-flex; align-items: center; gap: 6px;
286
+ font-size: 11px; font-weight: 600; padding: 3px 10px 3px 8px; border-radius: 999px;
287
+ color: var(--hc); border: 1px solid color-mix(in srgb, var(--hc) 45%, transparent);
288
+ background: color-mix(in srgb, var(--hc) 15%, transparent);
289
+ }
290
+ .harness-ico { display: inline-flex; align-items: center; flex: none; }
291
+ .harness-badge svg, .harness-ico svg { width: 13px; height: 13px; display: block; }
292
+ .detail .d-desc { font-size: 13px; color: var(--fg); line-height: 1.5; }
293
+ .detail .d-sec { display: flex; flex-direction: column; gap: 7px; }
294
+ .detail .d-chips { display: flex; flex-wrap: wrap; gap: 6px; }
295
+ .detail .d-chip {
296
+ font-size: 11px; padding: 2px 9px; border-radius: 12px;
297
+ background: var(--tile); border: 1px solid var(--line); color: var(--dim);
298
+ }
299
+ .detail .d-skills { display: flex; flex-direction: column; gap: 8px; }
300
+ .detail .d-skill {
301
+ display: flex; flex-direction: column; gap: 1px; padding: 8px 12px;
302
+ border-radius: 8px; background: var(--tile); border: 1px solid var(--line);
303
+ }
304
+ .detail .d-skill .nm { font-size: 12.5px; font-weight: 600; color: var(--fg); }
305
+ .detail .d-skill .dsc { font-size: 11.5px; color: var(--dim); }
306
+ .detail .d-kv { display: flex; flex-direction: column; gap: 4px; }
307
+ .detail .d-kv .row { display: flex; gap: 10px; font-size: 12px; }
308
+ .detail .d-kv .k { color: var(--faint); min-width: 90px; flex: none; }
309
+ .detail .d-kv .v { color: var(--fg); word-break: break-word; }
310
+ .detail .d-foot {
311
+ display: flex; flex-wrap: wrap; gap: 6px 14px; margin-top: 4px;
312
+ padding-top: 11px; border-top: 1px solid var(--line);
313
+ }
314
+ .detail .d-foot-item { font-size: 10.5px; color: var(--faint); }
315
+ .detail .d-foot-item.id { font-family: ui-monospace, monospace; word-break: break-all; }
272
316
  </style>
273
317
  </head>
274
318
  <body>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@cotal-ai/cli",
3
3
  "description": "Cotal mesh CLI: up, join, watch, console, web, spawn, mint, channels, history.",
4
- "version": "0.4.0",
4
+ "version": "0.5.0",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",
@@ -21,7 +21,7 @@
21
21
  "@clack/prompts": "^1.0.0",
22
22
  "ink": "^6.0.0",
23
23
  "react": "^19.0.0",
24
- "@cotal-ai/core": "0.4.0"
24
+ "@cotal-ai/core": "0.5.0"
25
25
  },
26
26
  "optionalDependencies": {
27
27
  "@eplightning/nats-server-darwin-arm64": "^2.14.0",