drafted 1.8.4 → 1.8.5

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.
Files changed (2) hide show
  1. package/cli/drafted.mjs +227 -0
  2. 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,230 @@ 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
+ async function syncOneSkill(ref, outDir) {
1227
+ const at = ref.lastIndexOf('@');
1228
+ const idOrSlug = at > 0 ? ref.slice(0, at) : ref;
1229
+ const server = getServerUrl().replace(/\/$/, '');
1230
+ const loadUrl = UUID_RE.test(idOrSlug)
1231
+ ? `${server}/api/skills/${idOrSlug}`
1232
+ : `${server}/api/skills/slug/${encodeURIComponent(idOrSlug)}`;
1233
+ const res = await authFetch(loadUrl);
1234
+ if (res.status === 404) return { ref, slug: '', hash: '', status: 'unresolvable', error: 'skill not found' };
1235
+ if (!res.ok) return { ref, slug: '', hash: '', status: 'error', error: `HTTP ${res.status}` };
1236
+ const skill = await res.json();
1237
+ const slug = skill.slug || idOrSlug;
1238
+
1239
+ const files = { 'SKILL.md': synthesizeSkillMd(skill) };
1240
+ for (const p of Array.isArray(skill.files) ? skill.files : []) {
1241
+ if (typeof p !== 'string' || p.includes('..')) continue;
1242
+ const encoded = p.split('/').map(encodeURIComponent).join('/');
1243
+ const fr = await authFetch(`${server}/api/skills/${skill.id}/files/${encoded}`);
1244
+ if (!fr.ok) return { ref, slug, hash: '', status: 'error', error: `file ${p}: HTTP ${fr.status}` };
1245
+ const fdata = await fr.json();
1246
+ files[p] = typeof fdata === 'string' ? fdata : (fdata.content || '');
1247
+ }
1248
+ const hash = bundleHash(files);
1249
+
1250
+ const base = join(outDir, slug);
1251
+ for (const [rel, content] of Object.entries(files)) {
1252
+ const full = join(base, rel);
1253
+ mkdirSync(dirname(full), { recursive: true });
1254
+ writeFileSync(full, content);
1255
+ }
1256
+ return { ref, slug, hash, status: 'ok' };
1257
+ }
1258
+
1259
+ const skillCmd = program.command('skill').description('Skill library operations');
1260
+ skillCmd
1261
+ .command('sync')
1262
+ .description('Materialize pinned skills into a local cache dir (Causeway seam)')
1263
+ .option('--ref <ref>', 'skill ref slug[@hash] (repeatable)', (v, acc) => { acc.push(v); return acc; }, [])
1264
+ .requiredOption('--out <dir>', 'output directory for bundles')
1265
+ .option('--format <fmt>', 'output format: json or text', 'text')
1266
+ .action(async (opts) => {
1267
+ requireLogin();
1268
+ const refs = opts.ref || [];
1269
+ mkdirSync(opts.out, { recursive: true });
1270
+ const results = [];
1271
+ let allOk = refs.length > 0;
1272
+ for (const ref of refs) {
1273
+ try {
1274
+ const r = await syncOneSkill(ref, opts.out);
1275
+ results.push(r);
1276
+ if (r.status !== 'ok') allOk = false;
1277
+ } catch (err) {
1278
+ results.push({ ref, slug: '', hash: '', status: 'error', error: String((err && err.message) || err) });
1279
+ allOk = false;
1280
+ }
1281
+ }
1282
+ if (opts.format === 'json') {
1283
+ console.log(JSON.stringify(results));
1284
+ } else {
1285
+ for (const r of results) {
1286
+ console.log(`${r.status}\t${r.ref}\t${r.slug}\t${r.hash}${r.error ? '\t' + r.error : ''}`);
1287
+ }
1288
+ }
1289
+ process.exit(allOk ? 0 : 1);
1290
+ });
1291
+
1292
+ // ── skill mutations: Causeway write seam ─────────────────────────
1293
+ // add/update/remove mirror the sync seam so the Causeway daemon can create and
1294
+ // improve reusable skills without holding Drafted's token. Each reads its payload
1295
+ // from stdin JSON and prints a {slug,id,hash,status} result line.
1296
+ function readStdinJSON() {
1297
+ let raw = '';
1298
+ try { raw = readFileSync(0, 'utf8'); } catch { raw = ''; }
1299
+ raw = raw.trim();
1300
+ if (!raw) return {};
1301
+ return JSON.parse(raw);
1302
+ }
1303
+
1304
+ function emitSkillResult(format, obj) {
1305
+ if (format === 'json') { console.log(JSON.stringify(obj)); return; }
1306
+ console.log([obj.status, obj.slug || '', obj.id || '', obj.hash || '', obj.error || ''].join('\t'));
1307
+ }
1308
+
1309
+ skillCmd
1310
+ .command('add')
1311
+ .description('Create a skill in Drafted from stdin JSON {name,description,content,tags?,triggerPatterns?}')
1312
+ .option('--format <fmt>', 'output format: json or text', 'text')
1313
+ .action(async (opts) => {
1314
+ requireLogin();
1315
+ let p;
1316
+ try { p = readStdinJSON(); } catch { console.error('invalid JSON on stdin'); process.exit(1); }
1317
+ const server = getServerUrl().replace(/\/$/, '');
1318
+ const res = await authFetch(`${server}/api/skills`, {
1319
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
1320
+ body: JSON.stringify({ name: p.name, description: p.description, content: p.content, tags: p.tags, triggerPatterns: p.triggerPatterns }),
1321
+ });
1322
+ const data = await res.json().catch(() => ({}));
1323
+ if (!res.ok) {
1324
+ emitSkillResult(opts.format, { status: res.status === 409 ? 'conflict' : 'error', error: data.error || `HTTP ${res.status}` });
1325
+ process.exit(1);
1326
+ }
1327
+ emitSkillResult(opts.format, { status: 'ok', slug: data.slug, id: data.id, hash: bundleHash({ 'SKILL.md': synthesizeSkillMd(data) }) });
1328
+ });
1329
+
1330
+ skillCmd
1331
+ .command('update')
1332
+ .description('Update a Drafted skill (--id or --slug) from stdin JSON {description?,content?,triggerPatterns?,tags?}')
1333
+ .option('--id <id>', 'skill id')
1334
+ .option('--slug <slug>', 'skill slug (resolved to id)')
1335
+ .option('--format <fmt>', 'output format: json or text', 'text')
1336
+ .action(async (opts) => {
1337
+ requireLogin();
1338
+ let p;
1339
+ try { p = readStdinJSON(); } catch { console.error('invalid JSON on stdin'); process.exit(1); }
1340
+ const server = getServerUrl().replace(/\/$/, '');
1341
+ let id = opts.id;
1342
+ if (!id && opts.slug) {
1343
+ const lookup = await authFetch(`${server}/api/skills/slug/${encodeURIComponent(opts.slug)}`);
1344
+ if (!lookup.ok) { emitSkillResult(opts.format, { status: 'unresolvable', error: `slug ${opts.slug}: HTTP ${lookup.status}` }); process.exit(1); }
1345
+ id = (await lookup.json().catch(() => ({}))).id;
1346
+ }
1347
+ if (!id) { emitSkillResult(opts.format, { status: 'error', error: 'update requires --id or --slug' }); process.exit(1); }
1348
+ const res = await authFetch(`${server}/api/skills/${id}`, {
1349
+ method: 'PUT', headers: { 'Content-Type': 'application/json' },
1350
+ body: JSON.stringify({ name: p.name, description: p.description, content: p.content, tags: p.tags, triggerPatterns: p.triggerPatterns }),
1351
+ });
1352
+ const data = await res.json().catch(() => ({}));
1353
+ if (!res.ok) {
1354
+ emitSkillResult(opts.format, { status: res.status === 404 ? 'unresolvable' : 'error', error: data.error || `HTTP ${res.status}` });
1355
+ process.exit(1);
1356
+ }
1357
+ emitSkillResult(opts.format, { status: 'ok', slug: data.slug, id: data.id, hash: bundleHash({ 'SKILL.md': synthesizeSkillMd(data) }) });
1358
+ });
1359
+
1360
+ // check reports the current canonical hash for refs WITHOUT materializing — the
1361
+ // Causeway freshness probe. Hash is computed the same way as sync, so a check
1362
+ // hash and a sync hash for the same skill match.
1363
+ async function checkOneSkill(ref) {
1364
+ const at = ref.lastIndexOf('@');
1365
+ const idOrSlug = at > 0 ? ref.slice(0, at) : ref;
1366
+ const server = getServerUrl().replace(/\/$/, '');
1367
+ const loadUrl = UUID_RE.test(idOrSlug)
1368
+ ? `${server}/api/skills/${idOrSlug}`
1369
+ : `${server}/api/skills/slug/${encodeURIComponent(idOrSlug)}`;
1370
+ const res = await authFetch(loadUrl);
1371
+ if (res.status === 404) return { ref, slug: '', hash: '', status: 'unresolvable' };
1372
+ if (!res.ok) return { ref, slug: '', hash: '', status: 'error', error: `HTTP ${res.status}` };
1373
+ const skill = await res.json();
1374
+ const files = { 'SKILL.md': synthesizeSkillMd(skill) };
1375
+ for (const p of Array.isArray(skill.files) ? skill.files : []) {
1376
+ if (typeof p !== 'string' || p.includes('..')) continue;
1377
+ const fr = await authFetch(`${server}/api/skills/${skill.id}/files/${p.split('/').map(encodeURIComponent).join('/')}`);
1378
+ if (!fr.ok) return { ref, slug: skill.slug, hash: '', status: 'error', error: `file ${p}: HTTP ${fr.status}` };
1379
+ const fdata = await fr.json();
1380
+ files[p] = typeof fdata === 'string' ? fdata : (fdata.content || '');
1381
+ }
1382
+ return { ref, slug: skill.slug, hash: bundleHash(files), status: 'ok', updatedAt: skill.updatedAt };
1383
+ }
1384
+
1385
+ skillCmd
1386
+ .command('check')
1387
+ .description('Report the current canonical hash for refs without materializing (freshness)')
1388
+ .option('--ref <ref>', 'skill ref (repeatable)', (v, acc) => { acc.push(v); return acc; }, [])
1389
+ .option('--format <fmt>', 'output format: json or text', 'text')
1390
+ .action(async (opts) => {
1391
+ requireLogin();
1392
+ const results = [];
1393
+ for (const ref of (opts.ref || [])) {
1394
+ try { results.push(await checkOneSkill(ref)); }
1395
+ catch (e) { results.push({ ref, slug: '', hash: '', status: 'error', error: String((e && e.message) || e) }); }
1396
+ }
1397
+ if (opts.format === 'json') console.log(JSON.stringify(results));
1398
+ else for (const r of results) console.log(`${r.status}\t${r.ref}\t${r.slug}\t${r.hash}`);
1399
+ });
1400
+
1401
+ skillCmd
1402
+ .command('remove')
1403
+ .description('Delete a Drafted skill by --id (used to clean up throwaway skills)')
1404
+ .requiredOption('--id <id>', 'skill id')
1405
+ .option('--format <fmt>', 'output format: json or text', 'text')
1406
+ .action(async (opts) => {
1407
+ requireLogin();
1408
+ const server = getServerUrl().replace(/\/$/, '');
1409
+ const res = await authFetch(`${server}/api/skills/${opts.id}`, { method: 'DELETE' });
1410
+ const data = await res.json().catch(() => ({}));
1411
+ if (!res.ok) {
1412
+ emitSkillResult(opts.format, { status: 'error', id: opts.id, error: data.error || `HTTP ${res.status}` });
1413
+ process.exit(1);
1414
+ }
1415
+ emitSkillResult(opts.format, { status: 'ok', id: opts.id });
1416
+ });
1417
+
1191
1418
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drafted",
3
- "version": "1.8.4",
3
+ "version": "1.8.5",
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": [