@ninemind/agentgem 0.2.0 → 0.3.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.
@@ -157,6 +157,13 @@
157
157
  .prow .pm{margin-left:auto;color:var(--muted);font-size:12px;white-space:nowrap;font-family:var(--mono)}
158
158
  .redtag{color:var(--accent);font-size:11px}
159
159
  pre.json{background:var(--card);border:1px solid var(--line);border-radius:var(--r);padding:12px;font:12px/1.6 var(--mono);white-space:pre-wrap;word-break:break-word;margin:0}
160
+ /* ── channel picker ── */
161
+ #channelPicker{border:1px solid var(--line);border-radius:var(--r);padding:10px 13px;margin-bottom:12px;background:var(--card)}
162
+ #channelPicker legend{font-family:var(--mono);font-size:10px;text-transform:uppercase;letter-spacing:.14em;color:var(--muted);padding:0 4px}
163
+ #channelPicker legend .d{color:var(--muted);font-size:10px;font-weight:400;text-transform:none;letter-spacing:0}
164
+ #channelPicker .ch-labels{display:flex;flex-wrap:wrap;gap:6px 14px;margin-top:6px}
165
+ #channelPicker label{display:flex;align-items:center;gap:5px;font-size:13px;cursor:pointer}
166
+ #channelPicker input[type=checkbox]{accent-color:var(--accent);margin:0}
160
167
  /* ── filters ── */
161
168
  #filterPanel{border:1px solid var(--line);border-radius:var(--r);padding:11px 13px;margin-bottom:12px;background:var(--accent-soft)}
162
169
  #filterPanel[hidden]{display:none}
@@ -242,8 +249,19 @@
242
249
  <div class="bar" id="typeBar"></div>
243
250
  </div>
244
251
  <div class="bar" id="selbar"><strong style="flex:1;font-size:12px;text-transform:uppercase;letter-spacing:.05em;color:var(--muted)">Selection</strong><button id="all" class="ghost">Select all shown</button><button id="none" class="ghost">Clear</button></div>
245
- <div class="bar"><button id="importBtn" class="ghost">⬇ Import from machine…</button><span class="d" id="importStatus" style="margin-left:8px"></span></div>
252
+ <div class="bar"><button id="importBtn" class="ghost">⬇ Import from machine…</button><button id="getBtn" class="ghost">🔗 Get gems…</button><span class="d" id="importStatus" style="margin-left:8px"></span></div>
246
253
  <div id="inventory">Loading…</div>
254
+ <fieldset id="channelPicker" class="pgroup">
255
+ <legend>Channels <span class="d">(how this Gem is reached)</span></legend>
256
+ <div class="ch-labels">
257
+ <label><input type="checkbox" class="ch" value="slack"> Slack</label>
258
+ <label><input type="checkbox" class="ch" value="telegram"> Telegram</label>
259
+ <label><input type="checkbox" class="ch" value="discord"> Discord</label>
260
+ <label><input type="checkbox" class="ch" value="teams"> Teams</label>
261
+ <label><input type="checkbox" class="ch" value="twilio"> Twilio</label>
262
+ <label><input type="checkbox" class="ch" value="github"> GitHub</label>
263
+ </div>
264
+ </fieldset>
247
265
  <div id="testdrive" class="testdrive" hidden>
248
266
  <div class="hd">▶ Test-drive this agent <span class="pill" id="tdPill">Claude Code</span></div>
249
267
  <div class="cmd"><span class="dollar">$</span> <code id="tdCmd"></code> <button id="tdCopy" class="ghost">Copy</button></div>
@@ -251,7 +269,7 @@
251
269
  </div>
252
270
  </section>
253
271
  <section class="pane right">
254
- <div class="bar"><strong style="flex:1">Gem (live)</strong><select id="target" title="materialize target" style="margin-left:auto"><option value="claude">Claude</option><option value="codex">Codex</option><option value="agents">Agents</option><option value="hermes">Hermes</option><option value="eve">Eve</option><option value="flue">Flue</option><option value="openai-sandbox">OpenAI Sandbox</option><option value="agentcore">AgentCore</option><option value="a2a">A2A</option></select><span class="seg" id="preview-modes"><button type="button" data-pmode="summary">Summary</button><button type="button" data-pmode="json">JSON</button><button type="button" data-pmode="materialize">Materialize</button><button type="button" data-pmode="managed">Managed Agents</button></span><span class="exportwrap"><button id="exportBtn">Export ▾</button><div id="exportMenu" class="exportmenu" hidden><button id="dltar" class="menuitem" title="download the gem archive as a .tar.gz">⬇ Download .tar.gz</button><button id="save" class="menuitem" title="write the gem archive (manifest + lock + files) to a folder">💾 Save to folder…</button><button id="dl" class="menuitem">⬇ Download JSON</button><button id="copy" class="menuitem">⧉ Copy JSON</button></div></span><span class="d" id="archiveStatus" style="margin-left:6px"></span></div>
272
+ <div class="bar"><strong style="flex:1">Gem (live)</strong><select id="target" title="materialize target" style="margin-left:auto"><option value="claude">Claude</option><option value="codex">Codex</option><option value="agents">Agents</option><option value="hermes">Hermes</option><option value="eve">Eve</option><option value="flue">Flue</option><option value="openai-sandbox">OpenAI Sandbox</option><option value="agentcore">AgentCore</option><option value="a2a">A2A</option></select><span class="seg" id="preview-modes"><button type="button" data-pmode="summary">Summary</button><button type="button" data-pmode="json">JSON</button><button type="button" data-pmode="materialize">Materialize</button><button type="button" data-pmode="run">Run</button><button type="button" data-pmode="managed">Managed Agents</button></span><span class="exportwrap"><button id="exportBtn">Export ▾</button><div id="exportMenu" class="exportmenu" hidden><button id="dlgem" class="menuitem" title="download one portable, self-verifying .gem file to share">🔗 Download .gem (share)</button><button id="dltar" class="menuitem" title="download the gem archive as a .tar.gz">⬇ Download .tar.gz</button><button id="save" class="menuitem" title="write the gem archive (manifest + lock + files) to a folder">💾 Save to folder…</button><button id="dl" class="menuitem">⬇ Download JSON</button><button id="copy" class="menuitem">⧉ Copy JSON</button></div></span><span class="d" id="archiveStatus" style="margin-left:6px"></span></div>
255
273
  <div id="preview"></div>
256
274
  <p class="note">MCP server &amp; hook config secrets are shown as <code>&lt;redacted&gt;</code> and never exported. Skill, CLAUDE.md, rules bodies &amp; hook commands are bundled as written — don't keep secrets in them.</p>
257
275
  <div id="checksPanel" class="group" style="margin-top:14px">
@@ -266,6 +284,23 @@
266
284
  <pre class="modal-body" id="modal-body"></pre>
267
285
  </div>
268
286
  </div>
287
+ <div id="getModal" class="modal-bg" hidden>
288
+ <div class="modal">
289
+ <div class="modal-h"><strong class="t">Get gems</strong><span class="src">install a shared .gem, or search the registry</span><button id="getClose" class="ghost" style="margin-left:auto">✕ Close</button></div>
290
+ <div style="padding:14px;display:flex;flex-direction:column;gap:14px">
291
+ <div>
292
+ <div class="bar"><strong style="flex:1;font-size:12px;text-transform:uppercase;letter-spacing:.05em;color:var(--muted)">Install a .gem</strong></div>
293
+ <div class="bar"><input id="installSrc" type="text" placeholder="https://…/foo.gem or /path/to/foo.gem" style="flex:1" /><select id="installTarget" title="materialize target"></select><button id="installBtn">Install</button></div>
294
+ <span class="d" id="installStatus"></span>
295
+ </div>
296
+ <div>
297
+ <div class="bar"><strong style="flex:1;font-size:12px;text-transform:uppercase;letter-spacing:.05em;color:var(--muted)">Search the registry</strong></div>
298
+ <div class="bar"><input id="regSearch" type="text" placeholder="search names, tags, descriptions…" style="flex:1" /><button id="regSearchBtn" class="ghost">Search</button></div>
299
+ <div id="regResults"></div>
300
+ </div>
301
+ </div>
302
+ </div>
303
+ </div>
269
304
  <div id="importModal" class="modal-bg" hidden>
270
305
  <div class="modal">
271
306
  <div class="modal-h"><strong class="t">Import from machine</strong><span class="src">~/.claude · ~/.agents · ~/.codex · ~/.hermes</span><button id="importClose" class="ghost" style="margin-left:auto">✕ Close</button></div>
@@ -489,8 +524,11 @@ function renderAnError(view, msg){
489
524
  // Read-only view of the candidate Gems. Each card adopts the project AND applies
490
525
  // just that candidate's selection.
491
526
  let anCandidates = []; // stash so a card's button can grab its candidate
527
+ let anDistilled = []; // stash distilled drafts for the accept buttons
528
+ let acceptedDrafts = []; // distilled drafts the user folded into the build
492
529
  function renderAnView(view, path, flavor, name, res){
493
530
  anCandidates = res.candidates || [];
531
+ anDistilled = res.distilled || [];
494
532
  const degraded = res.degraded ? ` <span class="d">(usage frequency — agent unavailable)</span>` : "";
495
533
  const gaps = (res.gaps || []).length ? `<div class="d" style="margin:8px 0">Used but not in inventory: ${esc(res.gaps.join(", "))}</div>` : "";
496
534
  const li = (n, t, reason, root) => {
@@ -509,6 +547,30 @@ function renderAnView(view, path, flavor, name, res){
509
547
  + `</div>`;
510
548
  }).join("");
511
549
 
550
+ // Distilled draft skills: recurring procedures the analyzer captured from the
551
+ // builtin work (Bash/Edit/test/commit) that aren't yet a skill. Accepting writes
552
+ // a reviewable SKILL.md draft — it is NOT installed.
553
+ const drafts = anDistilled.map((d, i) => {
554
+ const trig = (d.triggers || []).map(t => `<span class="src">${esc(t)}</span>`).join(" ");
555
+ const mut = d.mutating ? ` <span class="src" title="this workflow writes/executes">mutating</span>` : "";
556
+ return `<div style="border:1px dashed var(--line);border-radius:8px;padding:10px 12px;margin-bottom:10px">`
557
+ + `<div style="font-size:14px"><b>${esc(d.name)}</b> <span class="d">· distilled · ${d.confidence}</span>${mut}</div>`
558
+ + `<div class="d" style="margin:3px 0 6px">${esc(d.description)}</div>`
559
+ + (trig ? `<div style="margin-bottom:6px">${trig}</div>` : "")
560
+ + `<div class="d" style="margin-bottom:6px">tools: ${esc((d.tools || []).join(", ") || "—")} · seen in ${d.evidence?.sessions ?? "?"} session(s)</div>`
561
+ + `<details style="margin-bottom:8px"><summary class="d" style="cursor:pointer">view workflow body</summary>`
562
+ + `<pre style="white-space:pre-wrap;font-size:12px;background:var(--bg2,#0001);padding:8px;border-radius:6px;overflow:auto">${esc(d.body || "")}</pre></details>`
563
+ + `<button type="button" class="anFold" data-i="${i}" style="margin-top:2px">Add to build ▸</button>`
564
+ + ` <button type="button" class="ghost anAccept" data-i="${i}" style="margin-top:2px">write SKILL.md only</button>`
565
+ + ` <span class="anAcceptMsg d" data-i="${i}"></span>`
566
+ + `</div>`;
567
+ }).join("");
568
+ const draftsSection = anDistilled.length
569
+ ? `<div style="margin-top:14px;font-size:15px"><b>${anDistilled.length} distilled skill draft(s)</b> <span class="d">· captured from repeated workflows</span></div>`
570
+ + `<div class="d" style="margin:2px 0 10px">Reviewed drafts are written to <code>.agentgem/distilled/</code> — edit and promote them yourself; they are not installed.</div>`
571
+ + drafts
572
+ : "";
573
+
512
574
  const heading = anCandidates.length > 1 ? `${anCandidates.length} candidate Gems` : (anCandidates.length === 1 ? "Recommended Gem" : "No recurring flows found");
513
575
  const cached = res.cached ? ` <span class="src" title="cached — re-analyze to refresh">cached</span>` : "";
514
576
  view.innerHTML =
@@ -517,12 +579,57 @@ function renderAnView(view, path, flavor, name, res){
517
579
  + `<div style="margin-top:10px;font-size:15px"><b>${esc(heading)}</b>${degraded}${cached} <span class="d">· ${res.signalSummary.sessionsScanned} session(s)</span></div>`
518
580
  + `<div class="d" style="margin:4px 0 10px;word-break:break-all">${esc(path)}</div>`
519
581
  + (cards || `<div class="d">Nothing to recommend from this project's usage.</div>`)
520
- + gaps;
582
+ + gaps
583
+ + draftsSection;
521
584
  view.querySelector(".anBack").onclick = (e) => { e.preventDefault(); showPicker(); };
522
585
  view.querySelector(".anFresh").onclick = (e) => { e.preventDefault(); analyzeWorkflow(path, flavor, name, true); };
523
586
  view.querySelectorAll(".anApply").forEach(btn => {
524
587
  btn.onclick = (e) => { e.preventDefault(); switchAndApply(path, flavor, anCandidates[+btn.dataset.i]); };
525
588
  });
589
+ view.querySelectorAll(".anAccept").forEach(btn => {
590
+ btn.onclick = (e) => { e.preventDefault(); acceptDraft(view, +btn.dataset.i, btn); };
591
+ });
592
+ view.querySelectorAll(".anFold").forEach(btn => {
593
+ btn.onclick = (e) => { e.preventDefault(); addDraftToBuild(+btn.dataset.i); };
594
+ });
595
+ }
596
+
597
+ // Fold a distilled draft into the active build: adopt the analyzed project as the
598
+ // testbed, inject the draft as a project skill (so it renders + selects like any
599
+ // other), and stash it so buildSelectionBody ships it as `distilledDrafts` — the
600
+ // server stages it into the inventory before resolving the selection.
601
+ async function addDraftToBuild(i){
602
+ const skill = anDistilled[i];
603
+ const root = skill.evidence.root;
604
+ document.getElementById("recentModal").hidden = true; showPicker();
605
+ await useTestbed(root, "claude", skill.name); // adopts + loads + renders the project's inventory
606
+ let p = (inv.projects || []).find(x => x.root === root);
607
+ if (!p) { p = { root, name: root.replace(/^.*\//, ""), skills: [], mcpServers: [], instructions: [], hooks: [] }; (inv.projects = inv.projects || []).push(p); }
608
+ if (!p.skills.some(s => s.name === skill.name)) p.skills.push({ type: "skill", name: skill.name, description: skill.description, source: "distilled-draft", content: skill.body });
609
+ if (!acceptedDrafts.some(d => d.name === skill.name)) acceptedDrafts.push(skill);
610
+ projSel(root).skills.add(skill.name);
611
+ fetch("/api/workflow/draft", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(skill) }).catch(() => {}); // also persist a reviewable SKILL.md (best-effort)
612
+ renderInventoryPane(); // re-render so the injected draft shows as a checked skill
613
+ restoreChecks(); refresh();
614
+ ensureAnalyzeBanner().innerHTML = `<b>${esc(skill.name)}</b> <span class="d">(distilled draft)</span> added to the build — it's bundled into the Gem and a SKILL.md draft is written under <code>.agentgem/distilled/</code> on build.`;
615
+ }
616
+
617
+ // Accept a distilled draft: POST it; the server writes the SKILL.md draft and
618
+ // returns the path. The draft is NOT installed — the user promotes it by hand.
619
+ async function acceptDraft(view, i, btn){
620
+ const skill = anDistilled[i];
621
+ const msg = view.querySelector(`.anAcceptMsg[data-i="${i}"]`);
622
+ btn.disabled = true; if (msg) msg.textContent = "writing…";
623
+ try {
624
+ const res = await fetch("/api/workflow/draft", { method:"POST", headers:{"content-type":"application/json"}, body: JSON.stringify(skill) });
625
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
626
+ const { path } = await res.json();
627
+ btn.textContent = "✓ draft written";
628
+ if (msg) msg.innerHTML = ` → <code>${esc(path)}</code>`;
629
+ } catch (err) {
630
+ btn.disabled = false;
631
+ if (msg) msg.textContent = `failed: ${err.message}`;
632
+ }
526
633
  }
527
634
 
528
635
  // The explicit switch: adopt the project as the active testbed, then pre-check
@@ -637,6 +744,12 @@ async function load() {
637
744
  const qs = `?projects=${encodeURIComponent(JSON.stringify(projects))}`;
638
745
  inv = await (await fetch("/api/inventory" + qs)).json();
639
746
  inv.projects = inv.projects || [];
747
+ renderInventoryPane();
748
+ }
749
+
750
+ // Render the active testbed's project groups from the current `inv` (re-runnable so
751
+ // an injected distilled draft skill can appear without a re-fetch).
752
+ function renderInventoryPane(){
640
753
  // Render ONLY the active testbed's project groups (global groups are reached via Import).
641
754
  const proj = inv.projects.find(p => p.root === activeTestbed) || { root: activeTestbed, name: activeTestbed.replace(/^.*\//, ""), skills: [], mcpServers: [], instructions: [], hooks: [] };
642
755
  let html = group("Skills", proj.skills, "projectSkills", proj.root)
@@ -786,8 +899,14 @@ function buildSelectionBody(){
786
899
  const selection = { skills, mcpServers, includeInstructions: sel.includeInstructions };
787
900
  if (hooks.length) selection.hooks = hooks;
788
901
  if (Object.keys(projectsSel).length) selection.projects = projectsSel;
902
+ const channels = Array.from(document.querySelectorAll("#channelPicker .ch:checked")).map((el) => ({ platform: el.value }));
789
903
  const reqBody = { selection, name: document.getElementById("name").value || "gem" };
904
+ if (channels.length) reqBody.channels = channels;
790
905
  if (projects.length) reqBody.projects = projects;
906
+ // Ship the distilled drafts that are actually selected so the server can stage
907
+ // them into the inventory before resolving the selection (proposal §7b).
908
+ const drafts = acceptedDrafts.filter(d => (projectsSel[d.evidence.root]?.skills || []).includes(d.name));
909
+ if (drafts.length) reqBody.distilledDrafts = drafts;
791
910
  return reqBody;
792
911
  }
793
912
  async function build(){
@@ -815,6 +934,9 @@ document.getElementById("suggestChecks").addEventListener("click", async () => {
815
934
  renderChecks(); build();
816
935
  });
817
936
  let previewMode = "summary";
937
+ // "Run" preview mode: drive a locally-installed ACP coding agent against the live Gem.
938
+ let __gemRun = { task: "", agent: "claude", expect: "" };
939
+ let __gemRunES = null; // active EventSource for the streaming run (one at a time)
818
940
  function fmtSize(n){ return n < 1024 ? `${n} B` : `${(n / 1024).toFixed(n < 10240 ? 1 : 0)} KB`; }
819
941
  function artSize(a){ return (a.type === "mcp_server" || a.type === "hook") ? JSON.stringify(a.config || {}).length : (a.content || "").length; }
820
942
  const GEM_MARK_SVG = `<svg class="gemmark" viewBox="0 0 64 64" fill="none" aria-hidden="true"><path d="M32 4 58 24 32 60 6 24 Z" fill="#9a3324"/><path d="M32 4 58 24 32 26 Z" fill="#bb4a38"/><path d="M32 4 6 24 32 26 Z" fill="#7d2a1e"/><path d="M6 24 32 26 32 60 Z" fill="#8b2e21"/><path d="M58 24 32 26 32 60 Z" fill="#a8392a"/><path d="M6 24 58 24 32 26 Z" fill="#c8543f"/><path class="glint" d="M32 4 58 24 32 26 Z" fill="#ffd9a0"/></svg>`;
@@ -860,6 +982,7 @@ function renderPreview(){
860
982
  if (typeof renderRail === "function") renderRail();
861
983
  if (previewMode === "materialize") { renderMaterialize(); document.querySelectorAll("#preview-modes button").forEach(b => b.classList.toggle("on", b.dataset.pmode === previewMode)); return; }
862
984
  if (previewMode === "managed") { renderPublish(); document.querySelectorAll("#preview-modes button").forEach(b => b.classList.toggle("on", b.dataset.pmode === previewMode)); return; }
985
+ if (previewMode === "run") { renderRun(); document.querySelectorAll("#preview-modes button").forEach(b => b.classList.toggle("on", b.dataset.pmode === previewMode)); return; }
863
986
  const p = window.__gem;
864
987
  if (!p) { el.textContent = ""; }
865
988
  else if (p.error || previewMode === "json") {
@@ -888,6 +1011,81 @@ async function renderMaterialize(){
888
1011
  h += `<p class="note">Compatibility: ${esc(compat)}</p></div>`;
889
1012
  el.innerHTML = h;
890
1013
  }
1014
+ // "Run" preview mode: test-run the live Gem with a locally-installed ACP coding agent.
1015
+ // Materializes the selected Gem into a testbed, drives the agent against a task, and
1016
+ // (when "expect tools" is given) shows a verification verdict — the trust primitive.
1017
+ function renderRun(){
1018
+ const el = document.getElementById("preview");
1019
+ const r = window.__gemRunResult;
1020
+ let h = `<div class="psummary"><div class="phead"><strong>Run with a local agent</strong> <span class="d">· drives a locally-installed coding agent over ACP against this Gem</span></div>`;
1021
+ h += `<div class="pgroup"><h3>Task</h3><textarea id="gemRunTask" rows="3" placeholder="Describe what the agent should do with this Gem…" style="width:100%;box-sizing:border-box">${esc(__gemRun.task)}</textarea></div>`;
1022
+ h += `<div class="pgroup" style="display:flex;gap:12px;align-items:flex-end;flex-wrap:wrap">`
1023
+ + `<label>Agent <select id="gemRunAgent"><option value="claude"${__gemRun.agent === "claude" ? " selected" : ""}>Claude</option><option value="codex"${__gemRun.agent === "codex" ? " selected" : ""}>Codex (unvalidated)</option></select></label>`
1024
+ + `<label style="flex:1;min-width:160px">Expect tools <span class="d">(optional, comma-separated)</span><input id="gemRunExpect" placeholder="e.g. Write, qa" value="${esc(__gemRun.expect)}" style="width:100%;box-sizing:border-box"></label>`
1025
+ + `<button id="gemRunBtn">Run ▶</button>`
1026
+ + `<span class="d" id="gemRunStatus"></span></div>`;
1027
+ h += `<div id="gemRunOut" class="pgroup">${r ? runResultHtml(r) : '<p class="d">No run yet — select artifacts on the left, enter a task, and Run.</p>'}</div></div>`;
1028
+ el.innerHTML = h;
1029
+ document.getElementById("gemRunBtn").onclick = runGem;
1030
+ }
1031
+ function runResultHtml(res){
1032
+ if (!res || res.error) return `<pre class="json">${esc(JSON.stringify(res, null, 2))}</pre>`;
1033
+ const run = res.run || {}, rr = run.result || {}, v = res.verification;
1034
+ let h = "";
1035
+ if (v){
1036
+ h += `<div class="prow"><span style="display:inline-block;padding:2px 9px;border-radius:10px;font-weight:600;color:#fff;background:${v.passed ? "#1f7a33" : "#a3301f"}">${v.passed ? "✓ Verified" : "✗ Not verified"}</span></div>`;
1037
+ h += (v.checks || []).map(c => `<div class="prow"><span class="pn">${c.passed ? "✓" : "✗"} ${esc(c.name)}</span> <span class="pm">${esc(c.detail)}</span></div>`).join("");
1038
+ }
1039
+ if (run.ok === false) h += `<div class="prow"><span class="pm">${esc(run.error || "run failed")}</span></div>`;
1040
+ if ((rr.toolCalls || []).length) h += `<h3>Tools</h3><div>` + rr.toolCalls.map(t => `<span style="display:inline-block;padding:2px 8px;margin:2px;border-radius:6px;background:rgba(33,28,21,.08);font-size:12px">${esc(t.title)}${t.status ? " · " + esc(t.status) : ""}</span>`).join("") + `</div>`;
1041
+ if (rr.text) h += `<h3>Agent</h3><div style="white-space:pre-wrap;background:rgba(33,28,21,.04);padding:8px;border-radius:6px">${esc(rr.text)}</div>`;
1042
+ h += `<p class="note">Ran <strong>${esc(res.agent || "")}</strong> · materialized ${(res.materialized && res.materialized.written || []).length} artifact(s) into <code>${esc(res.dir || "")}</code></p>`;
1043
+ return h;
1044
+ }
1045
+ // Two-step streaming flow: POST /prepare materializes the Gem (carries the full
1046
+ // selection) and returns an opaque runId; then EventSource streams the agent run
1047
+ // (tool calls + token deltas) live, mirroring the workflow-analyze stream.
1048
+ async function runGem(){
1049
+ const taskEl = document.getElementById("gemRunTask");
1050
+ __gemRun.task = taskEl.value.trim();
1051
+ __gemRun.agent = document.getElementById("gemRunAgent").value;
1052
+ __gemRun.expect = document.getElementById("gemRunExpect").value;
1053
+ if (!__gemRun.task){ taskEl.focus(); return; }
1054
+ const btn = document.getElementById("gemRunBtn"), status = document.getElementById("gemRunStatus"), out = document.getElementById("gemRunOut");
1055
+ if (__gemRunES){ __gemRunES.close(); __gemRunES = null; }
1056
+ btn.disabled = true;
1057
+ const t0 = Date.now();
1058
+ const timer = setInterval(() => { status.textContent = "Running… " + ((Date.now() - t0) / 1000).toFixed(0) + "s"; }, 250);
1059
+ const finish = (label) => { clearInterval(timer); status.textContent = label; btn.disabled = false; };
1060
+ // Live result, accumulated as events stream in.
1061
+ const live = { agent: __gemRun.agent, run: { result: { text: "", toolCalls: [] } } };
1062
+ window.__gemRunResult = live;
1063
+ try {
1064
+ status.textContent = "Materializing…";
1065
+ const prep = await (await fetch("/api/gem/run/prepare", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ ...buildSelectionBody(), agent: __gemRun.agent }) })).json();
1066
+ if (prep.error || !prep.runId){ window.__gemRunResult = prep; out.innerHTML = runResultHtml(prep); finish("Failed"); return; }
1067
+ live.dir = prep.runDir; live.materialized = prep.materialized;
1068
+ out.innerHTML = runResultHtml(live);
1069
+ status.textContent = "Running…";
1070
+ const tools = __gemRun.expect.split(",").map(s => s.trim()).filter(Boolean);
1071
+ const qs = new URLSearchParams({ runId: prep.runId, task: __gemRun.task });
1072
+ if (tools.length) qs.set("expectTools", tools.join(","));
1073
+ const es = new EventSource(`/api/gem/run/stream?${qs}`); __gemRunES = es;
1074
+ es.addEventListener("tool", e => { live.run.result.toolCalls.push(JSON.parse(e.data)); out.innerHTML = runResultHtml(live); });
1075
+ es.addEventListener("delta", e => { live.run.result.text += JSON.parse(e.data).text; out.innerHTML = runResultHtml(live); });
1076
+ es.addEventListener("done", e => {
1077
+ const d = JSON.parse(e.data);
1078
+ window.__gemRunResult = { ...live, run: d.run, verification: d.verification, agent: d.agent };
1079
+ out.innerHTML = runResultHtml(window.__gemRunResult);
1080
+ es.close(); __gemRunES = null;
1081
+ finish(d.run && d.run.ok ? "Done in " + ((Date.now() - t0) / 1000).toFixed(1) + "s" : "Failed");
1082
+ });
1083
+ es.addEventListener("failed", e => { out.innerHTML = `<p class="pm">${esc(JSON.parse(e.data).message || "failed")}</p>`; es.close(); __gemRunES = null; finish("Failed"); });
1084
+ es.onerror = () => { es.close(); __gemRunES = null; finish("Connection lost"); };
1085
+ } catch (e) {
1086
+ out.innerHTML = `<p class="pm">${esc(String(e))}</p>`; finish("Error");
1087
+ }
1088
+ }
891
1089
  // Managed Agents publish: offline preview of the agent payload + a gated Publish action.
892
1090
  let __publishReady = null, __publishRequestId = null, __publishFingerprint = null, __deployTargets = null;
893
1091
  // The default deploy backend for each materialize target/harness. Selecting a harness in the
@@ -1217,6 +1415,7 @@ document.getElementById("none").addEventListener("click", () => {
1217
1415
  });
1218
1416
  });
1219
1417
  document.getElementById("name").addEventListener("input", () => { nameEdited = true; refresh(); });
1418
+ document.getElementById("channelPicker").addEventListener("change", () => refresh());
1220
1419
  document.getElementById("dl").addEventListener("click", () => {
1221
1420
  const blob = new Blob([JSON.stringify(window.__gem || {}, null, 2)], { type: "application/json" });
1222
1421
  const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = (document.getElementById("name").value || "gem") + ".json"; a.click();
@@ -1233,6 +1432,17 @@ document.getElementById("save").addEventListener("click", async () => {
1233
1432
  const n = Object.keys(r.files || {}).length, sk = (r.skipped || []).length;
1234
1433
  status.textContent = `Saved ${n} files to ${r.path}` + (sk ? ` · ${sk} skipped` : "");
1235
1434
  });
1435
+ // Download one portable, self-verifying .gem (gzipped tar) to hand to someone else.
1436
+ document.getElementById("dlgem").addEventListener("click", async () => {
1437
+ const status = document.getElementById("archiveStatus");
1438
+ status.textContent = "Building .gem…";
1439
+ const r = await (await fetch("/api/archive", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ ...buildSelectionBody(), tar: true }) })).json();
1440
+ if (!r.tarGz) { status.textContent = "No archive — select artifacts on the left."; return; }
1441
+ const bytes = Uint8Array.from(atob(r.tarGz), c => c.charCodeAt(0));
1442
+ const blob = new Blob([bytes], { type: "application/gzip" });
1443
+ const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = (document.getElementById("name").value || "gem") + ".gem"; a.click();
1444
+ status.textContent = `Downloaded ${a.download} — share it; install it with “Get gems”.`;
1445
+ });
1236
1446
  // Download the gem archive as a single .tar.gz (the transport/shipping form).
1237
1447
  document.getElementById("dltar").addEventListener("click", async () => {
1238
1448
  const status = document.getElementById("archiveStatus");
@@ -1244,6 +1454,52 @@ document.getElementById("dltar").addEventListener("click", async () => {
1244
1454
  const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = (document.getElementById("name").value || "gem") + ".gem.tar.gz"; a.click();
1245
1455
  status.textContent = `Downloaded ${a.download}`;
1246
1456
  });
1457
+ // Get gems: install a shared .gem by URL/path, or search the configured registry.
1458
+ (function(){
1459
+ const modal = document.getElementById("getModal");
1460
+ const open = () => {
1461
+ document.getElementById("installTarget").innerHTML = document.getElementById("target").innerHTML;
1462
+ modal.hidden = false;
1463
+ };
1464
+ document.getElementById("getBtn").addEventListener("click", open);
1465
+ document.getElementById("getClose").addEventListener("click", () => { modal.hidden = true; });
1466
+ modal.addEventListener("click", e => { if (e.target === modal) modal.hidden = true; });
1467
+
1468
+ document.getElementById("installBtn").addEventListener("click", async () => {
1469
+ const src = document.getElementById("installSrc").value.trim();
1470
+ const status = document.getElementById("installStatus");
1471
+ if (!src) { status.textContent = "Enter a .gem URL or local path."; return; }
1472
+ const body = { target: document.getElementById("installTarget").value };
1473
+ if (/^https?:\/\//i.test(src)) body.gemUrl = src; else body.gemPath = src;
1474
+ status.textContent = "Installing…";
1475
+ try {
1476
+ const m = await (await fetch("/api/materialize", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) })).json();
1477
+ if (m.error) { status.textContent = "Failed: " + m.error; return; }
1478
+ window.__materialize = m;
1479
+ status.textContent = `Installed — ${Object.keys(m.files || {}).length} files for ${m.target}. Switch the preview to Materialize to inspect.`;
1480
+ } catch (e) { status.textContent = "Failed: " + (e && e.message || e); }
1481
+ });
1482
+
1483
+ async function runSearch(){
1484
+ const el = document.getElementById("regResults");
1485
+ const q = document.getElementById("regSearch").value.trim();
1486
+ el.innerHTML = '<p class="note">Searching…</p>';
1487
+ try {
1488
+ const ready = await (await fetch("/api/registry/ready")).json();
1489
+ if (!ready.ready) { el.innerHTML = '<p class="note">No registry configured (set <code>AGENTGEM_REGISTRY_REPO</code>). You can still install a .gem by URL or path above.</p>'; return; }
1490
+ const r = await (await fetch("/api/registry/search?q=" + encodeURIComponent(q) + "&limit=25")).json();
1491
+ const hits = r.results || [];
1492
+ if (!hits.length) { el.innerHTML = '<p class="note">No matching gems.</p>'; return; }
1493
+ el.innerHTML = hits.map(h => {
1494
+ const kinds = (h.artifactKinds || []).join(", ");
1495
+ return `<button type="button" class="prow" data-ref="${esc(h.key)}"><span class="pn">${esc(h.key)}</span> <span class="pm">${esc(h.latest)}${kinds ? " · " + esc(kinds) : ""}</span></button>` +
1496
+ (h.description ? `<div class="note" style="margin:0 0 6px 8px">${esc(h.description)}</div>` : "");
1497
+ }).join("");
1498
+ } catch (e) { el.innerHTML = '<p class="note">Search failed: ' + esc(String(e && e.message || e)) + "</p>"; }
1499
+ }
1500
+ document.getElementById("regSearchBtn").addEventListener("click", runSearch);
1501
+ document.getElementById("regSearch").addEventListener("keydown", e => { if (e.key === "Enter") runSearch(); });
1502
+ })();
1247
1503
  // Export ▾ dropdown: toggle, close on outside click / Escape, and close after any item runs.
1248
1504
  (function(){
1249
1505
  const btn = document.getElementById("exportBtn"), menu = document.getElementById("exportMenu");
@@ -1403,7 +1659,8 @@ async function wsRender(target){
1403
1659
  tree.innerHTML = `<p class="d">Rendering ${esc(target)}…</p>`;
1404
1660
  let r;
1405
1661
  try {
1406
- const res = await fetch("/api/workspace/render", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ name: wsCurrent, target }) });
1662
+ const body = { name: wsCurrent, target, ...(target === "a2a" && a2aServerMode ? { a2aServer: true } : {}) };
1663
+ const res = await fetch("/api/workspace/render", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
1407
1664
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
1408
1665
  r = await res.json();
1409
1666
  } catch {