@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.
- package/README.md +3 -1
- package/dist/gem/acpRecommender.js +19 -63
- package/dist/gem/acpRun.js +156 -0
- package/dist/gem/acpSession.js +79 -0
- package/dist/gem/analysisCache.js +6 -2
- package/dist/gem/archive.js +17 -0
- package/dist/gem/binPath.js +9 -0
- package/dist/gem/buildGem.js +4 -1
- package/dist/gem/channels.js +29 -0
- package/dist/gem/credentials.js +3 -2
- package/dist/gem/distill.js +162 -0
- package/dist/gem/draftStage.js +77 -0
- package/dist/gem/gemVerify.js +35 -0
- package/dist/gem/inputError.js +21 -0
- package/dist/gem/registry.js +23 -4
- package/dist/gem/runGem.js +161 -0
- package/dist/gem/safeFetch.js +112 -0
- package/dist/gem/sandbox.js +37 -0
- package/dist/gem/sandboxLaunch.js +55 -0
- package/dist/gem/scrub.js +108 -0
- package/dist/gem/search.js +34 -0
- package/dist/gem/share.js +21 -0
- package/dist/gem/targets.js +85 -39
- package/dist/gem/workflowScan.js +0 -0
- package/dist/gem/workspaces.js +4 -3
- package/dist/gem.controller.js +121 -17
- package/dist/gem.tools.js +53 -5
- package/dist/gemRunStream.js +67 -0
- package/dist/index.js +12 -2
- package/dist/originGuard.js +36 -0
- package/dist/public/index.html +261 -4
- package/dist/schemas.js +149 -8
- package/dist/workflowStream.js +10 -4
- package/package.json +6 -2
package/dist/public/index.html
CHANGED
|
@@ -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 & hook config secrets are shown as <code><redacted></code> and never exported. Skill, CLAUDE.md, rules bodies & 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
|
|
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 {
|