rewritable 0.14.0 → 0.15.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 +13 -0
- package/bin/rwa.mjs +30 -0
- package/package.json +1 -1
- package/seeds/rewritable.html +25 -4
- package/src/intelligence.mjs +79 -0
package/README.md
CHANGED
|
@@ -261,6 +261,19 @@ rwa host notes.html --url https://host.example --json # {"id":"…","token":"
|
|
|
261
261
|
|
|
262
262
|
This command is **network-bearing** (like `rwa clone` / `rwa publish-site`), so the offline-first rule does not apply to it.
|
|
263
263
|
|
|
264
|
+
### `rwa intelligence new <role>`
|
|
265
|
+
|
|
266
|
+
Mint a **droppable intelligence** — a signed `rwa-agent/1` role packaged as a *carrier* rewritable you can drop onto another rewritable to retune its ⌘K editor (intelligence/0.2). It generates a fresh Ed25519 keypair, signs the role, and scaffolds a self-describing `skill-host` carrier. The **private key** is written to a sibling `<name>.key.json`, kept out of the carrier — keep it to publish updates under the same author identity.
|
|
267
|
+
|
|
268
|
+
```
|
|
269
|
+
rwa intelligence new concise \
|
|
270
|
+
--prompt "Tighten prose: shorter sentences, fewer hedges, meaning preserved." \
|
|
271
|
+
--model anthropic/claude-sonnet-4-6 --backend openrouter \
|
|
272
|
+
--affinity document,presentation
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Flags: `--prompt` (required — the role's system prompt), `--description`, `--model` / `--backend` (a *recommended* model offered on activation behind consent — never auto-applied, never carries your key), `--affinity` (comma-separated document kinds; advisory — a mismatch only warns), `--vault` (comma-separated namespaces the role may reach), `--out <path>`, `--force`. Offline; the carrier holds only the public key + signature.
|
|
276
|
+
|
|
264
277
|
### `rwa skin <path> <name>`
|
|
265
278
|
|
|
266
279
|
Pick a **named look** for a rewritable instead of hand-styling it from the blank lens. A skin is one self-contained `<style data-rwa-skin="NAME">` block — system fonts only, no web fonts or remote assets — that the command splices into the **document body**. So it commits with the document, ships inside the exported `.html`, survives sharing, and one in-browser undo (`⌘Z`) reverts it. Five presets ship today: `notion-clean`, `linear-dark`, `editorial-serif`, `stripe-docs`, `terminal-mono` (clean · dark · editorial · docs · terminal).
|
package/bin/rwa.mjs
CHANGED
|
@@ -728,6 +728,36 @@ function detectProductKind(fileText) {
|
|
|
728
728
|
// `rwa skill publish <file.rwa-skill.json> [--url base] [--json]` — publish a SIGNED skill
|
|
729
729
|
// envelope to the marketplace index (POST /skills/publish, I6 §11). The envelope is already
|
|
730
730
|
// signed (no key needed). Online by design; exit 4 labeled `publish_error` (like `publish`).
|
|
731
|
+
// `rwa intelligence new <role> --prompt "..." [--description ..] [--model id] [--backend name]
|
|
732
|
+
// [--affinity kind,kind] [--vault ns,ns] [--out file] [--force]` — I-C (intelligence/0.2 §6):
|
|
733
|
+
// mint a signed rwa-agent/1 role and scaffold a carrier rewritable (private key → sibling file).
|
|
734
|
+
if (verb === 'intelligence') {
|
|
735
|
+
const sub = rest[0];
|
|
736
|
+
if (sub !== 'new') {
|
|
737
|
+
process.stderr.write("rwa intelligence: unknown subcommand '" + (sub || '') + "' (try: rwa intelligence new <role> --prompt \"...\")\n");
|
|
738
|
+
process.exitCode = 1;
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
const subRest = rest.slice(1);
|
|
742
|
+
const valFlags = ['--prompt', '--description', '--model', '--backend', '--affinity', '--vault', '--out'];
|
|
743
|
+
const role = subRest.find((a, i) => !a.startsWith('-') && !valFlags.includes(subRest[i - 1]));
|
|
744
|
+
const g = (n) => getFlag(n, subRest).value;
|
|
745
|
+
const list = (n) => { const v = g(n); return v ? v.split(',').map(s => s.trim()).filter(Boolean) : []; };
|
|
746
|
+
try {
|
|
747
|
+
const { intelligenceNewCmd } = await import('../src/intelligence.mjs');
|
|
748
|
+
await intelligenceNewCmd({
|
|
749
|
+
role, prompt: g('--prompt'), description: g('--description'),
|
|
750
|
+
model: g('--model'), backend: g('--backend'),
|
|
751
|
+
affinity: list('--affinity'), vault: list('--vault'),
|
|
752
|
+
outPath: g('--out'), force: subRest.includes('--force') || subRest.includes('-f'),
|
|
753
|
+
});
|
|
754
|
+
} catch (e) {
|
|
755
|
+
process.stderr.write('rwa intelligence: ' + ((e && e.message) || e) + '\n');
|
|
756
|
+
process.exitCode = (e && e.exitCode) || 1;
|
|
757
|
+
}
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
|
|
731
761
|
if (verb === 'skill') {
|
|
732
762
|
const sub = rest[0];
|
|
733
763
|
const subRest = rest.slice(1);
|
package/package.json
CHANGED
package/seeds/rewritable.html
CHANGED
|
@@ -2332,7 +2332,8 @@ async function renderActionsModePanel(panel) {
|
|
|
2332
2332
|
const isActive = a.role === activeRole;
|
|
2333
2333
|
const isAdvisor = advisorSet.indexOf(a.role) >= 0;
|
|
2334
2334
|
const state = isActive ? 'primary' : (isAdvisor ? 'advisor' : '');
|
|
2335
|
-
const
|
|
2335
|
+
const aff = (a.affinity && a.affinity.length) ? ' · for ' + a.affinity.join('/') : '';
|
|
2336
|
+
const meta = (a.verified ? 'verified' : 'unverified') + (state ? ' · ' + state : '') + aff;
|
|
2336
2337
|
let btns = '';
|
|
2337
2338
|
if (a.verified) {
|
|
2338
2339
|
const r = escRuntimeHtml(a.role);
|
|
@@ -2375,14 +2376,16 @@ async function renderActionsModePanel(panel) {
|
|
|
2375
2376
|
btn.addEventListener('click', () => runtimeSetMode('skills'));
|
|
2376
2377
|
});
|
|
2377
2378
|
panel.querySelectorAll('[data-agent-on]').forEach(btn => btn.addEventListener('click', () => {
|
|
2378
|
-
|
|
2379
|
+
const role = btn.getAttribute('data-agent-on');
|
|
2380
|
+
try { runtimeActivateAgent(role); const aw = affinityWarning(role); if (aw && typeof setStatus === 'function') setStatus('', aw); } // setActive + offer model; advisory affinity warn (I-D)
|
|
2379
2381
|
catch (e) { if (typeof setStatus === 'function') setStatus('err', '✗ ' + (e && (e.code || e.message))); }
|
|
2380
2382
|
renderActionsModePanel(panel);
|
|
2381
2383
|
}));
|
|
2382
2384
|
const agentOff = panel.querySelector('[data-agent-off]');
|
|
2383
2385
|
if (agentOff) agentOff.addEventListener('click', () => { runtimeSetActiveAgent(null); renderActionsModePanel(panel); });
|
|
2384
2386
|
panel.querySelectorAll('[data-agent-advon]').forEach(btn => btn.addEventListener('click', () => {
|
|
2385
|
-
|
|
2387
|
+
const role = btn.getAttribute('data-agent-advon');
|
|
2388
|
+
try { runtimeAddAdvisor(role); const aw = affinityWarning(role); if (aw && typeof setStatus === 'function') setStatus('', aw); } // I-E: layer an advisory lens; I-D affinity warn
|
|
2386
2389
|
catch (e) { if (typeof setStatus === 'function') setStatus('err', '✗ ' + (e && (e.code || e.message))); }
|
|
2387
2390
|
renderActionsModePanel(panel);
|
|
2388
2391
|
}));
|
|
@@ -7844,7 +7847,7 @@ function _agAgentsRegion() {
|
|
|
7844
7847
|
};
|
|
7845
7848
|
}
|
|
7846
7849
|
function runtimeListAgents() {
|
|
7847
|
-
return Array.from(installedAgents.values()).map(a => ({ role: a.role, author_pubkey: a.manifest && a.manifest.author_pubkey, verified: a.verified }));
|
|
7850
|
+
return Array.from(installedAgents.values()).map(a => ({ role: a.role, author_pubkey: a.manifest && a.manifest.author_pubkey, verified: a.verified, affinity: getAffinity(a.envelope) }));
|
|
7848
7851
|
}
|
|
7849
7852
|
function runtimeAgentActive() {
|
|
7850
7853
|
if (!activeAgentRole) return null;
|
|
@@ -8166,6 +8169,24 @@ function getRecommendation(envelope) {
|
|
|
8166
8169
|
if (typeof e.recommended_backend === 'string' && REC_BACKENDS.includes(e.recommended_backend.trim())) out.backend = e.recommended_backend.trim();
|
|
8167
8170
|
return (out.model || out.backend) ? out : null;
|
|
8168
8171
|
}
|
|
8172
|
+
// I-D (intelligence/0.2 §6) — advisory kind-affinity. A role may declare an UNSIGNED `affinity`
|
|
8173
|
+
// envelope field (the document kinds it is tuned for). Activating/advising it on a mismatched
|
|
8174
|
+
// PRODUCT_KIND only WARNS — never blocks (spec §4: affinity is a soft note). Advisory by design.
|
|
8175
|
+
function getAffinity(envelope) {
|
|
8176
|
+
const a = envelope && envelope.affinity;
|
|
8177
|
+
if (Array.isArray(a)) return a.filter(x => typeof x === 'string' && x);
|
|
8178
|
+
if (typeof a === 'string' && a) return [a];
|
|
8179
|
+
return [];
|
|
8180
|
+
}
|
|
8181
|
+
function affinityWarning(role) {
|
|
8182
|
+
const rec = Array.from(installedAgents.values()).find(x => x.role === role);
|
|
8183
|
+
if (!rec) return null;
|
|
8184
|
+
const aff = getAffinity(rec.envelope);
|
|
8185
|
+
if (!aff.length || aff.indexOf(PRODUCT_KIND) >= 0) return null;
|
|
8186
|
+
return '⚠ “' + role + '” is tuned for ' + aff.join('/') + ' documents, but this is a ' + PRODUCT_KIND + ' — applied anyway.';
|
|
8187
|
+
}
|
|
8188
|
+
window.__rwaGetAffinity = getAffinity;
|
|
8189
|
+
window.__rwaAffinityWarning = affinityWarning;
|
|
8169
8190
|
function applyRecommendation(rec) {
|
|
8170
8191
|
const r = rec || {};
|
|
8171
8192
|
const applied = {};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// I-C (intelligence/0.2 §6) — `rwa intelligence new <role>`: mint a signed rwa-agent/1 role and
|
|
2
|
+
// scaffold a CARRIER rewritable (a skill-host holding the record + a self-describing card). The
|
|
3
|
+
// carrier ships only the PUBLIC key + signature; the PRIVATE key is written to a sibling .key.json
|
|
4
|
+
// (keep it to publish updates under the same author identity). Offline; reuses the agent canon
|
|
5
|
+
// (skill-manifest) and the seed bootstrap (seed.mjs) — no new wire-type, no canon fork.
|
|
6
|
+
import fs from 'node:fs/promises';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { webcrypto, randomUUID } from 'node:crypto';
|
|
9
|
+
import { SEED_CANDIDATES } from './commands.mjs';
|
|
10
|
+
import { loadSeed, applySeedSubs, kindOverrides, replaceInlineDoc } from './seed.mjs';
|
|
11
|
+
import { agentSigningMessage } from './skill-manifest.mjs';
|
|
12
|
+
|
|
13
|
+
const ROLE_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
14
|
+
const REC_MODEL_RE = /^[A-Za-z0-9._:\/-]{1,200}$/;
|
|
15
|
+
const REC_BACKENDS = ['openrouter', 'ollama', 'lmstudio', 'atomic', 'bridge', 'bridge-session'];
|
|
16
|
+
const b64 = (u8) => Buffer.from(u8).toString('base64');
|
|
17
|
+
const rel = (p) => path.relative(process.cwd(), p) || p;
|
|
18
|
+
const esc = (s) => String(s == null ? '' : s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
19
|
+
const fail = (msg, code = 2) => { const e = new Error(msg); e.exitCode = code; throw e; };
|
|
20
|
+
|
|
21
|
+
export async function intelligenceNewCmd(opts = {}) {
|
|
22
|
+
const role = opts.role, prompt = opts.prompt;
|
|
23
|
+
if (!role || !ROLE_RE.test(role)) fail('intelligence: <role> must be lowercase a-z0-9_- (≤64, leading alphanumeric)');
|
|
24
|
+
if (!prompt || typeof prompt !== 'string') fail('intelligence: --prompt "<system prompt>" is required');
|
|
25
|
+
if (prompt.includes('`') || prompt.includes('${') || /<\/?DOC>/i.test(prompt)) fail('intelligence: --prompt must not contain ` ${ or <DOC>');
|
|
26
|
+
if (opts.model != null && !REC_MODEL_RE.test(String(opts.model))) fail('intelligence: --model is not a valid model id');
|
|
27
|
+
if (opts.backend != null && !REC_BACKENDS.includes(String(opts.backend))) fail('intelligence: --backend must be one of ' + REC_BACKENDS.join('/'));
|
|
28
|
+
const vault = (opts.vault || []).map(v => /^vault:/.test(v) ? v : 'vault:' + v);
|
|
29
|
+
const affinity = (opts.affinity || []).filter(Boolean);
|
|
30
|
+
|
|
31
|
+
// Mint + sign the rwa-agent/1 record. The signature is over `agent` (the canon); the
|
|
32
|
+
// recommendation/affinity ride OUTSIDE it (unsigned envelope fields, per I-A/I-D).
|
|
33
|
+
const kp = await webcrypto.subtle.generateKey({ name: 'Ed25519' }, true, ['sign', 'verify']);
|
|
34
|
+
const author_pubkey = b64(new Uint8Array(await webcrypto.subtle.exportKey('raw', kp.publicKey)));
|
|
35
|
+
const agent = { author_pubkey, description: opts.description || ('The ' + role + ' role.'), role, system_prompt: prompt, vault_namespace_set: vault, version: 'rwa-agent/1' };
|
|
36
|
+
const signature = b64(new Uint8Array(await webcrypto.subtle.sign({ name: 'Ed25519' }, kp.privateKey, agentSigningMessage(agent))));
|
|
37
|
+
const envelope = { agent, signature };
|
|
38
|
+
if (opts.model) envelope.recommended_model = String(opts.model);
|
|
39
|
+
if (opts.backend) envelope.recommended_backend = String(opts.backend);
|
|
40
|
+
if (affinity.length) envelope.affinity = affinity;
|
|
41
|
+
|
|
42
|
+
// Scaffold the carrier — a skill-host bootstrap + card + frozen #rwa-agents zone.
|
|
43
|
+
const out = path.resolve(opts.outPath || ('./' + role + '.intelligence.html'));
|
|
44
|
+
if (!opts.force) { let exists = false; try { await fs.access(out); exists = true; } catch (_) {} if (exists) fail('intelligence: ' + rel(out) + ' exists (use --force)'); }
|
|
45
|
+
const seed = await loadSeed(SEED_CANDIDATES);
|
|
46
|
+
const ov = kindOverrides('skill-host');
|
|
47
|
+
let result = applySeedSubs(seed, { uuid: randomUUID(), title: 'Intelligence — ' + role, fileMeta: path.basename(out), productKind: 'skill-host', lensPlaceholder: ov.lensPlaceholder, palPlaceholder: ov.palPlaceholder, productHeader: ov.productHeader, lensClickToAnchor: ov.lensClickToAnchor });
|
|
48
|
+
const zone = '<div data-rwa-frozen id="rwa-agents"><script type="application/rwa-agent+json">' + b64(Buffer.from(JSON.stringify(envelope))) + '</script></div>';
|
|
49
|
+
result = replaceInlineDoc(result, buildCard({ role, prompt, model: opts.model, backend: opts.backend, affinity, vault }) + '\n' + zone);
|
|
50
|
+
await fs.writeFile(out, result, 'utf8');
|
|
51
|
+
|
|
52
|
+
// The private key — needed to re-sign updates under the same author identity. Sibling file, loud.
|
|
53
|
+
const fingerprint = Buffer.from(await webcrypto.subtle.digest('SHA-256', Buffer.from(author_pubkey, 'base64'))).toString('hex').slice(0, 16);
|
|
54
|
+
const keyOut = out.replace(/\.html?$/i, '') + '.key.json';
|
|
55
|
+
// 0600: the file holds the PRIVATE key — owner read/write only, never world-readable.
|
|
56
|
+
await fs.writeFile(keyOut, JSON.stringify({
|
|
57
|
+
role, author_pubkey, fingerprint,
|
|
58
|
+
private_key_pkcs8_b64: b64(new Uint8Array(await webcrypto.subtle.exportKey('pkcs8', kp.privateKey))),
|
|
59
|
+
warning: 'SECRET. Keep this to publish updates to this intelligence under the same author identity. Never commit or share it. The carrier .html holds only the public key.',
|
|
60
|
+
}, null, 2) + '\n', { mode: 0o600 });
|
|
61
|
+
try { await fs.chmod(keyOut, 0o600); } catch (_) {} // guarantee owner-only even if the file pre-existed (writeFile mode applies only on create; best-effort on non-POSIX)
|
|
62
|
+
|
|
63
|
+
console.log('wrote ' + rel(out) + ' (intelligence "' + role + '")');
|
|
64
|
+
console.log('author ' + fingerprint + ' — private key saved to ' + rel(keyOut) + ' (keep secret; needed to update this intelligence)');
|
|
65
|
+
return { out, keyOut, fingerprint, envelope };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function buildCard({ role, prompt, model, backend, affinity, vault }) {
|
|
69
|
+
const recLine = model ? '\n<li><strong>Recommended model:</strong> <code>' + esc(model) + '</code>' + (backend ? ' on <code>' + esc(backend) + '</code>' : '') + ' — offered on activation, behind consent (your session only; key untouched).</li>' : '';
|
|
70
|
+
const affLine = affinity.length ? '\n<li><strong>Affinity:</strong> ' + esc(affinity.join(', ')) + ' (advisory — a mismatch only warns).</li>' : '';
|
|
71
|
+
const vaultLine = '\n<li><strong>Vault namespaces:</strong> ' + (vault.length ? esc(vault.join(', ')) : 'none') + '.</li>';
|
|
72
|
+
return '<article>\n' +
|
|
73
|
+
'<h1>Intelligence — “' + esc(role) + '”</h1>\n' +
|
|
74
|
+
'<p class="lede">A droppable <strong>intelligence</strong> (intelligence/0.2): a signed <code>rwa-agent/1</code> role you can drop onto another rewritable to retune its ⌘K editor. This file is the carrier — open it, read it, drop it.</p>\n' +
|
|
75
|
+
'<h2>What it does</h2>\n<p>' + esc(prompt) + '</p>\n' +
|
|
76
|
+
'<h2>What it carries</h2>\n<ul>\n<li><strong>Role:</strong> <code>' + esc(role) + '</code></li>' + recLine + affLine + vaultLine + '\n</ul>\n' +
|
|
77
|
+
'<h2>How to use it</h2>\n<p>Drop this file onto another rewritable to install the role (behind the consent dialog), then activate it from the Activity panel’s <em>Intelligences</em> section. This carrier is itself a skill-host, so the role is already installed here — try it directly.</p>\n' +
|
|
78
|
+
'</article>';
|
|
79
|
+
}
|