drafted 1.2.4 → 1.4.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/mcp/server.mjs +99 -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', 'bulk-write']).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,14 @@ 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'),
|
|
1800
|
+
manifest_path: z.string().optional().describe('[bulk-write] absolute path to a JSON file containing {pages: [{path, title, content, type?, frontmatter?}, ...]}. The MCP wrapper reads the file locally and posts the array to the server in a single request — avoids round-tripping every page through tool args.'),
|
|
1801
|
+
pages: z.array(z.any()).optional().describe('[bulk-write] alternative to manifest_path: pass the pages array inline.'),
|
|
1794
1802
|
}, async (args) => {
|
|
1795
1803
|
try {
|
|
1796
1804
|
const { action } = args;
|
|
@@ -1807,7 +1815,7 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
|
|
|
1807
1815
|
// Ensure the wiki-maintainer skill is attached to this org BEFORE the
|
|
1808
1816
|
// gate check, so the gate fires reliably on the very first wiki call —
|
|
1809
1817
|
// not just after the org has visited /wiki in a browser. Idempotent.
|
|
1810
|
-
const MUTATING = new Set(['write', 'edit', 'mv', 'rm', 'log']);
|
|
1818
|
+
const MUTATING = new Set(['write', 'edit', 'mv', 'rm', 'log', 'source-register', 'bulk-write']);
|
|
1811
1819
|
if (MUTATING.has(action)) {
|
|
1812
1820
|
try { await api('POST', '/api/wiki/_ensure-skill'); } catch { /* non-fatal */ }
|
|
1813
1821
|
const skillErr = await checkOrgSkills(orgId, action);
|
|
@@ -2043,6 +2051,95 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
|
|
|
2043
2051
|
return ok(withOrg({ deleted: true, path: normalized, id: page.id, brokenReferences: broken }));
|
|
2044
2052
|
}
|
|
2045
2053
|
|
|
2054
|
+
// ── bulk-write ──────────────────────────────────────────────
|
|
2055
|
+
// Commit many pages in a single transaction. The MCP wrapper reads
|
|
2056
|
+
// the manifest file locally and posts the pages array inline to
|
|
2057
|
+
// /api/wiki/pages/bulk — avoids paying a tool-call round-trip per
|
|
2058
|
+
// page and keeps the orchestrator's context clean.
|
|
2059
|
+
case 'bulk-write': {
|
|
2060
|
+
const { manifest_path, pages: inlinePages } = args;
|
|
2061
|
+
let pages;
|
|
2062
|
+
if (manifest_path) {
|
|
2063
|
+
if (!existsSync(manifest_path)) throw new Error(`manifest not found: ${manifest_path}`);
|
|
2064
|
+
const text = readFileSync(manifest_path, 'utf8');
|
|
2065
|
+
let parsed;
|
|
2066
|
+
try { parsed = JSON.parse(text); } catch (e) { throw new Error(`invalid JSON in ${manifest_path}: ${e.message}`); }
|
|
2067
|
+
// Accept either {pages: [...]} or a bare array
|
|
2068
|
+
pages = Array.isArray(parsed) ? parsed : parsed.pages;
|
|
2069
|
+
} else if (Array.isArray(inlinePages)) {
|
|
2070
|
+
pages = inlinePages;
|
|
2071
|
+
} else {
|
|
2072
|
+
throw new Error('bulk-write requires either manifest_path or pages array');
|
|
2073
|
+
}
|
|
2074
|
+
if (!Array.isArray(pages) || pages.length === 0) throw new Error('pages array is empty');
|
|
2075
|
+
// Strip surplus fields the server ignores; keep payload tight.
|
|
2076
|
+
const payload = pages.map((p) => ({
|
|
2077
|
+
path: p.path,
|
|
2078
|
+
title: p.title,
|
|
2079
|
+
content: p.content,
|
|
2080
|
+
type: p.type,
|
|
2081
|
+
frontmatter: p.frontmatter,
|
|
2082
|
+
}));
|
|
2083
|
+
const result = await api('POST', '/api/wiki/pages/bulk', { pages: payload });
|
|
2084
|
+
return ok(withOrg({
|
|
2085
|
+
created: result.created?.length ?? 0,
|
|
2086
|
+
updated: result.updated?.length ?? 0,
|
|
2087
|
+
createdPaths: (result.created ?? []).map((r) => r.path),
|
|
2088
|
+
updatedPaths: (result.updated ?? []).map((r) => r.path),
|
|
2089
|
+
errors: result.errors ?? [],
|
|
2090
|
+
}));
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
// ── source-register ────────────────────────────────────────
|
|
2094
|
+
// Register an external source (paper, article, transcript) by content
|
|
2095
|
+
// hash. Idempotent: same hash + same org returns the existing source
|
|
2096
|
+
// with `isNew: false` and the pages already derived from it. This is
|
|
2097
|
+
// the dedup boundary — if isNew=false and derivedPages is non-empty,
|
|
2098
|
+
// the agent should extend those pages, not create new ones.
|
|
2099
|
+
case 'source-register': {
|
|
2100
|
+
const { file_path: srcFile, contentHash, filename, contentType, size } = args;
|
|
2101
|
+
if (!srcFile && !contentHash) throw new Error('file_path or contentHash required for action=source-register');
|
|
2102
|
+
const body = {};
|
|
2103
|
+
if (contentHash) body.contentHash = contentHash;
|
|
2104
|
+
if (filename) body.filename = filename;
|
|
2105
|
+
if (contentType) body.contentType = contentType;
|
|
2106
|
+
if (size !== undefined) body.size = size;
|
|
2107
|
+
if (srcFile && !contentHash) {
|
|
2108
|
+
// Hash locally — server may not be able to read this client's filesystem.
|
|
2109
|
+
if (!existsSync(srcFile)) throw new Error(`file not found: ${srcFile}`);
|
|
2110
|
+
const buf = readFileSync(srcFile);
|
|
2111
|
+
body.contentHash = createHash('sha256').update(buf).digest('hex');
|
|
2112
|
+
body.size = body.size ?? buf.byteLength;
|
|
2113
|
+
body.filename = body.filename ?? basename(srcFile);
|
|
2114
|
+
}
|
|
2115
|
+
const result = await api('POST', '/api/wiki/sources', body);
|
|
2116
|
+
return ok(withOrg(result));
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
// ── source-list ────────────────────────────────────────────
|
|
2120
|
+
case 'source-list': {
|
|
2121
|
+
const { contentHash, limit: srcLimit = 50 } = args;
|
|
2122
|
+
if (contentHash) {
|
|
2123
|
+
try {
|
|
2124
|
+
const result = await api('GET', `/api/wiki/sources?hash=${encodeURIComponent(contentHash)}`);
|
|
2125
|
+
return ok({ sources: [result] });
|
|
2126
|
+
} catch (e) {
|
|
2127
|
+
if (/Not found/.test(e.message)) return ok({ sources: [] });
|
|
2128
|
+
throw e;
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
const result = await api('GET', `/api/wiki/sources?limit=${srcLimit}`);
|
|
2132
|
+
return ok(result);
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
// ── source-get ─────────────────────────────────────────────
|
|
2136
|
+
case 'source-get': {
|
|
2137
|
+
const { sourceId } = args;
|
|
2138
|
+
if (!sourceId) throw new Error('sourceId required for action=source-get');
|
|
2139
|
+
const result = await api('GET', `/api/wiki/sources/${encodeURIComponent(sourceId)}`);
|
|
2140
|
+
return ok(result);
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2046
2143
|
default:
|
|
2047
2144
|
throw new Error(`Unknown wiki action: ${action}`);
|
|
2048
2145
|
}
|