drafted 1.8.4 → 1.8.6
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/cli/drafted.mjs +244 -0
- package/package.json +1 -1
package/cli/drafted.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSy
|
|
|
13
13
|
import { join, dirname, basename, resolve } from 'path';
|
|
14
14
|
import { homedir, tmpdir, platform } from 'os';
|
|
15
15
|
import { fileURLToPath } from 'url';
|
|
16
|
+
import { createHash } from 'node:crypto';
|
|
16
17
|
import { DESIGN_SYSTEM_PROMPT, buildDesignPrompt } from './prompts.mjs';
|
|
17
18
|
|
|
18
19
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -1188,4 +1189,247 @@ program
|
|
|
1188
1189
|
console.log('Restart Claude Code for changes to take effect.');
|
|
1189
1190
|
});
|
|
1190
1191
|
|
|
1192
|
+
// ── skill: Causeway sync seam ────────────────────────────────────
|
|
1193
|
+
// Materialize pinned skills into a local cache dir so Causeway can project them
|
|
1194
|
+
// into agents and run their bundled scripts. The Causeway daemon shells out here
|
|
1195
|
+
// so it never holds Drafted's session token. Contract (in the causeway repo):
|
|
1196
|
+
// docs/contracts/drafted-skill-sync.md.
|
|
1197
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1198
|
+
|
|
1199
|
+
// synthesizeSkillMd builds a self-describing SKILL.md. Drafted stores `content`
|
|
1200
|
+
// as the body only (name/description/triggerPatterns are columns), so we prepend
|
|
1201
|
+
// YAML frontmatter — JSON scalars are valid YAML — for Causeway's projection and
|
|
1202
|
+
// Claude's native loader to read.
|
|
1203
|
+
function synthesizeSkillMd(skill) {
|
|
1204
|
+
const lines = ['---', `name: ${JSON.stringify(skill.name || skill.slug || '')}`];
|
|
1205
|
+
if (skill.description) lines.push(`description: ${JSON.stringify(skill.description)}`);
|
|
1206
|
+
const triggers = Array.isArray(skill.triggerPatterns) ? skill.triggerPatterns.filter(Boolean) : [];
|
|
1207
|
+
if (triggers.length) {
|
|
1208
|
+
lines.push('triggerPatterns:');
|
|
1209
|
+
for (const t of triggers) lines.push(` - ${JSON.stringify(t)}`);
|
|
1210
|
+
}
|
|
1211
|
+
lines.push('---', '');
|
|
1212
|
+
return lines.join('\n') + (skill.content || '');
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
// bundleHash is a stable content hash over the bundle (sorted path\0content\0),
|
|
1216
|
+
// so the same content yields the same hash regardless of cosmetic re-saves.
|
|
1217
|
+
// Drafted is the hash authority; this value is what a pin stores.
|
|
1218
|
+
function bundleHash(files) {
|
|
1219
|
+
const h = createHash('sha256');
|
|
1220
|
+
for (const p of Object.keys(files).sort()) {
|
|
1221
|
+
h.update(p); h.update('\0'); h.update(files[p]); h.update('\0');
|
|
1222
|
+
}
|
|
1223
|
+
return h.digest('hex').slice(0, 12);
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
// Build an `unresolvable` reason from a failed resolve response (404/403),
|
|
1227
|
+
// naming the org when --org was used so Causeway can render `✗ unresolvable
|
|
1228
|
+
// (org X)` instead of a silent miss.
|
|
1229
|
+
async function unresolvableReason(res, org) {
|
|
1230
|
+
let why = 'skill not found';
|
|
1231
|
+
try { const e = await res.json(); if (e && e.error) why = e.error; } catch { /* keep default */ }
|
|
1232
|
+
if (org && !why.toLowerCase().includes(String(org).toLowerCase())) return `${why} (org ${org})`;
|
|
1233
|
+
return why;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
async function syncOneSkill(ref, outDir, org) {
|
|
1237
|
+
const at = ref.lastIndexOf('@');
|
|
1238
|
+
const idOrSlug = at > 0 ? ref.slice(0, at) : ref;
|
|
1239
|
+
const server = getServerUrl().replace(/\/$/, '');
|
|
1240
|
+
// --org binds resolution to a specific org per-request (X-Drafted-Org header);
|
|
1241
|
+
// the server validates it against the caller's memberships and never mutates
|
|
1242
|
+
// the shared session's active org. Omitted -> ambient session org (unchanged).
|
|
1243
|
+
const orgHeaders = org ? { 'X-Drafted-Org': org } : {};
|
|
1244
|
+
const loadUrl = UUID_RE.test(idOrSlug)
|
|
1245
|
+
? `${server}/api/skills/${idOrSlug}`
|
|
1246
|
+
: `${server}/api/skills/slug/${encodeURIComponent(idOrSlug)}`;
|
|
1247
|
+
const res = await authFetch(loadUrl, { headers: orgHeaders });
|
|
1248
|
+
if (res.status === 404 || res.status === 403) return { ref, slug: '', hash: '', status: 'unresolvable', error: await unresolvableReason(res, org) };
|
|
1249
|
+
if (!res.ok) return { ref, slug: '', hash: '', status: 'error', error: `HTTP ${res.status}` };
|
|
1250
|
+
const skill = await res.json();
|
|
1251
|
+
const slug = skill.slug || idOrSlug;
|
|
1252
|
+
|
|
1253
|
+
const files = { 'SKILL.md': synthesizeSkillMd(skill) };
|
|
1254
|
+
for (const p of Array.isArray(skill.files) ? skill.files : []) {
|
|
1255
|
+
if (typeof p !== 'string' || p.includes('..')) continue;
|
|
1256
|
+
const encoded = p.split('/').map(encodeURIComponent).join('/');
|
|
1257
|
+
const fr = await authFetch(`${server}/api/skills/${skill.id}/files/${encoded}`, { headers: orgHeaders });
|
|
1258
|
+
if (!fr.ok) return { ref, slug, hash: '', status: 'error', error: `file ${p}: HTTP ${fr.status}` };
|
|
1259
|
+
const fdata = await fr.json();
|
|
1260
|
+
files[p] = typeof fdata === 'string' ? fdata : (fdata.content || '');
|
|
1261
|
+
}
|
|
1262
|
+
const hash = bundleHash(files);
|
|
1263
|
+
|
|
1264
|
+
const base = join(outDir, slug);
|
|
1265
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
1266
|
+
const full = join(base, rel);
|
|
1267
|
+
mkdirSync(dirname(full), { recursive: true });
|
|
1268
|
+
writeFileSync(full, content);
|
|
1269
|
+
}
|
|
1270
|
+
return { ref, slug, hash, status: 'ok' };
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
const skillCmd = program.command('skill').description('Skill library operations');
|
|
1274
|
+
skillCmd
|
|
1275
|
+
.command('sync')
|
|
1276
|
+
.description('Materialize pinned skills into a local cache dir (Causeway seam)')
|
|
1277
|
+
.option('--ref <ref>', 'skill ref slug[@hash] (repeatable)', (v, acc) => { acc.push(v); return acc; }, [])
|
|
1278
|
+
.requiredOption('--out <dir>', 'output directory for bundles')
|
|
1279
|
+
.option('--org <org>', 'resolve against this Drafted org (id or name); scopes per-request without switching the session')
|
|
1280
|
+
.option('--format <fmt>', 'output format: json or text', 'text')
|
|
1281
|
+
.action(async (opts) => {
|
|
1282
|
+
requireLogin();
|
|
1283
|
+
const refs = opts.ref || [];
|
|
1284
|
+
mkdirSync(opts.out, { recursive: true });
|
|
1285
|
+
const results = [];
|
|
1286
|
+
let allOk = refs.length > 0;
|
|
1287
|
+
for (const ref of refs) {
|
|
1288
|
+
try {
|
|
1289
|
+
const r = await syncOneSkill(ref, opts.out, opts.org);
|
|
1290
|
+
results.push(r);
|
|
1291
|
+
if (r.status !== 'ok') allOk = false;
|
|
1292
|
+
} catch (err) {
|
|
1293
|
+
results.push({ ref, slug: '', hash: '', status: 'error', error: String((err && err.message) || err) });
|
|
1294
|
+
allOk = false;
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
if (opts.format === 'json') {
|
|
1298
|
+
console.log(JSON.stringify(results));
|
|
1299
|
+
} else {
|
|
1300
|
+
for (const r of results) {
|
|
1301
|
+
console.log(`${r.status}\t${r.ref}\t${r.slug}\t${r.hash}${r.error ? '\t' + r.error : ''}`);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
process.exit(allOk ? 0 : 1);
|
|
1305
|
+
});
|
|
1306
|
+
|
|
1307
|
+
// ── skill mutations: Causeway write seam ─────────────────────────
|
|
1308
|
+
// add/update/remove mirror the sync seam so the Causeway daemon can create and
|
|
1309
|
+
// improve reusable skills without holding Drafted's token. Each reads its payload
|
|
1310
|
+
// from stdin JSON and prints a {slug,id,hash,status} result line.
|
|
1311
|
+
function readStdinJSON() {
|
|
1312
|
+
let raw = '';
|
|
1313
|
+
try { raw = readFileSync(0, 'utf8'); } catch { raw = ''; }
|
|
1314
|
+
raw = raw.trim();
|
|
1315
|
+
if (!raw) return {};
|
|
1316
|
+
return JSON.parse(raw);
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
function emitSkillResult(format, obj) {
|
|
1320
|
+
if (format === 'json') { console.log(JSON.stringify(obj)); return; }
|
|
1321
|
+
console.log([obj.status, obj.slug || '', obj.id || '', obj.hash || '', obj.error || ''].join('\t'));
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
skillCmd
|
|
1325
|
+
.command('add')
|
|
1326
|
+
.description('Create a skill in Drafted from stdin JSON {name,description,content,tags?,triggerPatterns?}')
|
|
1327
|
+
.option('--format <fmt>', 'output format: json or text', 'text')
|
|
1328
|
+
.action(async (opts) => {
|
|
1329
|
+
requireLogin();
|
|
1330
|
+
let p;
|
|
1331
|
+
try { p = readStdinJSON(); } catch { console.error('invalid JSON on stdin'); process.exit(1); }
|
|
1332
|
+
const server = getServerUrl().replace(/\/$/, '');
|
|
1333
|
+
const res = await authFetch(`${server}/api/skills`, {
|
|
1334
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1335
|
+
body: JSON.stringify({ name: p.name, description: p.description, content: p.content, tags: p.tags, triggerPatterns: p.triggerPatterns }),
|
|
1336
|
+
});
|
|
1337
|
+
const data = await res.json().catch(() => ({}));
|
|
1338
|
+
if (!res.ok) {
|
|
1339
|
+
emitSkillResult(opts.format, { status: res.status === 409 ? 'conflict' : 'error', error: data.error || `HTTP ${res.status}` });
|
|
1340
|
+
process.exit(1);
|
|
1341
|
+
}
|
|
1342
|
+
emitSkillResult(opts.format, { status: 'ok', slug: data.slug, id: data.id, hash: bundleHash({ 'SKILL.md': synthesizeSkillMd(data) }) });
|
|
1343
|
+
});
|
|
1344
|
+
|
|
1345
|
+
skillCmd
|
|
1346
|
+
.command('update')
|
|
1347
|
+
.description('Update a Drafted skill (--id or --slug) from stdin JSON {description?,content?,triggerPatterns?,tags?}')
|
|
1348
|
+
.option('--id <id>', 'skill id')
|
|
1349
|
+
.option('--slug <slug>', 'skill slug (resolved to id)')
|
|
1350
|
+
.option('--format <fmt>', 'output format: json or text', 'text')
|
|
1351
|
+
.action(async (opts) => {
|
|
1352
|
+
requireLogin();
|
|
1353
|
+
let p;
|
|
1354
|
+
try { p = readStdinJSON(); } catch { console.error('invalid JSON on stdin'); process.exit(1); }
|
|
1355
|
+
const server = getServerUrl().replace(/\/$/, '');
|
|
1356
|
+
let id = opts.id;
|
|
1357
|
+
if (!id && opts.slug) {
|
|
1358
|
+
const lookup = await authFetch(`${server}/api/skills/slug/${encodeURIComponent(opts.slug)}`);
|
|
1359
|
+
if (!lookup.ok) { emitSkillResult(opts.format, { status: 'unresolvable', error: `slug ${opts.slug}: HTTP ${lookup.status}` }); process.exit(1); }
|
|
1360
|
+
id = (await lookup.json().catch(() => ({}))).id;
|
|
1361
|
+
}
|
|
1362
|
+
if (!id) { emitSkillResult(opts.format, { status: 'error', error: 'update requires --id or --slug' }); process.exit(1); }
|
|
1363
|
+
const res = await authFetch(`${server}/api/skills/${id}`, {
|
|
1364
|
+
method: 'PUT', headers: { 'Content-Type': 'application/json' },
|
|
1365
|
+
body: JSON.stringify({ name: p.name, description: p.description, content: p.content, tags: p.tags, triggerPatterns: p.triggerPatterns }),
|
|
1366
|
+
});
|
|
1367
|
+
const data = await res.json().catch(() => ({}));
|
|
1368
|
+
if (!res.ok) {
|
|
1369
|
+
emitSkillResult(opts.format, { status: res.status === 404 ? 'unresolvable' : 'error', error: data.error || `HTTP ${res.status}` });
|
|
1370
|
+
process.exit(1);
|
|
1371
|
+
}
|
|
1372
|
+
emitSkillResult(opts.format, { status: 'ok', slug: data.slug, id: data.id, hash: bundleHash({ 'SKILL.md': synthesizeSkillMd(data) }) });
|
|
1373
|
+
});
|
|
1374
|
+
|
|
1375
|
+
// check reports the current canonical hash for refs WITHOUT materializing — the
|
|
1376
|
+
// Causeway freshness probe. Hash is computed the same way as sync, so a check
|
|
1377
|
+
// hash and a sync hash for the same skill match.
|
|
1378
|
+
async function checkOneSkill(ref, org) {
|
|
1379
|
+
const at = ref.lastIndexOf('@');
|
|
1380
|
+
const idOrSlug = at > 0 ? ref.slice(0, at) : ref;
|
|
1381
|
+
const server = getServerUrl().replace(/\/$/, '');
|
|
1382
|
+
const orgHeaders = org ? { 'X-Drafted-Org': org } : {};
|
|
1383
|
+
const loadUrl = UUID_RE.test(idOrSlug)
|
|
1384
|
+
? `${server}/api/skills/${idOrSlug}`
|
|
1385
|
+
: `${server}/api/skills/slug/${encodeURIComponent(idOrSlug)}`;
|
|
1386
|
+
const res = await authFetch(loadUrl, { headers: orgHeaders });
|
|
1387
|
+
if (res.status === 404 || res.status === 403) return { ref, slug: '', hash: '', status: 'unresolvable', error: await unresolvableReason(res, org) };
|
|
1388
|
+
if (!res.ok) return { ref, slug: '', hash: '', status: 'error', error: `HTTP ${res.status}` };
|
|
1389
|
+
const skill = await res.json();
|
|
1390
|
+
const files = { 'SKILL.md': synthesizeSkillMd(skill) };
|
|
1391
|
+
for (const p of Array.isArray(skill.files) ? skill.files : []) {
|
|
1392
|
+
if (typeof p !== 'string' || p.includes('..')) continue;
|
|
1393
|
+
const fr = await authFetch(`${server}/api/skills/${skill.id}/files/${p.split('/').map(encodeURIComponent).join('/')}`, { headers: orgHeaders });
|
|
1394
|
+
if (!fr.ok) return { ref, slug: skill.slug, hash: '', status: 'error', error: `file ${p}: HTTP ${fr.status}` };
|
|
1395
|
+
const fdata = await fr.json();
|
|
1396
|
+
files[p] = typeof fdata === 'string' ? fdata : (fdata.content || '');
|
|
1397
|
+
}
|
|
1398
|
+
return { ref, slug: skill.slug, hash: bundleHash(files), status: 'ok', updatedAt: skill.updatedAt };
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
skillCmd
|
|
1402
|
+
.command('check')
|
|
1403
|
+
.description('Report the current canonical hash for refs without materializing (freshness)')
|
|
1404
|
+
.option('--ref <ref>', 'skill ref (repeatable)', (v, acc) => { acc.push(v); return acc; }, [])
|
|
1405
|
+
.option('--org <org>', 'resolve against this Drafted org (id or name); scopes per-request without switching the session')
|
|
1406
|
+
.option('--format <fmt>', 'output format: json or text', 'text')
|
|
1407
|
+
.action(async (opts) => {
|
|
1408
|
+
requireLogin();
|
|
1409
|
+
const results = [];
|
|
1410
|
+
for (const ref of (opts.ref || [])) {
|
|
1411
|
+
try { results.push(await checkOneSkill(ref, opts.org)); }
|
|
1412
|
+
catch (e) { results.push({ ref, slug: '', hash: '', status: 'error', error: String((e && e.message) || e) }); }
|
|
1413
|
+
}
|
|
1414
|
+
if (opts.format === 'json') console.log(JSON.stringify(results));
|
|
1415
|
+
else for (const r of results) console.log(`${r.status}\t${r.ref}\t${r.slug}\t${r.hash}`);
|
|
1416
|
+
});
|
|
1417
|
+
|
|
1418
|
+
skillCmd
|
|
1419
|
+
.command('remove')
|
|
1420
|
+
.description('Delete a Drafted skill by --id (used to clean up throwaway skills)')
|
|
1421
|
+
.requiredOption('--id <id>', 'skill id')
|
|
1422
|
+
.option('--format <fmt>', 'output format: json or text', 'text')
|
|
1423
|
+
.action(async (opts) => {
|
|
1424
|
+
requireLogin();
|
|
1425
|
+
const server = getServerUrl().replace(/\/$/, '');
|
|
1426
|
+
const res = await authFetch(`${server}/api/skills/${opts.id}`, { method: 'DELETE' });
|
|
1427
|
+
const data = await res.json().catch(() => ({}));
|
|
1428
|
+
if (!res.ok) {
|
|
1429
|
+
emitSkillResult(opts.format, { status: 'error', id: opts.id, error: data.error || `HTTP ${res.status}` });
|
|
1430
|
+
process.exit(1);
|
|
1431
|
+
}
|
|
1432
|
+
emitSkillResult(opts.format, { status: 'ok', id: opts.id });
|
|
1433
|
+
});
|
|
1434
|
+
|
|
1191
1435
|
program.parse();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "drafted",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.6",
|
|
4
4
|
"description": "Drafted — visual thinking surface for humans and AI agents. Renders HTML, markdown, images, and code as frames on a zoomable canvas, with MCP tools for AI agents and real-time sync for humans.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|