drafted 1.2.3 → 1.3.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.
Files changed (2) hide show
  1. package/mcp/server.mjs +62 -2
  2. package/package.json +1 -1
package/mcp/server.mjs CHANGED
@@ -1772,7 +1772,7 @@ tool('skill', 'Manage the Drafted skill library. Skills are reusable prompts/gui
1772
1772
  // skill gate; mutations require org-level wiki-maintainer skills loaded.
1773
1773
 
1774
1774
  tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and other agents/humans share maintenance — every edit broadcasts live, and edits from others appear in `recent` and on `read`.\n\nBefore mutating: check `recent` and `search` for relevant existing pages. Before mv/rm: check `links` (or pass `dryRun=true`). After completing a logical session of work, append a `log` entry.\n\nThe tool handles bookkeeping you\'d otherwise forget: `mv` rewrites inbound references via the link index, `read` shows who edited last and when. Use `health` to find unlinked pages and broken links.\n\n**Skill gate:** the org may attach a `wiki-maintainer` skill that you MUST load before mutations. If you get a skill-gate error, run skill(action="load", skill="wiki-maintainer") then retry.', {
1775
- action: z.enum(['ls', 'recent', 'read', 'search', 'links', 'log', 'health', 'write', 'edit', 'mv', 'rm']).describe('Operation to perform.'),
1775
+ action: z.enum(['ls', 'recent', 'read', 'search', 'links', 'log', 'health', 'write', 'edit', 'mv', 'rm', 'source-register', 'source-list', 'source-get']).describe('Operation to perform.'),
1776
1776
  path: z.string().optional().describe('[ls|read|links] wiki path. For ls: default / (root). For read: required. For links: required.'),
1777
1777
  recursive: z.boolean().optional().describe('[ls] list recursively with depth indicators'),
1778
1778
  limit: z.number().optional().describe('[recent|search] max results (recent default 10, search default 25)'),
@@ -1791,6 +1791,12 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
1791
1791
  from: z.string().optional().describe('[mv] source path'),
1792
1792
  to: z.string().optional().describe('[mv] destination path'),
1793
1793
  dryRun: z.boolean().optional().describe('[mv|rm] preview impact without applying changes'),
1794
+ file_path: z.string().optional().describe('[source-register] absolute path to a local file. Server hashes it and registers the source. stdio MCP only.'),
1795
+ contentHash: z.string().optional().describe('[source-register|source-list] hex-encoded SHA-256 (64 chars). Use when the client already hashed the bytes (HTTP MCP).'),
1796
+ filename: z.string().optional().describe('[source-register] original filename for the source (informational)'),
1797
+ contentType: z.string().optional().describe('[source-register] MIME type (informational)'),
1798
+ size: z.number().optional().describe('[source-register] byte size (informational)'),
1799
+ sourceId: z.string().optional().describe('[source-get] source ID returned from source-register'),
1794
1800
  }, async (args) => {
1795
1801
  try {
1796
1802
  const { action } = args;
@@ -1804,8 +1810,12 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
1804
1810
  const withOrg = (result) => ({ ...result, org: orgCtx });
1805
1811
 
1806
1812
  // ── Skill gate: all mutation actions ──────────────────────────
1807
- const MUTATING = new Set(['write', 'edit', 'mv', 'rm', 'log']);
1813
+ // Ensure the wiki-maintainer skill is attached to this org BEFORE the
1814
+ // gate check, so the gate fires reliably on the very first wiki call —
1815
+ // not just after the org has visited /wiki in a browser. Idempotent.
1816
+ const MUTATING = new Set(['write', 'edit', 'mv', 'rm', 'log', 'source-register']);
1808
1817
  if (MUTATING.has(action)) {
1818
+ try { await api('POST', '/api/wiki/_ensure-skill'); } catch { /* non-fatal */ }
1809
1819
  const skillErr = await checkOrgSkills(orgId, action);
1810
1820
  if (skillErr) return err(new Error(skillErr));
1811
1821
  }
@@ -2039,6 +2049,56 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
2039
2049
  return ok(withOrg({ deleted: true, path: normalized, id: page.id, brokenReferences: broken }));
2040
2050
  }
2041
2051
 
2052
+ // ── source-register ────────────────────────────────────────
2053
+ // Register an external source (paper, article, transcript) by content
2054
+ // hash. Idempotent: same hash + same org returns the existing source
2055
+ // with `isNew: false` and the pages already derived from it. This is
2056
+ // the dedup boundary — if isNew=false and derivedPages is non-empty,
2057
+ // the agent should extend those pages, not create new ones.
2058
+ case 'source-register': {
2059
+ const { file_path: srcFile, contentHash, filename, contentType, size } = args;
2060
+ if (!srcFile && !contentHash) throw new Error('file_path or contentHash required for action=source-register');
2061
+ const body = {};
2062
+ if (contentHash) body.contentHash = contentHash;
2063
+ if (filename) body.filename = filename;
2064
+ if (contentType) body.contentType = contentType;
2065
+ if (size !== undefined) body.size = size;
2066
+ if (srcFile && !contentHash) {
2067
+ // Hash locally — server may not be able to read this client's filesystem.
2068
+ if (!existsSync(srcFile)) throw new Error(`file not found: ${srcFile}`);
2069
+ const buf = readFileSync(srcFile);
2070
+ body.contentHash = createHash('sha256').update(buf).digest('hex');
2071
+ body.size = body.size ?? buf.byteLength;
2072
+ body.filename = body.filename ?? basename(srcFile);
2073
+ }
2074
+ const result = await api('POST', '/api/wiki/sources', body);
2075
+ return ok(withOrg(result));
2076
+ }
2077
+
2078
+ // ── source-list ────────────────────────────────────────────
2079
+ case 'source-list': {
2080
+ const { contentHash, limit: srcLimit = 50 } = args;
2081
+ if (contentHash) {
2082
+ try {
2083
+ const result = await api('GET', `/api/wiki/sources?hash=${encodeURIComponent(contentHash)}`);
2084
+ return ok({ sources: [result] });
2085
+ } catch (e) {
2086
+ if (/Not found/.test(e.message)) return ok({ sources: [] });
2087
+ throw e;
2088
+ }
2089
+ }
2090
+ const result = await api('GET', `/api/wiki/sources?limit=${srcLimit}`);
2091
+ return ok(result);
2092
+ }
2093
+
2094
+ // ── source-get ─────────────────────────────────────────────
2095
+ case 'source-get': {
2096
+ const { sourceId } = args;
2097
+ if (!sourceId) throw new Error('sourceId required for action=source-get');
2098
+ const result = await api('GET', `/api/wiki/sources/${encodeURIComponent(sourceId)}`);
2099
+ return ok(result);
2100
+ }
2101
+
2042
2102
  default:
2043
2103
  throw new Error(`Unknown wiki action: ${action}`);
2044
2104
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drafted",
3
- "version": "1.2.3",
3
+ "version": "1.3.0",
4
4
  "description": "Drafted CLI - Design preview server for Claude agents",
5
5
  "type": "module",
6
6
  "files": [