rewritable 0.11.0 → 0.13.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rewritable",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "CLI for re-writeable: emit and import single-file rwa documents.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -2322,6 +2322,18 @@ async function renderActionsModePanel(panel) {
2322
2322
  const skillRows = skillAffs.length ? skillAffs.map(a =>
2323
2323
  '<div class="rwa-mode-row"><div><div class="rwa-mode-title">' + escRuntimeHtml(a.name) + '</div><div class="rwa-mode-meta">' + escRuntimeHtml(a.kind || 'skill') + ' · ' + escRuntimeHtml(a.skillId) + '</div></div><button type="button" data-action-skill="' + escRuntimeHtml(a.skillId) + '">Open in Skills</button></div>'
2324
2324
  ).join('') : '<div class="rwa-mode-empty">No installed skills.</div>';
2325
+ // Intelligences (rwa-agent/1 roles) — list + activate/deactivate. Activating offers the role's
2326
+ // recommended model (intelligence/0.2 I-A). A verified role can be activated; unverified cannot.
2327
+ const agentList = (typeof runtimeListAgents === 'function') ? runtimeListAgents() : [];
2328
+ const activeRole = (runtimeAgentActive() || {}).role || null;
2329
+ const agentRows = agentList.length ? agentList.map(a => {
2330
+ const isActive = a.role === activeRole;
2331
+ const meta = (a.verified ? 'verified' : 'unverified') + (isActive ? ' · active' : '');
2332
+ const btn = !a.verified ? '' : (isActive
2333
+ ? '<button type="button" data-agent-off="1">Deactivate</button>'
2334
+ : '<button type="button" data-agent-on="' + escRuntimeHtml(a.role) + '">Activate</button>');
2335
+ return '<div class="rwa-mode-row"><div><div class="rwa-mode-title">' + escRuntimeHtml(a.role) + '</div><div class="rwa-mode-meta">' + escRuntimeHtml('intelligence · ' + meta) + '</div></div>' + btn + '</div>';
2336
+ }).join('') : '<div class="rwa-mode-empty">No intelligences installed.</div>';
2325
2337
  panel.innerHTML = [
2326
2338
  '<div class="rwa-mode-section">',
2327
2339
  '<div class="rwa-mode-kicker">Activity</div>',
@@ -2336,6 +2348,7 @@ async function renderActionsModePanel(panel) {
2336
2348
  '<div class="rwa-mode-section"><div class="rwa-mode-kicker">Recent runs</div>' + histRows + '</div>',
2337
2349
  '<div class="rwa-mode-section"><div class="rwa-mode-kicker">Live affordances</div>' + affRows + '</div>',
2338
2350
  '<div class="rwa-mode-section"><div class="rwa-mode-kicker">Installed skill actions</div>' + skillRows + '</div>',
2351
+ '<div class="rwa-mode-section"><div class="rwa-mode-kicker">Intelligences</div>' + agentRows + '</div>',
2339
2352
  ].join('');
2340
2353
  const undoBtn = panel.querySelector('#rwa-actions-undo');
2341
2354
  if (undoBtn) undoBtn.addEventListener('click', () => runtimeUndo());
@@ -2352,6 +2365,13 @@ async function renderActionsModePanel(panel) {
2352
2365
  panel.querySelectorAll('[data-action-skill]').forEach(btn => {
2353
2366
  btn.addEventListener('click', () => runtimeSetMode('skills'));
2354
2367
  });
2368
+ panel.querySelectorAll('[data-agent-on]').forEach(btn => btn.addEventListener('click', () => {
2369
+ try { runtimeActivateAgent(btn.getAttribute('data-agent-on')); } // setActive + offer recommended model
2370
+ catch (e) { if (typeof setStatus === 'function') setStatus('err', '✗ ' + (e && (e.code || e.message))); }
2371
+ renderActionsModePanel(panel);
2372
+ }));
2373
+ const agentOff = panel.querySelector('[data-agent-off]');
2374
+ if (agentOff) agentOff.addEventListener('click', () => { runtimeSetActiveAgent(null); renderActionsModePanel(panel); });
2355
2375
  }
2356
2376
 
2357
2377
  // ─── Agent (rwa-edit/1) ─────────────────────────────────────────────
@@ -7991,13 +8011,157 @@ function showAgentInstallDialog(envelope) {
7991
8011
  const ib = card.querySelector('[data-act=install]'); if (ib) ib.onclick = async () => close(await runtimeInstallAgent(envelope));
7992
8012
  }));
7993
8013
  }
7994
- // §1.3 / §12 — install trigger: pick a .rwa-skill.json OR .rwa-agent.json, parse, dispatch by format.
8014
+ // §1.3 / §12 — install trigger: pick a .rwa-skill.json / .rwa-agent.json envelope OR an
8015
+ // intelligence carrier .html (a rewritable carrying a signed rwa-agent/1 record), then route.
7995
8016
  function runtimePromptInstall() {
7996
- const inp = document.createElement('input'); inp.type = 'file'; inp.accept = '.rwa-skill.json,.rwa-agent.json,application/json,.json';
7997
- inp.onchange = () => { const f = inp.files && inp.files[0]; if (!f) return; const rd = new FileReader(); rd.onload = () => { let env; try { env = JSON.parse(rd.result); } catch (_) { setStatus && setStatus('err', 'invalid skill/agent JSON'); return; } if (env && env.format === 'rwa-agent/1') showAgentInstallDialog(env); else showSkillInstallDialog(env); }; rd.readAsText(f); };
8017
+ const inp = document.createElement('input'); inp.type = 'file'; inp.accept = '.rwa-skill.json,.rwa-agent.json,application/json,.json,.html,.htm,text/html';
8018
+ inp.onchange = () => { const f = inp.files && inp.files[0]; if (!f) return; const rd = new FileReader(); rd.onload = () => { routeInstallFromText(String(rd.result || '')); }; rd.readAsText(f); };
7998
8019
  inp.click();
7999
8020
  }
8000
8021
 
8022
+ // ── intelligence/0.2 (docs/specs/rwa-intelligence-spec.md §5) — the file-drop bridge.
8023
+ // An "intelligence" ships as a CARRIER: a (skill-host) rewritable carrying a signed rwa-agent/1
8024
+ // record in its frozen #rwa-agents zone (the overlay half is the v0.9 agent role — see
8025
+ // runtime.agents / buildAgentZone above). Dropping a carrier onto a target extracts that record
8026
+ // and routes it to the existing consent dialog + runtime.agents.install. In a carrier's RAW bytes
8027
+ // the record lives inside INLINE_DOC, so its closing script tag is backslash-escaped (the zone
8028
+ // <div>/</div> delimiters are not). We extract + un-escape INLINE_DOC (the inverse of buildFile's
8029
+ // escapeTL — every escape is one backslash + one char), then parse the zone exactly as
8030
+ // readTrustworthyAgents does. The signature stays the trust anchor: the dialog re-verifies.
8031
+ function _carrierDoc(html) {
8032
+ const marker = 'const INLINE_DOC = `';
8033
+ const i = String(html || '').indexOf(marker);
8034
+ if (i < 0) return String(html || ''); // not a full container — treat the text itself as the doc
8035
+ let j = i + marker.length;
8036
+ for (; j < html.length; j++) { const c = html[j]; if (c === '\\') { j++; continue; } if (c === '`') break; }
8037
+ return html.slice(i + marker.length, j).replace(/\\([\s\S])/g, '$1');
8038
+ }
8039
+ function extractAgentEnvelopesFromCarrier(html) {
8040
+ const zone = _agExtractZone(_carrierDoc(html));
8041
+ if (!zone) return [];
8042
+ const out = [];
8043
+ for (const m of zone.matchAll(/<script\s+type="application\/rwa-agent\+json">([\s\S]*?)<\/script>/g)) {
8044
+ let env; try { env = JSON.parse(new TextDecoder().decode(_skFromB64(m[1].trim()))); } catch (_) { continue; }
8045
+ if (env && env.agent && typeof env.agent.role === 'string') out.push(env);
8046
+ }
8047
+ return out;
8048
+ }
8049
+ // Classify a dropped/picked file's text: a carrier .html, a bare envelope JSON, or nothing.
8050
+ function classifyInstallText(text) {
8051
+ const s = String(text || '');
8052
+ let obj = null; try { obj = JSON.parse(s); } catch (_) {}
8053
+ if (obj && typeof obj === 'object') {
8054
+ if (obj.agent || obj.format === 'rwa-agent/1') return { kind: 'json-agent', envelope: obj };
8055
+ if (obj.skill || obj.format === 'rwa-skill/1') return { kind: 'json-skill', envelope: obj };
8056
+ return { kind: 'none' };
8057
+ }
8058
+ const envelopes = extractAgentEnvelopesFromCarrier(s);
8059
+ if (envelopes.length) return { kind: 'agent-carrier', envelopes };
8060
+ return { kind: 'none' };
8061
+ }
8062
+ // Route extracted content to the right consent dialog. Install stays behind the dialog (the trust
8063
+ // anchor); the dialog is fire-and-forget so the drop handler returns immediately. Multiple records
8064
+ // in one carrier are queued (each dialog awaits the previous close).
8065
+ async function routeInstallFromText(text) {
8066
+ const c = classifyInstallText(text);
8067
+ if (c.kind === 'json-agent') showAgentInstallDialog(c.envelope);
8068
+ else if (c.kind === 'json-skill') showSkillInstallDialog(c.envelope);
8069
+ else if (c.kind === 'agent-carrier') { (async () => { for (const env of c.envelopes) { await showAgentInstallDialog(env); } })(); }
8070
+ else if (typeof setStatus === 'function') setStatus('err', 'no installable skill or intelligence found in that file');
8071
+ return c;
8072
+ }
8073
+ async function _readDroppedText(file) {
8074
+ if (file && typeof file.text === 'function') return file.text();
8075
+ return new Promise((res, rej) => { const rd = new FileReader(); rd.onload = () => res(String(rd.result || '')); rd.onerror = rej; rd.readAsText(file); });
8076
+ }
8077
+ // Drop gesture — capture phase, so a carrier is claimed before the Edit-mode image-mount drop.
8078
+ // Acts only on an .html/.htm file; any other drop flows through untouched to existing handlers.
8079
+ // A carrier is a self-contained .html (seed + doc, ~0.6 MB; larger with embedded images); cap the
8080
+ // read so a wildly oversized drop can't be slurped into memory (mirrors the image-ingest size cap).
8081
+ const CARRIER_MAX_BYTES = 32 * 1024 * 1024;
8082
+ async function handleCarrierDrop(e) {
8083
+ const files = Array.from((e && e.dataTransfer && e.dataTransfer.files) || []);
8084
+ const carrier = files.find(f => /text\/html/i.test(f.type || '') || /\.html?$/i.test(f.name || ''));
8085
+ if (!carrier) return; // not a carrier — let the image/other drop handlers run
8086
+ if (e.preventDefault) e.preventDefault();
8087
+ if (e.stopPropagation) e.stopPropagation();
8088
+ if (carrier.size > CARRIER_MAX_BYTES) { if (typeof setStatus === 'function') setStatus('err', 'that file is too large to be an intelligence carrier (' + Math.round(carrier.size / 1048576) + ' MB)'); return; }
8089
+ try { await routeInstallFromText(await _readDroppedText(carrier)); }
8090
+ catch (_) { if (typeof setStatus === 'function') setStatus('err', 'could not read the dropped file'); }
8091
+ }
8092
+ function handleCarrierDragOver(e) {
8093
+ // Let a file drop fire anywhere on the page (a carrier can be dropped onto the document, not only
8094
+ // the edit mount). Idempotent with the mount's own dragover; the drop handler decides whether to claim it.
8095
+ const t = e && e.dataTransfer;
8096
+ if (t && (Array.from(t.items || []).some(i => i.kind === 'file') || Array.from(t.types || []).includes('Files'))) e.preventDefault();
8097
+ }
8098
+ window.addEventListener('dragover', handleCarrierDragOver, true);
8099
+ window.addEventListener('drop', handleCarrierDrop, true);
8100
+ // Automation/test hooks (mirror window.__ingestImageFile).
8101
+ window.__rwaExtractAgentCarrier = extractAgentEnvelopesFromCarrier;
8102
+ window.__rwaClassifyInstallText = classifyInstallText;
8103
+ window.__rwaInstallFromText = routeInstallFromText;
8104
+ window.__rwaHandleCarrierDrop = handleCarrierDrop;
8105
+
8106
+ // ── intelligence/0.2 I-A (docs/specs/rwa-intelligence-spec.md §6) — recommended model on activation.
8107
+ // A carrier may carry a NON-SECRET recommended_model / recommended_backend on its rwa-agent/1
8108
+ // ENVELOPE (OUTSIDE the signed `agent`, so canonicalAgent is unchanged and the signature still
8109
+ // verifies — this stays seed-only). On activation the runtime OFFERS to apply it to sessionStorage
8110
+ // behind a one-line consent: it never auto-applies, never sets a base-URL, never touches the API
8111
+ // key. A recommendation is a suggestion, not a stored credential (key/model are sessionStorage-only).
8112
+ const REC_MODEL_RE = /^[A-Za-z0-9._:\/-]{1,200}$/; // a model id: provider/name, dots, colon (ollama), hyphen
8113
+ const REC_BACKENDS = ['openrouter', 'ollama', 'lmstudio', 'atomic', 'bridge', 'bridge-session'];
8114
+ function getRecommendation(envelope) {
8115
+ const e = envelope || {};
8116
+ const out = {};
8117
+ if (typeof e.recommended_model === 'string' && REC_MODEL_RE.test(e.recommended_model.trim())) out.model = e.recommended_model.trim();
8118
+ if (typeof e.recommended_backend === 'string' && REC_BACKENDS.includes(e.recommended_backend.trim())) out.backend = e.recommended_backend.trim();
8119
+ return (out.model || out.backend) ? out : null;
8120
+ }
8121
+ function applyRecommendation(rec) {
8122
+ const r = rec || {};
8123
+ const applied = {};
8124
+ if (r.model && REC_MODEL_RE.test(String(r.model))) { sessionStorage.setItem(RWA.K_MODEL, String(r.model)); applied.model = String(r.model); }
8125
+ if (r.backend && REC_BACKENDS.includes(String(r.backend))) { sessionStorage.setItem(RWA.K_BACKEND, String(r.backend)); applied.backend = String(r.backend); }
8126
+ try { const m = document.getElementById('rwa-model'); if (m && applied.model) m.value = applied.model; const b = document.getElementById('rwa-backend'); if (b && applied.backend) { b.value = applied.backend; b.dispatchEvent(new Event('change')); } } catch (_) {}
8127
+ return applied;
8128
+ }
8129
+ // Offer the role's recommended model, if any and if it differs from the current session. Fire-and-
8130
+ // forget consent dialog; resolves with the choice. Activation already happened — this only proposes.
8131
+ function offerRecommendedModel(role) {
8132
+ const recRec = Array.from(installedAgents.values()).find(a => a.role === role);
8133
+ const rec = recRec && getRecommendation(recRec.envelope);
8134
+ if (!rec) return Promise.resolve({ offered: false });
8135
+ const curModel = sessionStorage.getItem(RWA.K_MODEL) || RWA.MODEL;
8136
+ const curBackend = sessionStorage.getItem(RWA.K_BACKEND) || 'openrouter';
8137
+ if ((!rec.model || rec.model === curModel) && (!rec.backend || rec.backend === curBackend)) return Promise.resolve({ offered: false });
8138
+ return new Promise(resolve => {
8139
+ const prev = document.getElementById('rwa-model-offer'); if (prev) prev.remove();
8140
+ const overlay = document.createElement('div'); overlay.id = 'rwa-model-offer';
8141
+ overlay.style.cssText = 'position:fixed;inset:0;z-index:99999;background:rgba(0,0,0,.45);display:flex;align-items:center;justify-content:center;';
8142
+ const card = document.createElement('div');
8143
+ card.style.cssText = 'background:#fff;max-width:480px;width:92%;border-radius:18px;padding:24px 26px;font:14px/1.5 var(--font-ui,system-ui);box-shadow:0 12px 48px rgba(0,0,0,.28);';
8144
+ const what = [rec.model ? 'model <code>' + _skEsc(rec.model) + '</code>' : '', rec.backend ? 'backend <code>' + _skEsc(rec.backend) + '</code>' : ''].filter(Boolean).join(' on ');
8145
+ card.innerHTML =
8146
+ '<h2 style="margin:0 0 .4em;font-size:1.15rem">Use this intelligence’s recommended ' + (rec.model ? 'model' : 'backend') + '?</h2>' +
8147
+ '<p style="margin:.2em 0;color:#444">The <strong>' + _skEsc(role) + '</strong> role suggests ' + what + ' for ⌘K. This is a <em>recommendation</em> (not part of the signed role) — applying it changes only your session’s model/backend selection. Your API key is untouched.</p>' +
8148
+ '<div style="display:flex;gap:10px;margin-top:1.2em;justify-content:flex-end">' +
8149
+ '<button data-act="keep" style="padding:9px 16px;border:1px solid #ccc;border-radius:10px;background:#fff;cursor:pointer">Keep current</button>' +
8150
+ '<button data-act="apply" style="padding:9px 16px;border:none;border-radius:10px;background:var(--gray-900,#111);color:#fff;cursor:pointer">Use it</button>' +
8151
+ '</div>';
8152
+ overlay.appendChild(card); document.body.appendChild(overlay);
8153
+ const close = (choice) => { overlay.remove(); resolve(choice); };
8154
+ card.querySelector('[data-act=keep]').onclick = () => close({ offered: true, applied: null });
8155
+ card.querySelector('[data-act=apply]').onclick = () => close({ offered: true, applied: applyRecommendation(rec) });
8156
+ });
8157
+ }
8158
+ // Activate a role AND offer its recommended model — the "on activation" entry the UI calls.
8159
+ function runtimeActivateAgent(role) { runtimeSetActiveAgent(role); return offerRecommendedModel(role); }
8160
+ window.__rwaGetRecommendation = getRecommendation;
8161
+ window.__rwaApplyRecommendation = applyRecommendation;
8162
+ window.__rwaOfferRecommendedModel = offerRecommendedModel;
8163
+ window.__rwaActivateAgent = runtimeActivateAgent;
8164
+
8001
8165
  // §4/§5a — does a network: host pattern admit a host? Mirror of cli/src/skill-manifest.mjs
8002
8166
  // matchNetworkOrigin (keep in step). The bridge's per-call origin check.
8003
8167
  function _skMatchNetworkOrigin(pattern, host) {
@@ -9530,7 +9694,7 @@ document.addEventListener('keydown', e => {
9530
9694
  discoverSkills: runtimeDiscoverSkills, // v0.9 §11 (I6) — GET the marketplace index (opt-in network)
9531
9695
  fetchSkillFromIndex: runtimeFetchSkillFromIndex, // v0.9 §11 (I6) — fetch + client-side-verify an indexed skill
9532
9696
  vault: { get: runtimeVaultGet, set: runtimeVaultSet, has: runtimeVaultHas, namespaces: runtimeVaultNamespaces, unlock: runtimeVaultUnlock, lock: runtimeVaultLock, isLocked: runtimeVaultIsLocked, export: runtimeVaultExport, import: runtimeVaultImport }, // v0.8 §6 + v0.9 §14 (I13) portable export/import
9533
- agents: { list: runtimeListAgents, active: runtimeAgentActive, setActive: runtimeSetActiveAgent, install: runtimeInstallAgent, uninstall: runtimeUninstallAgent, message: runtimeAgentMessage, showInstallDialog: showAgentInstallDialog }, // v0.9 §12 — multi-agent roles
9697
+ agents: { list: runtimeListAgents, active: runtimeAgentActive, setActive: runtimeSetActiveAgent, activate: runtimeActivateAgent, install: runtimeInstallAgent, uninstall: runtimeUninstallAgent, message: runtimeAgentMessage, showInstallDialog: showAgentInstallDialog, offerModel: offerRecommendedModel }, // v0.9 §12 — multi-agent roles; `activate` = setActive + offer recommended model (intelligence/0.2 I-A)
9534
9698
  hookLog: runtimeHookLog, // v0.9 §9 — the hook audit trail (rwa_hook_log)
9535
9699
  };
9536
9700
  // `status` is a getter so each read returns a fresh snapshot of