drafted 1.25.0 → 1.25.2

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 CHANGED
@@ -1679,8 +1679,9 @@ program
1679
1679
  // docs/contracts/drafted-skill-sync.md.
1680
1680
  const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1681
1681
 
1682
- // synthesizeSkillMd builds a self-describing SKILL.md. Drafted stores `content`
1683
- // as the body only (name/description/triggerPatterns are columns), so we prepend
1682
+ // synthesizeSkillMd builds a self-describing SKILL.md. name/description/
1683
+ // triggerPatterns are columns, and `content` may still carry the author's own
1684
+ // frontmatter (merged by splitStoredFrontmatter), so we prepend
1684
1685
  // YAML frontmatter — JSON scalars are valid YAML — for Causeway's projection and
1685
1686
  // Claude's native loader to read.
1686
1687
  // normalizeSetup mirrors the server (server/lib/skill-hash.mjs) — keep in lockstep.
@@ -1690,6 +1691,31 @@ function normalizeSetup(setup) {
1690
1691
  return arr.filter((s) => typeof s === 'string' && s.trim().length > 0);
1691
1692
  }
1692
1693
 
1694
+ // Keys synthesizeSkillMd writes from the skill row's columns. The row is the
1695
+ // authority for these, so a copy of one in the stored content is dropped.
1696
+ const COLUMN_KEYS = new Set(['name', 'description', 'triggerPatterns', 'setup', 'type', 'title', 'tags', 'timestamp']);
1697
+
1698
+ // Content written through fs/MCP keeps the author's frontmatter. Emitting it
1699
+ // as-is after the synthesized block gave every such SKILL.md TWO frontmatter
1700
+ // blocks (56 of 267 live skills on 2026-10-24), and loaders read only the
1701
+ // first, so author keys such as a Causeway `chain:` or `allowed-tools` were
1702
+ // lost on export. Split it: the author's other top-level keys, verbatim, join
1703
+ // the one block; the rest is the body. Line-based so the author's formatting
1704
+ // survives byte for byte; content with no frontmatter is returned unchanged.
1705
+ function splitStoredFrontmatter(content) {
1706
+ const text = content || '';
1707
+ const m = /^---\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/.exec(text);
1708
+ if (!m) return { extra: [], body: text };
1709
+ const extra = [];
1710
+ let keep = false;
1711
+ for (const line of m[1].split(/\r?\n/)) {
1712
+ const key = /^([A-Za-z_][\w.-]*)\s*:/.exec(line);
1713
+ if (key) keep = !COLUMN_KEYS.has(key[1]);
1714
+ if (keep) extra.push(line);
1715
+ }
1716
+ return { extra, body: text.slice(m[0].length) };
1717
+ }
1718
+
1693
1719
  function synthesizeSkillMd(skill) {
1694
1720
  const lines = ['---', `name: ${JSON.stringify(skill.name || skill.slug || '')}`];
1695
1721
  if (skill.description) lines.push(`description: ${JSON.stringify(skill.description)}`);
@@ -1719,8 +1745,9 @@ function synthesizeSkillMd(skill) {
1719
1745
  }
1720
1746
  const ts = skill.updatedAt ? new Date(skill.updatedAt) : null;
1721
1747
  if (ts && !Number.isNaN(ts.getTime())) lines.push(`timestamp: ${JSON.stringify(ts.toISOString())}`);
1722
- lines.push('---', '');
1723
- return lines.join('\n') + (skill.content || '');
1748
+ const { extra, body } = splitStoredFrontmatter(skill.content);
1749
+ lines.push(...extra, '---', '');
1750
+ return lines.join('\n') + body;
1724
1751
  }
1725
1752
 
1726
1753
  // bundleHash is a stable content hash over the bundle (sorted path\0content\0),
package/mcp/server.mjs CHANGED
@@ -2952,10 +2952,11 @@ tool('action', {
2952
2952
  actions: z.string().optional().describe('[propose] JSON array of {type,payload} to approve TOGETHER as one chain — e.g. share with three people, then comment tagging them. One screen, one decision. Execution is sequential and stops at the first failure; a sent email cannot be rolled back, so it is never all-or-nothing.'),
2953
2953
  groupId: z.string().optional().describe('[read] The chain to read.'),
2954
2954
  reason: z.string().optional().describe('[propose] One line telling the approver WHY. They see it next to what the action does.'),
2955
+ assignee: z.string().optional().describe('[propose] Whose Approvals inbox it lands in: an org member\'s email or user id. Omit it and it lands in yours (the user this session runs as). It routes, it does not decide who may approve; a send_email can only ever be assigned to you, the mailbox owner. An unknown or non-member assignee is refused with the member list.'),
2955
2956
  limit: z.number().optional().describe('[list] Default 25.'),
2956
2957
  offset: z.number().optional().describe('[list] Default 0.'),
2957
2958
  compact: z.boolean().optional().describe('[list] Identity fields only.'),
2958
- }, async ({ action, type, payload, actions, groupId, reason, limit, offset, compact }) => {
2959
+ }, async ({ action, type, payload, actions, groupId, reason, assignee, limit, offset, compact }) => {
2959
2960
  try {
2960
2961
  if (action === 'propose') {
2961
2962
  let items;
@@ -2970,10 +2971,11 @@ tool('action', {
2970
2971
  const result = await api('POST', '/api/actions', {
2971
2972
  actions: items,
2972
2973
  ...(reason ? { reason: String(reason) } : {}),
2974
+ ...(assignee ? { assignee: String(assignee) } : {}),
2973
2975
  });
2974
2976
  return ok({
2975
2977
  ...result,
2976
- next: 'Proposed. A person must approve this in Drafted before anything is sent — you cannot approve it yourself, and there is no verb here that would let you.',
2978
+ next: `Proposed, and waiting in ${result?.assignee?.name ? result.assignee.name + "'s" : 'the'} Approvals inbox (receipt.url opens it). A person must approve it in Drafted before anything is sent — you cannot approve it yourself, and there is no verb here that would let you.`,
2977
2979
  });
2978
2980
  }
2979
2981
  if (action === 'read') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drafted",
3
- "version": "1.25.0",
3
+ "version": "1.25.2",
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
  "bin": {
@@ -43,3 +43,26 @@ export function parseRootPath(pathname) {
43
43
  path: path,
44
44
  };
45
45
  }
46
+
47
+ /**
48
+ * Org-level views: pages that belong to the ORG, not to a folder, so they take
49
+ * no folder chain and are not roots (a folder may still be named after one).
50
+ * /o/<org>/approvals[/<groupId>] is the Approvals inbox: an approval is
51
+ * org-scoped, and showing it inside some project was the old bug.
52
+ */
53
+ export const ORG_VIEWS = ['approvals'];
54
+
55
+ /**
56
+ * @param {string} pathname
57
+ * @returns {{orgParam: string, root: string, path: string}|null}
58
+ * Same contract as parseRootPath, minus the folder chain. ZERO backslashes,
59
+ * for the same template-literal reason.
60
+ */
61
+ export function parseOrgViewPath(pathname) {
62
+ var re = new RegExp('^/o/([^/]+)/(approvals)(?:/(.*))?$');
63
+ var m = re.exec(String(pathname || ''));
64
+ if (!m) return null;
65
+ var path = m[3] || '';
66
+ while (path.length && path.charAt(path.length - 1) === '/') path = path.slice(0, -1);
67
+ return { orgParam: decodeURIComponent(m[1]), root: m[2], path: path };
68
+ }