drafted 1.8.5 → 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.
Files changed (2) hide show
  1. package/cli/drafted.mjs +27 -10
  2. package/package.json +1 -1
package/cli/drafted.mjs CHANGED
@@ -1223,15 +1223,29 @@ function bundleHash(files) {
1223
1223
  return h.digest('hex').slice(0, 12);
1224
1224
  }
1225
1225
 
1226
- async function syncOneSkill(ref, outDir) {
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) {
1227
1237
  const at = ref.lastIndexOf('@');
1228
1238
  const idOrSlug = at > 0 ? ref.slice(0, at) : ref;
1229
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 } : {};
1230
1244
  const loadUrl = UUID_RE.test(idOrSlug)
1231
1245
  ? `${server}/api/skills/${idOrSlug}`
1232
1246
  : `${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' };
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) };
1235
1249
  if (!res.ok) return { ref, slug: '', hash: '', status: 'error', error: `HTTP ${res.status}` };
1236
1250
  const skill = await res.json();
1237
1251
  const slug = skill.slug || idOrSlug;
@@ -1240,7 +1254,7 @@ async function syncOneSkill(ref, outDir) {
1240
1254
  for (const p of Array.isArray(skill.files) ? skill.files : []) {
1241
1255
  if (typeof p !== 'string' || p.includes('..')) continue;
1242
1256
  const encoded = p.split('/').map(encodeURIComponent).join('/');
1243
- const fr = await authFetch(`${server}/api/skills/${skill.id}/files/${encoded}`);
1257
+ const fr = await authFetch(`${server}/api/skills/${skill.id}/files/${encoded}`, { headers: orgHeaders });
1244
1258
  if (!fr.ok) return { ref, slug, hash: '', status: 'error', error: `file ${p}: HTTP ${fr.status}` };
1245
1259
  const fdata = await fr.json();
1246
1260
  files[p] = typeof fdata === 'string' ? fdata : (fdata.content || '');
@@ -1262,6 +1276,7 @@ skillCmd
1262
1276
  .description('Materialize pinned skills into a local cache dir (Causeway seam)')
1263
1277
  .option('--ref <ref>', 'skill ref slug[@hash] (repeatable)', (v, acc) => { acc.push(v); return acc; }, [])
1264
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')
1265
1280
  .option('--format <fmt>', 'output format: json or text', 'text')
1266
1281
  .action(async (opts) => {
1267
1282
  requireLogin();
@@ -1271,7 +1286,7 @@ skillCmd
1271
1286
  let allOk = refs.length > 0;
1272
1287
  for (const ref of refs) {
1273
1288
  try {
1274
- const r = await syncOneSkill(ref, opts.out);
1289
+ const r = await syncOneSkill(ref, opts.out, opts.org);
1275
1290
  results.push(r);
1276
1291
  if (r.status !== 'ok') allOk = false;
1277
1292
  } catch (err) {
@@ -1360,21 +1375,22 @@ skillCmd
1360
1375
  // check reports the current canonical hash for refs WITHOUT materializing — the
1361
1376
  // Causeway freshness probe. Hash is computed the same way as sync, so a check
1362
1377
  // hash and a sync hash for the same skill match.
1363
- async function checkOneSkill(ref) {
1378
+ async function checkOneSkill(ref, org) {
1364
1379
  const at = ref.lastIndexOf('@');
1365
1380
  const idOrSlug = at > 0 ? ref.slice(0, at) : ref;
1366
1381
  const server = getServerUrl().replace(/\/$/, '');
1382
+ const orgHeaders = org ? { 'X-Drafted-Org': org } : {};
1367
1383
  const loadUrl = UUID_RE.test(idOrSlug)
1368
1384
  ? `${server}/api/skills/${idOrSlug}`
1369
1385
  : `${server}/api/skills/slug/${encodeURIComponent(idOrSlug)}`;
1370
- const res = await authFetch(loadUrl);
1371
- if (res.status === 404) return { ref, slug: '', hash: '', status: 'unresolvable' };
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) };
1372
1388
  if (!res.ok) return { ref, slug: '', hash: '', status: 'error', error: `HTTP ${res.status}` };
1373
1389
  const skill = await res.json();
1374
1390
  const files = { 'SKILL.md': synthesizeSkillMd(skill) };
1375
1391
  for (const p of Array.isArray(skill.files) ? skill.files : []) {
1376
1392
  if (typeof p !== 'string' || p.includes('..')) continue;
1377
- const fr = await authFetch(`${server}/api/skills/${skill.id}/files/${p.split('/').map(encodeURIComponent).join('/')}`);
1393
+ const fr = await authFetch(`${server}/api/skills/${skill.id}/files/${p.split('/').map(encodeURIComponent).join('/')}`, { headers: orgHeaders });
1378
1394
  if (!fr.ok) return { ref, slug: skill.slug, hash: '', status: 'error', error: `file ${p}: HTTP ${fr.status}` };
1379
1395
  const fdata = await fr.json();
1380
1396
  files[p] = typeof fdata === 'string' ? fdata : (fdata.content || '');
@@ -1386,12 +1402,13 @@ skillCmd
1386
1402
  .command('check')
1387
1403
  .description('Report the current canonical hash for refs without materializing (freshness)')
1388
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')
1389
1406
  .option('--format <fmt>', 'output format: json or text', 'text')
1390
1407
  .action(async (opts) => {
1391
1408
  requireLogin();
1392
1409
  const results = [];
1393
1410
  for (const ref of (opts.ref || [])) {
1394
- try { results.push(await checkOneSkill(ref)); }
1411
+ try { results.push(await checkOneSkill(ref, opts.org)); }
1395
1412
  catch (e) { results.push({ ref, slug: '', hash: '', status: 'error', error: String((e && e.message) || e) }); }
1396
1413
  }
1397
1414
  if (opts.format === 'json') console.log(JSON.stringify(results));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drafted",
3
- "version": "1.8.5",
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": [