@ninemind/agentgem 0.1.1 → 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 +26 -0
- package/dist/gem/acpRecommender.js +259 -0
- package/dist/gem/acpRun.js +156 -0
- package/dist/gem/acpSession.js +79 -0
- package/dist/gem/analysisCache.js +55 -0
- 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 +280 -16
- package/dist/gem/testbedFlavors.js +1 -0
- package/dist/gem/workflowScan.js +0 -0
- package/dist/gem/workspaces.js +4 -3
- package/dist/gem.controller.js +151 -16
- package/dist/gem.tools.js +53 -5
- package/dist/gemRunStream.js +67 -0
- package/dist/index.js +15 -0
- package/dist/originGuard.js +36 -0
- package/dist/public/index.html +444 -10
- package/dist/schemas.js +180 -7
- package/dist/workflowStream.js +78 -0
- package/package.json +7 -2
package/dist/public/index.html
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8" /><meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
5
|
+
<!-- Same-origin by default. Inline script/style are required by this single-file UI;
|
|
6
|
+
no eval is used, so 'unsafe-eval' is omitted (this clears Electron's insecure-CSP
|
|
7
|
+
warning). Google Fonts (stylesheet + font files) and the data: SVG favicon are
|
|
8
|
+
allowlisted; API calls are same-origin. -->
|
|
9
|
+
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data:; connect-src 'self'; base-uri 'self'; object-src 'none'" />
|
|
5
10
|
<title>agentgem — Gem Builder</title>
|
|
6
11
|
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 40' fill='none'><path d='M20 2 L36 14 L20 38 L4 14 Z' fill='%239a3324'/><path d='M20 2 L36 14 L20 16 Z' fill='%23bb4a38'/><path d='M20 2 L4 14 L20 16 Z' fill='%237d2a1e'/><path d='M4 14 L20 16 L20 38 Z' fill='%238b2e21'/><path d='M36 14 L20 16 L20 38 Z' fill='%23a8392a'/><path d='M4 14 L36 14 L20 16 Z' fill='%23c8543f'/></svg>" />
|
|
7
12
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
@@ -152,6 +157,13 @@
|
|
|
152
157
|
.prow .pm{margin-left:auto;color:var(--muted);font-size:12px;white-space:nowrap;font-family:var(--mono)}
|
|
153
158
|
.redtag{color:var(--accent);font-size:11px}
|
|
154
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}
|
|
155
167
|
/* ── filters ── */
|
|
156
168
|
#filterPanel{border:1px solid var(--line);border-radius:var(--r);padding:11px 13px;margin-bottom:12px;background:var(--accent-soft)}
|
|
157
169
|
#filterPanel[hidden]{display:none}
|
|
@@ -202,7 +214,7 @@
|
|
|
202
214
|
</nav>
|
|
203
215
|
<main>
|
|
204
216
|
<section class="pane left">
|
|
205
|
-
<div class="bar" id="nameBar" style="display:none"><input id="name" type="text" placeholder="gem name" value="gem" style="flex:1" /></div>
|
|
217
|
+
<div class="bar" id="nameBar" style="display:none"><label for="name" class="d" style="white-space:nowrap">Gem name</label><input id="name" type="text" placeholder="gem name" value="gem" style="flex:1" /></div>
|
|
206
218
|
<div class="bar" id="wsBar" style="display:none">
|
|
207
219
|
<select id="wsSelect" title="open a saved workspace" style="flex:1"><option value="">— no workspace —</option></select>
|
|
208
220
|
<button id="wsNew" class="ghost" title="save the current selection as a new workspace">New workspace…</button>
|
|
@@ -237,8 +249,19 @@
|
|
|
237
249
|
<div class="bar" id="typeBar"></div>
|
|
238
250
|
</div>
|
|
239
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>
|
|
240
|
-
<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>
|
|
241
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>
|
|
242
265
|
<div id="testdrive" class="testdrive" hidden>
|
|
243
266
|
<div class="hd">▶ Test-drive this agent <span class="pill" id="tdPill">Claude Code</span></div>
|
|
244
267
|
<div class="cmd"><span class="dollar">$</span> <code id="tdCmd"></code> <button id="tdCopy" class="ghost">Copy</button></div>
|
|
@@ -246,7 +269,7 @@
|
|
|
246
269
|
</div>
|
|
247
270
|
</section>
|
|
248
271
|
<section class="pane right">
|
|
249
|
-
<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></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>
|
|
250
273
|
<div id="preview"></div>
|
|
251
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>
|
|
252
275
|
<div id="checksPanel" class="group" style="margin-top:14px">
|
|
@@ -261,6 +284,23 @@
|
|
|
261
284
|
<pre class="modal-body" id="modal-body"></pre>
|
|
262
285
|
</div>
|
|
263
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>
|
|
264
304
|
<div id="importModal" class="modal-bg" hidden>
|
|
265
305
|
<div class="modal">
|
|
266
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>
|
|
@@ -275,6 +315,8 @@
|
|
|
275
315
|
<div class="modal">
|
|
276
316
|
<div class="modal-h"><strong class="t">Open a testbed</strong><button id="recentClose" class="ghost" style="margin-left:auto">✕ Close</button></div>
|
|
277
317
|
<div class="modal-body" style="padding:14px">
|
|
318
|
+
<div id="anView" hidden></div>
|
|
319
|
+
<div id="anPicker">
|
|
278
320
|
<div id="tbCwd" hidden style="padding:12px;border:1px solid var(--line);border-radius:8px;margin-bottom:14px">
|
|
279
321
|
<div style="margin-bottom:6px">This folder looks like a <span id="tbCwdFlavor"></span> project</div>
|
|
280
322
|
<div class="d" id="tbCwdPath" style="margin-bottom:8px;word-break:break-all"></div>
|
|
@@ -289,8 +331,9 @@
|
|
|
289
331
|
<div id="recentList">Loading…</div>
|
|
290
332
|
<div class="src" style="margin:14px 0 6px">Discovered <span class="d">(projects from your Claude / Codex history)</span></div>
|
|
291
333
|
<div id="discoveredList">Loading…</div>
|
|
334
|
+
</div>
|
|
292
335
|
</div>
|
|
293
|
-
<div class="modal-h" style="border-top:1px solid var(--line);border-bottom:0">
|
|
336
|
+
<div class="modal-h" id="anFooter" style="border-top:1px solid var(--line);border-bottom:0">
|
|
294
337
|
<span class="d">Not this folder? Pick a recent one above, or browse.</span>
|
|
295
338
|
<button id="recentBrowse" style="margin-left:auto">Browse folder…</button>
|
|
296
339
|
</div>
|
|
@@ -328,6 +371,7 @@ let candidate = null; // { path, flavor } currently shown in the top block
|
|
|
328
371
|
|
|
329
372
|
async function openOrCreateTestbed(){
|
|
330
373
|
document.getElementById("recentModal").hidden = false;
|
|
374
|
+
showPicker(); // always open on the project list, not a stale recommendation view
|
|
331
375
|
try {
|
|
332
376
|
const s = await (await fetch("/api/testbed/suggestion")).json();
|
|
333
377
|
if (s.looksLikeProject) renderCandidate(s.cwd, s.flavor, s.name);
|
|
@@ -381,11 +425,15 @@ async function renderRecents(){
|
|
|
381
425
|
const fl = esc((FLAVORS[p.flavor]||FLAVORS.claude).label);
|
|
382
426
|
const when = p.lastUsed ? esc(p.lastUsed.slice(0,10)) : "";
|
|
383
427
|
const stale = p.exists ? "" : ` <span class="d" title="path no longer exists">· missing</span>`;
|
|
384
|
-
|
|
428
|
+
const an = p.exists ? `<button type="button" class="ghost anBtn" data-i="${i}" title="recommend a Gem from this project's session history">Analyze</button>` : "";
|
|
429
|
+
return `<label class="row recent" data-i="${i}"${p.exists?"":' style="opacity:.5"'}><span><b>${short}</b> <span class="src">${fl}</span> <span class="d">${esc(p.path)}</span>${stale}</span><span class="d" style="margin-left:auto">${when}</span>${an}</label>`;
|
|
385
430
|
}).join("");
|
|
386
431
|
list.querySelectorAll("label.recent").forEach(row=>{
|
|
387
432
|
row.onclick = ()=>{ const p = recents[+row.dataset.i]; if(!p.exists) return; document.getElementById("recentModal").hidden = true; useTestbed(p.path, p.flavor, p.name); };
|
|
388
433
|
});
|
|
434
|
+
list.querySelectorAll("button.anBtn").forEach(btn=>{
|
|
435
|
+
btn.onclick = (e)=>{ e.preventDefault(); e.stopPropagation(); const p = recents[+btn.dataset.i]; analyzeWorkflow(p.path, p.flavor, p.name); };
|
|
436
|
+
});
|
|
389
437
|
}
|
|
390
438
|
|
|
391
439
|
// Cross-repo projects from Claude/Codex session history, minus anything already in Recent.
|
|
@@ -404,11 +452,15 @@ async function renderDiscovered(){
|
|
|
404
452
|
const fl = esc((FLAVORS[p.flavor]||FLAVORS.claude).label);
|
|
405
453
|
const when = p.lastUsed ? esc(p.lastUsed.slice(0,10)) : "";
|
|
406
454
|
const stale = p.exists ? "" : ` <span class="d" title="path no longer exists">· missing</span>`;
|
|
407
|
-
|
|
455
|
+
const an = p.exists ? `<button type="button" class="ghost anBtn" data-i="${i}" title="recommend a Gem from this project's session history">Analyze</button>` : "";
|
|
456
|
+
return `<label class="row disc" data-i="${i}"${p.exists?"":' style="opacity:.5"'}><span><b>${short}</b> <span class="src">${fl}</span> <span class="d">${esc(p.path)}</span>${stale}</span><span class="d" style="margin-left:auto">${when}</span>${an}</label>`;
|
|
408
457
|
}).join("");
|
|
409
458
|
list.querySelectorAll("label.disc").forEach(row=>{
|
|
410
459
|
row.onclick = ()=>{ const p = projects[+row.dataset.i]; if(!p.exists) return; document.getElementById("recentModal").hidden = true; useTestbed(p.path, p.flavor, p.path.replace(/^.*\//, "")); };
|
|
411
460
|
});
|
|
461
|
+
list.querySelectorAll("button.anBtn").forEach(btn=>{
|
|
462
|
+
btn.onclick = (e)=>{ e.preventDefault(); e.stopPropagation(); const p = projects[+btn.dataset.i]; analyzeWorkflow(p.path, p.flavor, p.path.replace(/^.*\//, "")); };
|
|
463
|
+
});
|
|
412
464
|
}
|
|
413
465
|
|
|
414
466
|
// Adopt a known project+flavor: scaffold is idempotent (writeIfAbsent) and records a recent.
|
|
@@ -419,12 +471,237 @@ async function useTestbed(path, flavor, name){
|
|
|
419
471
|
setTestbed(path);
|
|
420
472
|
nameEdited = false; // picking a project is explicit — default the gem name to it
|
|
421
473
|
document.getElementById("name").value = name;
|
|
422
|
-
load();
|
|
474
|
+
await load(); // awaited so callers (e.g. Analyze) can apply once the inventory is rendered
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Swap the picker for a focused recommendation view (same modal, no stacking).
|
|
478
|
+
function anModalTitle(t){ const el = document.querySelector("#recentModal .modal-h .t"); if (el) el.textContent = t; }
|
|
479
|
+
// NB: #anFooter has class `modal-h` whose CSS `display:flex` overrides the
|
|
480
|
+
// `hidden` attribute, so toggle its inline display directly.
|
|
481
|
+
function showAnView(){ document.getElementById("anPicker").hidden = true; document.getElementById("anFooter").style.display = "none"; document.getElementById("anView").hidden = false; anModalTitle("Workflow recommendation"); }
|
|
482
|
+
function showPicker(){ document.getElementById("anView").hidden = true; document.getElementById("anPicker").hidden = false; document.getElementById("anFooter").style.display = ""; anModalTitle("Open a testbed"); }
|
|
483
|
+
|
|
484
|
+
// Recommend a Gem for `path` from its session history — IN PLACE, without adopting
|
|
485
|
+
// it as the active testbed. Renders a focused view inside the same modal. One at a
|
|
486
|
+
// time: the focused view replaces the list, so analyses can't overlap from the UI.
|
|
487
|
+
function analyzeWorkflow(path, flavor, name, fresh){
|
|
488
|
+
const view = document.getElementById("anView");
|
|
489
|
+
let es = null, acc = "", finished = false;
|
|
490
|
+
view.innerHTML =
|
|
491
|
+
`<button type="button" class="ghost anBack">← back to projects</button>`
|
|
492
|
+
+ `<div style="margin-top:10px"><b>${esc(name)}</b></div>`
|
|
493
|
+
+ `<div class="d" id="anPhase" style="margin-top:8px">Starting…</div>`
|
|
494
|
+
+ `<pre id="anStream" style="margin-top:8px;max-height:140px;overflow:auto;white-space:pre-wrap;word-break:break-word;font-size:11px;color:var(--muted);background:rgba(127,127,127,.06);padding:8px;border-radius:6px;display:none"></pre>`;
|
|
495
|
+
view.querySelector(".anBack").onclick = (e) => { e.preventDefault(); finished = true; if (es) es.close(); showPicker(); };
|
|
496
|
+
showAnView();
|
|
497
|
+
const phaseEl = view.querySelector("#anPhase");
|
|
498
|
+
const streamEl = view.querySelector("#anStream");
|
|
499
|
+
|
|
500
|
+
es = new EventSource(`/api/workflow/analyze/stream?root=${encodeURIComponent(path)}${fresh ? "&fresh=1" : ""}`);
|
|
501
|
+
es.addEventListener("phase", (ev) => {
|
|
502
|
+
const d = JSON.parse(ev.data);
|
|
503
|
+
if (d.phase === "scanning") phaseEl.textContent = "Scanning session history…";
|
|
504
|
+
else if (d.phase === "scanned") phaseEl.textContent = `Scanned ${d.transcripts} transcript(s) · ${d.sessions} session(s). Asking Claude…`;
|
|
505
|
+
else if (d.phase === "thinking") phaseEl.textContent = "Claude is clustering a Gem from your usage…";
|
|
506
|
+
else if (d.phase === "validating") phaseEl.textContent = "Validating against your inventory…";
|
|
507
|
+
});
|
|
508
|
+
es.addEventListener("delta", (ev) => {
|
|
509
|
+
acc += JSON.parse(ev.data).text;
|
|
510
|
+
streamEl.style.display = "block";
|
|
511
|
+
streamEl.textContent = acc.slice(-1200); // tail of the agent's live output
|
|
512
|
+
streamEl.scrollTop = streamEl.scrollHeight;
|
|
513
|
+
});
|
|
514
|
+
es.addEventListener("done", (ev) => { finished = true; es.close(); renderAnView(view, path, flavor, name, JSON.parse(ev.data)); });
|
|
515
|
+
es.addEventListener("failed", (ev) => { finished = true; es.close(); renderAnError(view, esc(JSON.parse(ev.data).message || "unknown error")); });
|
|
516
|
+
es.onerror = () => { if (finished) return; finished = true; es.close(); renderAnError(view, "connection lost"); };
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function renderAnError(view, msg){
|
|
520
|
+
view.innerHTML = `<button type="button" class="ghost anBack">← back to projects</button><div class="d" style="margin-top:10px">Analysis failed: ${msg}</div>`;
|
|
521
|
+
view.querySelector(".anBack").onclick = (e) => { e.preventDefault(); showPicker(); };
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Read-only view of the candidate Gems. Each card adopts the project AND applies
|
|
525
|
+
// just that candidate's selection.
|
|
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
|
|
529
|
+
function renderAnView(view, path, flavor, name, res){
|
|
530
|
+
anCandidates = res.candidates || [];
|
|
531
|
+
anDistilled = res.distilled || [];
|
|
532
|
+
const degraded = res.degraded ? ` <span class="d">(usage frequency — agent unavailable)</span>` : "";
|
|
533
|
+
const gaps = (res.gaps || []).length ? `<div class="d" style="margin:8px 0">Used but not in inventory: ${esc(res.gaps.join(", "))}</div>` : "";
|
|
534
|
+
const li = (n, t, reason, root) => {
|
|
535
|
+
const scope = root === null ? ` <span class="src" title="global / plugin artifact">global</span>` : "";
|
|
536
|
+
return `<li style="margin-bottom:4px"><b>${esc(n)}</b> <span class="d">${esc(t)}</span>${scope}${reason ? ` — ${esc(reason)}` : ""}</li>`;
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
const cards = anCandidates.map((c, i) => {
|
|
540
|
+
const items = c.include.map(it => li(it.name, it.type, it.reason, it.root)).join("");
|
|
541
|
+
const instr = c.includeInstructions ? li("CLAUDE.md", "instructions", "loaded every session", c.root) : "";
|
|
542
|
+
return `<div style="border:1px solid var(--line);border-radius:8px;padding:10px 12px;margin-bottom:10px">`
|
|
543
|
+
+ `<div style="font-size:14px"><b>${esc(c.name)}</b> <span class="d">· ${c.confidence}</span></div>`
|
|
544
|
+
+ `<div class="d" style="margin:3px 0 8px">${esc(c.description)}</div>`
|
|
545
|
+
+ (items || instr ? `<ul style="margin:0 0 8px 18px;padding:0">${items}${instr}</ul>` : `<div class="d">No project-scoped artifacts.</div>`)
|
|
546
|
+
+ `<button type="button" class="anApply" data-i="${i}" style="margin-top:4px">Switch & apply this Gem ▸</button>`
|
|
547
|
+
+ `</div>`;
|
|
548
|
+
}).join("");
|
|
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
|
+
|
|
574
|
+
const heading = anCandidates.length > 1 ? `${anCandidates.length} candidate Gems` : (anCandidates.length === 1 ? "Recommended Gem" : "No recurring flows found");
|
|
575
|
+
const cached = res.cached ? ` <span class="src" title="cached — re-analyze to refresh">cached</span>` : "";
|
|
576
|
+
view.innerHTML =
|
|
577
|
+
`<div style="display:flex;align-items:center;gap:8px"><button type="button" class="ghost anBack">← back to projects</button>`
|
|
578
|
+
+ `<button type="button" class="ghost anFresh" style="margin-left:auto" title="re-run the analysis ignoring the cache">↻ Re-analyze</button></div>`
|
|
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>`
|
|
580
|
+
+ `<div class="d" style="margin:4px 0 10px;word-break:break-all">${esc(path)}</div>`
|
|
581
|
+
+ (cards || `<div class="d">Nothing to recommend from this project's usage.</div>`)
|
|
582
|
+
+ gaps
|
|
583
|
+
+ draftsSection;
|
|
584
|
+
view.querySelector(".anBack").onclick = (e) => { e.preventDefault(); showPicker(); };
|
|
585
|
+
view.querySelector(".anFresh").onclick = (e) => { e.preventDefault(); analyzeWorkflow(path, flavor, name, true); };
|
|
586
|
+
view.querySelectorAll(".anApply").forEach(btn => {
|
|
587
|
+
btn.onclick = (e) => { e.preventDefault(); switchAndApply(path, flavor, anCandidates[+btn.dataset.i]); };
|
|
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
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// The explicit switch: adopt the project as the active testbed, then pre-check
|
|
636
|
+
// the chosen candidate's selection.
|
|
637
|
+
async function switchAndApply(path, flavor, candidate){
|
|
638
|
+
document.getElementById("recentModal").hidden = true;
|
|
639
|
+
showPicker(); // reset modal for next open
|
|
640
|
+
await useTestbed(path, flavor, candidate.name); // adopt + render this project's inventory
|
|
641
|
+
applyCandidate(path, candidate); // pre-check this candidate's boxes + banner
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function ensureAnalyzeBanner(){
|
|
645
|
+
let b = document.getElementById("analyzeBanner");
|
|
646
|
+
if (!b) { b = document.createElement("div"); b.id = "analyzeBanner"; b.className = "note"; b.style.margin = "0 0 12px";
|
|
647
|
+
const inv = document.getElementById("inventory"); inv.insertBefore(b, inv.firstChild); }
|
|
648
|
+
return b;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Mutate the shared `sel` model (same shape onToggle maintains) from one
|
|
652
|
+
// candidate's selection — global artifacts go top-level, project ones under
|
|
653
|
+
// projects[root] — then re-sync the checkboxes and the build preview.
|
|
654
|
+
function applyCandidate(root, candidate){
|
|
655
|
+
const s = candidate.selection;
|
|
656
|
+
// global (top-level) artifacts
|
|
657
|
+
(s.skills || []).forEach(n => sel.skills.add(n));
|
|
658
|
+
(s.mcpServers || []).forEach(n => sel.mcpServers.add(n));
|
|
659
|
+
(s.hooks || []).forEach(n => sel.hooks.add(n));
|
|
660
|
+
if (s.includeInstructions) sel.includeInstructions = true;
|
|
661
|
+
// project artifacts
|
|
662
|
+
const ps = (s.projects || {})[root] || {};
|
|
663
|
+
const ts = projSel(root);
|
|
664
|
+
(ps.skills || []).forEach(n => ts.skills.add(n));
|
|
665
|
+
(ps.mcpServers || []).forEach(n => ts.mcpServers.add(n));
|
|
666
|
+
(ps.hooks || []).forEach(n => ts.hooks.add(n));
|
|
667
|
+
if (ps.includeInstructions) ts.includeInstructions = true;
|
|
668
|
+
|
|
669
|
+
renderGlobalGroup(candidate); // surface the global picks as checkboxes
|
|
670
|
+
restoreChecks(); refresh();
|
|
671
|
+
|
|
672
|
+
const reasons = candidate.include.map(i => `${esc(i.name)}: ${esc(i.reason)}`).join(" · ");
|
|
673
|
+
ensureAnalyzeBanner().innerHTML = `<b>${esc(candidate.name)}</b> — ${esc(candidate.description)}<br>${reasons}`;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// The testbed inventory pane normally shows only project artifacts (globals are
|
|
677
|
+
// reached via Import). When a candidate bundles globals, render them as a
|
|
678
|
+
// dedicated checkbox group so they're visible and reviewable. Their data-kind is
|
|
679
|
+
// the top-level kind, so onToggle/restoreChecks/buildSelectionBody treat them as
|
|
680
|
+
// the global selection.
|
|
681
|
+
function renderGlobalGroup(candidate){
|
|
682
|
+
document.getElementById("globalGroup")?.remove();
|
|
683
|
+
const globals = (candidate.include || []).filter(i => i.root === null);
|
|
684
|
+
if (!globals.length) return;
|
|
685
|
+
const kindOf = t => t === "skill" ? "skills" : t === "mcp_server" ? "mcpServers" : "hooks";
|
|
686
|
+
const rows = globals.map(i =>
|
|
687
|
+
`<label class="row"><input type="checkbox" data-kind="${kindOf(i.type)}" data-name="${esc(i.name)}"> <span>${esc(i.name)} <span class="src">global</span> <span class="d">— ${esc(i.type)}</span></span></label>`).join("");
|
|
688
|
+
document.getElementById("inventory").insertAdjacentHTML("afterbegin",
|
|
689
|
+
`<div class="group" id="globalGroup"><h2>Global / plugin (recommended)</h2>${rows}</div>`);
|
|
690
|
+
document.querySelectorAll("#globalGroup input[type=checkbox]").forEach(cb => cb.addEventListener("change", onToggle));
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// Prefer the Electron-native folder dialog when running in the desktop app;
|
|
694
|
+
// fall back to the local REST picker in a plain browser. Both return { path }.
|
|
695
|
+
async function pickFolderPath() {
|
|
696
|
+
if (window.agentgem && window.agentgem.pickFolder) {
|
|
697
|
+
return await window.agentgem.pickFolder();
|
|
698
|
+
}
|
|
699
|
+
return await (await fetch("/api/pick-folder")).json();
|
|
423
700
|
}
|
|
424
701
|
|
|
425
702
|
// Browse routes the picked folder back through the same confirm block (no prompts).
|
|
426
703
|
async function browseForTestbed(){
|
|
427
|
-
const pick = await (
|
|
704
|
+
const pick = await pickFolderPath();
|
|
428
705
|
if(!pick.path) return;
|
|
429
706
|
const flavor = (await (await fetch(`/api/testbed/detect?root=${encodeURIComponent(pick.path)}`)).json()).flavor;
|
|
430
707
|
renderCandidate(pick.path, flavor, pick.path.replace(/^.*\//, ""));
|
|
@@ -467,6 +744,12 @@ async function load() {
|
|
|
467
744
|
const qs = `?projects=${encodeURIComponent(JSON.stringify(projects))}`;
|
|
468
745
|
inv = await (await fetch("/api/inventory" + qs)).json();
|
|
469
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(){
|
|
470
753
|
// Render ONLY the active testbed's project groups (global groups are reached via Import).
|
|
471
754
|
const proj = inv.projects.find(p => p.root === activeTestbed) || { root: activeTestbed, name: activeTestbed.replace(/^.*\//, ""), skills: [], mcpServers: [], instructions: [], hooks: [] };
|
|
472
755
|
let html = group("Skills", proj.skills, "projectSkills", proj.root)
|
|
@@ -616,8 +899,14 @@ function buildSelectionBody(){
|
|
|
616
899
|
const selection = { skills, mcpServers, includeInstructions: sel.includeInstructions };
|
|
617
900
|
if (hooks.length) selection.hooks = hooks;
|
|
618
901
|
if (Object.keys(projectsSel).length) selection.projects = projectsSel;
|
|
902
|
+
const channels = Array.from(document.querySelectorAll("#channelPicker .ch:checked")).map((el) => ({ platform: el.value }));
|
|
619
903
|
const reqBody = { selection, name: document.getElementById("name").value || "gem" };
|
|
904
|
+
if (channels.length) reqBody.channels = channels;
|
|
620
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;
|
|
621
910
|
return reqBody;
|
|
622
911
|
}
|
|
623
912
|
async function build(){
|
|
@@ -645,6 +934,9 @@ document.getElementById("suggestChecks").addEventListener("click", async () => {
|
|
|
645
934
|
renderChecks(); build();
|
|
646
935
|
});
|
|
647
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)
|
|
648
940
|
function fmtSize(n){ return n < 1024 ? `${n} B` : `${(n / 1024).toFixed(n < 10240 ? 1 : 0)} KB`; }
|
|
649
941
|
function artSize(a){ return (a.type === "mcp_server" || a.type === "hook") ? JSON.stringify(a.config || {}).length : (a.content || "").length; }
|
|
650
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>`;
|
|
@@ -690,6 +982,7 @@ function renderPreview(){
|
|
|
690
982
|
if (typeof renderRail === "function") renderRail();
|
|
691
983
|
if (previewMode === "materialize") { renderMaterialize(); document.querySelectorAll("#preview-modes button").forEach(b => b.classList.toggle("on", b.dataset.pmode === previewMode)); return; }
|
|
692
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; }
|
|
693
986
|
const p = window.__gem;
|
|
694
987
|
if (!p) { el.textContent = ""; }
|
|
695
988
|
else if (p.error || previewMode === "json") {
|
|
@@ -700,21 +993,99 @@ function renderPreview(){
|
|
|
700
993
|
}
|
|
701
994
|
document.querySelectorAll("#preview-modes button").forEach(b => b.classList.toggle("on", b.dataset.pmode === previewMode));
|
|
702
995
|
}
|
|
996
|
+
let a2aServerMode = false; // a2a target: false = Agent Card only (default), true = also emit the runnable server
|
|
703
997
|
async function renderMaterialize(){
|
|
704
998
|
const el = document.getElementById("preview");
|
|
705
999
|
const reqBody = buildSelectionBody();
|
|
706
1000
|
reqBody.target = document.getElementById("target").value;
|
|
1001
|
+
if (reqBody.target === "a2a" && a2aServerMode) reqBody.a2aServer = true;
|
|
707
1002
|
const m = await (await fetch("/api/materialize", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(reqBody) })).json();
|
|
708
1003
|
window.__materialize = m;
|
|
709
1004
|
if (m.error) { el.innerHTML = ""; const pre = document.createElement("pre"); pre.className = "json"; pre.textContent = JSON.stringify(m, null, 2); el.appendChild(pre); return; }
|
|
710
1005
|
const paths = Object.keys(m.files || {});
|
|
711
1006
|
const compat = Object.entries(m.compatibility || {}).map(([t, c]) => `${t} ${c.skipped ? c.skipped + " skipped" : "✓"}`).join(" · ");
|
|
712
1007
|
let h = `<div class="psummary"><div class="phead"><strong>${esc(m.target)}</strong> <span class="d">· ${paths.length} file${paths.length === 1 ? "" : "s"}</span></div>`;
|
|
1008
|
+
if (reqBody.target === "a2a") h += `<div class="pgroup"><label style="display:flex;align-items:center;gap:6px"><input type="checkbox" id="a2aServerToggle" ${a2aServerMode ? "checked" : ""}> Emit runnable server (AI SDK v7) — otherwise just the Agent Card</label></div>`;
|
|
713
1009
|
h += `<div class="pgroup"><h3>Files</h3>` + (paths.length ? paths.map(p => `<button type="button" class="prow" data-mpath="${esc(p)}"><span class="pn">${esc(p)}</span></button>`).join("") : `<p class="d">No files — select artifacts on the left.</p>`) + `</div>`;
|
|
714
1010
|
if ((m.skipped || []).length) h += `<div class="pgroup"><h3>Skipped (${m.skipped.length})</h3>` + m.skipped.map(s => `<div class="prow"><span class="pn">${esc(s.artifact)}</span> <span class="pm">${esc(s.reason)}</span></div>`).join("") + `</div>`;
|
|
715
1011
|
h += `<p class="note">Compatibility: ${esc(compat)}</p></div>`;
|
|
716
1012
|
el.innerHTML = h;
|
|
717
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
|
+
}
|
|
718
1089
|
// Managed Agents publish: offline preview of the agent payload + a gated Publish action.
|
|
719
1090
|
let __publishReady = null, __publishRequestId = null, __publishFingerprint = null, __deployTargets = null;
|
|
720
1091
|
// The default deploy backend for each materialize target/harness. Selecting a harness in the
|
|
@@ -1018,6 +1389,10 @@ document.getElementById("target").addEventListener("change", (e) => {
|
|
|
1018
1389
|
if (mapped) deployBackend = mapped;
|
|
1019
1390
|
renderPreview();
|
|
1020
1391
|
});
|
|
1392
|
+
// a2a server toggle (rendered inside the materialize preview): re-materialize with/without the server.
|
|
1393
|
+
document.getElementById("preview").addEventListener("change", (e) => {
|
|
1394
|
+
if (e.target.id === "a2aServerToggle") { a2aServerMode = e.target.checked; renderMaterialize(); }
|
|
1395
|
+
});
|
|
1021
1396
|
document.getElementById("preview").addEventListener("click", e => {
|
|
1022
1397
|
if (e.target.id === "publishBtn") { doPublish(); return; }
|
|
1023
1398
|
const mp = e.target.closest("[data-mpath]");
|
|
@@ -1040,6 +1415,7 @@ document.getElementById("none").addEventListener("click", () => {
|
|
|
1040
1415
|
});
|
|
1041
1416
|
});
|
|
1042
1417
|
document.getElementById("name").addEventListener("input", () => { nameEdited = true; refresh(); });
|
|
1418
|
+
document.getElementById("channelPicker").addEventListener("change", () => refresh());
|
|
1043
1419
|
document.getElementById("dl").addEventListener("click", () => {
|
|
1044
1420
|
const blob = new Blob([JSON.stringify(window.__gem || {}, null, 2)], { type: "application/json" });
|
|
1045
1421
|
const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = (document.getElementById("name").value || "gem") + ".json"; a.click();
|
|
@@ -1049,13 +1425,24 @@ document.getElementById("copy").addEventListener("click", () => navigator.clipbo
|
|
|
1049
1425
|
document.getElementById("save").addEventListener("click", async () => {
|
|
1050
1426
|
const status = document.getElementById("archiveStatus");
|
|
1051
1427
|
status.textContent = "Choose a folder…";
|
|
1052
|
-
const picked = await (
|
|
1428
|
+
const picked = await pickFolderPath();
|
|
1053
1429
|
if (!picked.path) { status.textContent = ""; return; }
|
|
1054
1430
|
status.textContent = "Saving…";
|
|
1055
1431
|
const r = await (await fetch("/api/archive", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ ...buildSelectionBody(), outDir: picked.path }) })).json();
|
|
1056
1432
|
const n = Object.keys(r.files || {}).length, sk = (r.skipped || []).length;
|
|
1057
1433
|
status.textContent = `Saved ${n} files to ${r.path}` + (sk ? ` · ${sk} skipped` : "");
|
|
1058
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
|
+
});
|
|
1059
1446
|
// Download the gem archive as a single .tar.gz (the transport/shipping form).
|
|
1060
1447
|
document.getElementById("dltar").addEventListener("click", async () => {
|
|
1061
1448
|
const status = document.getElementById("archiveStatus");
|
|
@@ -1067,6 +1454,52 @@ document.getElementById("dltar").addEventListener("click", async () => {
|
|
|
1067
1454
|
const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = (document.getElementById("name").value || "gem") + ".gem.tar.gz"; a.click();
|
|
1068
1455
|
status.textContent = `Downloaded ${a.download}`;
|
|
1069
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
|
+
})();
|
|
1070
1503
|
// Export ▾ dropdown: toggle, close on outside click / Escape, and close after any item runs.
|
|
1071
1504
|
(function(){
|
|
1072
1505
|
const btn = document.getElementById("exportBtn"), menu = document.getElementById("exportMenu");
|
|
@@ -1226,7 +1659,8 @@ async function wsRender(target){
|
|
|
1226
1659
|
tree.innerHTML = `<p class="d">Rendering ${esc(target)}…</p>`;
|
|
1227
1660
|
let r;
|
|
1228
1661
|
try {
|
|
1229
|
-
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) });
|
|
1230
1664
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1231
1665
|
r = await res.json();
|
|
1232
1666
|
} catch {
|