drafted 1.8.6 → 1.9.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/cli/drafted.mjs +119 -5
- package/package.json +1 -1
package/cli/drafted.mjs
CHANGED
|
@@ -1200,6 +1200,13 @@ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
|
|
|
1200
1200
|
// as the body only (name/description/triggerPatterns are columns), so we prepend
|
|
1201
1201
|
// YAML frontmatter — JSON scalars are valid YAML — for Causeway's projection and
|
|
1202
1202
|
// Claude's native loader to read.
|
|
1203
|
+
// normalizeSetup mirrors the server (server/lib/skill-hash.mjs) — keep in lockstep.
|
|
1204
|
+
function normalizeSetup(setup) {
|
|
1205
|
+
if (setup == null) return [];
|
|
1206
|
+
const arr = Array.isArray(setup) ? setup : [setup];
|
|
1207
|
+
return arr.filter((s) => typeof s === 'string' && s.trim().length > 0);
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1203
1210
|
function synthesizeSkillMd(skill) {
|
|
1204
1211
|
const lines = ['---', `name: ${JSON.stringify(skill.name || skill.slug || '')}`];
|
|
1205
1212
|
if (skill.description) lines.push(`description: ${JSON.stringify(skill.description)}`);
|
|
@@ -1208,6 +1215,15 @@ function synthesizeSkillMd(skill) {
|
|
|
1208
1215
|
lines.push('triggerPatterns:');
|
|
1209
1216
|
for (const t of triggers) lines.push(` - ${JSON.stringify(t)}`);
|
|
1210
1217
|
}
|
|
1218
|
+
// `setup` emitted ONLY when present, so a skill without it produces the exact
|
|
1219
|
+
// SKILL.md (and hash) it did before the field existed. Must match the server.
|
|
1220
|
+
const setup = normalizeSetup(skill.setup);
|
|
1221
|
+
if (setup.length === 1 && typeof skill.setup === 'string') {
|
|
1222
|
+
lines.push(`setup: ${JSON.stringify(setup[0])}`);
|
|
1223
|
+
} else if (setup.length) {
|
|
1224
|
+
lines.push('setup:');
|
|
1225
|
+
for (const s of setup) lines.push(` - ${JSON.stringify(s)}`);
|
|
1226
|
+
}
|
|
1211
1227
|
lines.push('---', '');
|
|
1212
1228
|
return lines.join('\n') + (skill.content || '');
|
|
1213
1229
|
}
|
|
@@ -1259,7 +1275,9 @@ async function syncOneSkill(ref, outDir, org) {
|
|
|
1259
1275
|
const fdata = await fr.json();
|
|
1260
1276
|
files[p] = typeof fdata === 'string' ? fdata : (fdata.content || '');
|
|
1261
1277
|
}
|
|
1262
|
-
|
|
1278
|
+
// Drafted is the hash authority — prefer the server-reported hash; fall back to
|
|
1279
|
+
// local bundle compute only for older servers that don't return one.
|
|
1280
|
+
const hash = skill.hash || bundleHash(files);
|
|
1263
1281
|
|
|
1264
1282
|
const base = join(outDir, slug);
|
|
1265
1283
|
for (const [rel, content] of Object.entries(files)) {
|
|
@@ -1332,14 +1350,14 @@ skillCmd
|
|
|
1332
1350
|
const server = getServerUrl().replace(/\/$/, '');
|
|
1333
1351
|
const res = await authFetch(`${server}/api/skills`, {
|
|
1334
1352
|
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 }),
|
|
1353
|
+
body: JSON.stringify({ name: p.name, description: p.description, content: p.content, tags: p.tags, triggerPatterns: p.triggerPatterns, setup: p.setup }),
|
|
1336
1354
|
});
|
|
1337
1355
|
const data = await res.json().catch(() => ({}));
|
|
1338
1356
|
if (!res.ok) {
|
|
1339
1357
|
emitSkillResult(opts.format, { status: res.status === 409 ? 'conflict' : 'error', error: data.error || `HTTP ${res.status}` });
|
|
1340
1358
|
process.exit(1);
|
|
1341
1359
|
}
|
|
1342
|
-
emitSkillResult(opts.format, { status: 'ok', slug: data.slug, id: data.id, hash: bundleHash({ 'SKILL.md': synthesizeSkillMd(data) }) });
|
|
1360
|
+
emitSkillResult(opts.format, { status: 'ok', slug: data.slug, id: data.id, hash: data.hash || bundleHash({ 'SKILL.md': synthesizeSkillMd(data) }) });
|
|
1343
1361
|
});
|
|
1344
1362
|
|
|
1345
1363
|
skillCmd
|
|
@@ -1362,14 +1380,14 @@ skillCmd
|
|
|
1362
1380
|
if (!id) { emitSkillResult(opts.format, { status: 'error', error: 'update requires --id or --slug' }); process.exit(1); }
|
|
1363
1381
|
const res = await authFetch(`${server}/api/skills/${id}`, {
|
|
1364
1382
|
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 }),
|
|
1383
|
+
body: JSON.stringify({ name: p.name, description: p.description, content: p.content, tags: p.tags, triggerPatterns: p.triggerPatterns, setup: p.setup }),
|
|
1366
1384
|
});
|
|
1367
1385
|
const data = await res.json().catch(() => ({}));
|
|
1368
1386
|
if (!res.ok) {
|
|
1369
1387
|
emitSkillResult(opts.format, { status: res.status === 404 ? 'unresolvable' : 'error', error: data.error || `HTTP ${res.status}` });
|
|
1370
1388
|
process.exit(1);
|
|
1371
1389
|
}
|
|
1372
|
-
emitSkillResult(opts.format, { status: 'ok', slug: data.slug, id: data.id, hash: bundleHash({ 'SKILL.md': synthesizeSkillMd(data) }) });
|
|
1390
|
+
emitSkillResult(opts.format, { status: 'ok', slug: data.slug, id: data.id, hash: data.hash || bundleHash({ 'SKILL.md': synthesizeSkillMd(data) }) });
|
|
1373
1391
|
});
|
|
1374
1392
|
|
|
1375
1393
|
// check reports the current canonical hash for refs WITHOUT materializing — the
|
|
@@ -1387,6 +1405,10 @@ async function checkOneSkill(ref, org) {
|
|
|
1387
1405
|
if (res.status === 404 || res.status === 403) return { ref, slug: '', hash: '', status: 'unresolvable', error: await unresolvableReason(res, org) };
|
|
1388
1406
|
if (!res.ok) return { ref, slug: '', hash: '', status: 'error', error: `HTTP ${res.status}` };
|
|
1389
1407
|
const skill = await res.json();
|
|
1408
|
+
// Drafted is the hash authority — when the server reports a hash, trust it and
|
|
1409
|
+
// skip the per-file fetches entirely (cheap freshness probe). Older servers
|
|
1410
|
+
// without a reported hash fall back to local bundle compute.
|
|
1411
|
+
if (skill.hash) return { ref, slug: skill.slug, hash: skill.hash, status: 'ok', updatedAt: skill.updatedAt };
|
|
1390
1412
|
const files = { 'SKILL.md': synthesizeSkillMd(skill) };
|
|
1391
1413
|
for (const p of Array.isArray(skill.files) ? skill.files : []) {
|
|
1392
1414
|
if (typeof p !== 'string' || p.includes('..')) continue;
|
|
@@ -1432,4 +1454,96 @@ skillCmd
|
|
|
1432
1454
|
emitSkillResult(opts.format, { status: 'ok', id: opts.id });
|
|
1433
1455
|
});
|
|
1434
1456
|
|
|
1457
|
+
// Resolve --slug -> id (honoring --org), shared by fork/push. Returns id or null.
|
|
1458
|
+
async function resolveSkillId(opts, server, orgHeaders) {
|
|
1459
|
+
if (opts.id) return opts.id;
|
|
1460
|
+
if (!opts.slug) return null;
|
|
1461
|
+
const lookup = await authFetch(`${server}/api/skills/slug/${encodeURIComponent(opts.slug)}`, { headers: orgHeaders });
|
|
1462
|
+
if (!lookup.ok) return null;
|
|
1463
|
+
return (await lookup.json().catch(() => ({}))).id || null;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
skillCmd
|
|
1467
|
+
.command('fork')
|
|
1468
|
+
.description('Fork a skill into your org so you can edit it (block-then-fork; system/other-org skills are read-only)')
|
|
1469
|
+
.option('--id <id>', 'source skill id')
|
|
1470
|
+
.option('--slug <slug>', 'source skill slug (resolved to id)')
|
|
1471
|
+
.option('--org <org>', 'fork into / resolve against this Drafted org (id or name)')
|
|
1472
|
+
.option('--format <fmt>', 'output format: json or text', 'text')
|
|
1473
|
+
.action(async (opts) => {
|
|
1474
|
+
requireLogin();
|
|
1475
|
+
const server = getServerUrl().replace(/\/$/, '');
|
|
1476
|
+
const orgHeaders = opts.org ? { 'X-Drafted-Org': opts.org } : {};
|
|
1477
|
+
const id = await resolveSkillId(opts, server, orgHeaders);
|
|
1478
|
+
if (!id) { emitSkillResult(opts.format, { status: 'unresolvable', error: opts.slug ? `slug ${opts.slug} not found` : 'fork requires --id or --slug' }); process.exit(1); }
|
|
1479
|
+
const res = await authFetch(`${server}/api/skills/${id}/fork`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...orgHeaders } });
|
|
1480
|
+
const data = await res.json().catch(() => ({}));
|
|
1481
|
+
if (!res.ok) {
|
|
1482
|
+
const status = res.status === 409 ? 'conflict' : (res.status === 404 || res.status === 403 ? 'unresolvable' : 'error');
|
|
1483
|
+
emitSkillResult(opts.format, { status, slug: data.slug, error: data.error || `HTTP ${res.status}` });
|
|
1484
|
+
process.exit(1);
|
|
1485
|
+
}
|
|
1486
|
+
emitSkillResult(opts.format, { status: 'ok', slug: data.slug, id: data.id, hash: data.hash });
|
|
1487
|
+
});
|
|
1488
|
+
|
|
1489
|
+
// collectSkillTree walks a local source tree for `skill push`, pre-filtering heavy
|
|
1490
|
+
// dirs and any local .skillignore as a convenience so we don't ship a node_modules
|
|
1491
|
+
// tree over the wire. The SERVER re-runs the authoritative hygiene pipeline.
|
|
1492
|
+
function collectSkillTree(dir) {
|
|
1493
|
+
const root = resolve(dir);
|
|
1494
|
+
if (!existsSync(root) || !statSync(root).isDirectory()) throw new Error(`not a directory: ${dir}`);
|
|
1495
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', '.venv', 'venv', '__pycache__', 'dist', 'build', '.next', 'target', 'coverage', '.cache', '.turbo', '.gradle', 'Pods', '.terraform']);
|
|
1496
|
+
let ignore = () => false;
|
|
1497
|
+
const ignPath = join(root, '.skillignore');
|
|
1498
|
+
if (existsSync(ignPath)) {
|
|
1499
|
+
const pats = readFileSync(ignPath, 'utf8').split(/\r?\n/).map((l) => l.trim()).filter((l) => l && !l.startsWith('#'));
|
|
1500
|
+
ignore = (rel) => pats.some((p) => { const d = p.replace(/\/$/, ''); return rel === d || rel.startsWith(d + '/') || rel.split('/').pop() === d; });
|
|
1501
|
+
}
|
|
1502
|
+
const out = [];
|
|
1503
|
+
const walk = (abs, rel) => {
|
|
1504
|
+
for (const name of readdirSync(abs)) {
|
|
1505
|
+
const childAbs = join(abs, name);
|
|
1506
|
+
const childRel = rel ? `${rel}/${name}` : name;
|
|
1507
|
+
const st = statSync(childAbs);
|
|
1508
|
+
if (st.isDirectory()) { if (SKIP_DIRS.has(name) || ignore(childRel)) continue; walk(childAbs, childRel); }
|
|
1509
|
+
else if (st.isFile()) {
|
|
1510
|
+
if (ignore(childRel)) continue;
|
|
1511
|
+
const content = readFileSync(childAbs, 'utf8');
|
|
1512
|
+
if (content.includes('\x00')) continue; // skip binary; server rejects it anyway
|
|
1513
|
+
out.push({ path: childRel, content });
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
};
|
|
1517
|
+
walk(root, '');
|
|
1518
|
+
return out;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
skillCmd
|
|
1522
|
+
.command('push')
|
|
1523
|
+
.description('Push a local source tree into a skill (bulk ingest; server strips artifacts + enforces caps)')
|
|
1524
|
+
.option('--id <id>', 'skill id')
|
|
1525
|
+
.option('--slug <slug>', 'skill slug (resolved to id)')
|
|
1526
|
+
.requiredOption('--dir <dir>', 'local source tree to push')
|
|
1527
|
+
.option('--org <org>', 'resolve against this Drafted org (id or name)')
|
|
1528
|
+
.option('--delete-missing', 'remove stored files not present in the pushed tree')
|
|
1529
|
+
.option('--format <fmt>', 'output format: json or text', 'text')
|
|
1530
|
+
.action(async (opts) => {
|
|
1531
|
+
requireLogin();
|
|
1532
|
+
const server = getServerUrl().replace(/\/$/, '');
|
|
1533
|
+
const orgHeaders = opts.org ? { 'X-Drafted-Org': opts.org } : {};
|
|
1534
|
+
const id = await resolveSkillId(opts, server, orgHeaders);
|
|
1535
|
+
if (!id) { emitSkillResult(opts.format, { status: 'unresolvable', error: opts.slug ? `slug ${opts.slug} not found` : 'push requires --id or --slug' }); process.exit(1); }
|
|
1536
|
+
let files;
|
|
1537
|
+
try { files = collectSkillTree(opts.dir); }
|
|
1538
|
+
catch (e) { emitSkillResult(opts.format, { status: 'error', error: String((e && e.message) || e) }); process.exit(1); }
|
|
1539
|
+
const res = await authFetch(`${server}/api/skills/${id}/files/bulk`, {
|
|
1540
|
+
method: 'POST', headers: { 'Content-Type': 'application/json', ...orgHeaders },
|
|
1541
|
+
body: JSON.stringify({ files, deleteMissing: !!opts.deleteMissing }),
|
|
1542
|
+
});
|
|
1543
|
+
const data = await res.json().catch(() => ({}));
|
|
1544
|
+
if (!res.ok) { emitSkillResult(opts.format, { status: 'error', id, error: data.error || `HTTP ${res.status}` }); process.exit(1); }
|
|
1545
|
+
if (opts.format === 'json') console.log(JSON.stringify({ status: 'ok', id, hash: data.hash, count: data.count, stripped: data.stripped, files: data.files }));
|
|
1546
|
+
else console.log(`ok\t${id}\t${data.hash}\tpushed ${data.count}${data.stripped ? `, stripped ${data.stripped}` : ''}`);
|
|
1547
|
+
});
|
|
1548
|
+
|
|
1435
1549
|
program.parse();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "drafted",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
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": [
|