drafted 1.19.6 → 1.19.7
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 +55 -9
- package/package.json +1 -1
package/mcp/server.mjs
CHANGED
|
@@ -401,7 +401,7 @@ const TOOL_ANNOTATIONS = {
|
|
|
401
401
|
// Minions — checklist-driven intake surfaces bound to a project
|
|
402
402
|
minion: { title: 'Minions', readOnlyHint: false, destructiveHint: true, openWorldHint: false, description: 'Manage Minions: checklist-driven intake surfaces that guide a consumer through a checklist (via a shareable /c/<slug> link) and write a producible into the project. Dispatch by `action`: meta, list, get, create, update, enable, disable, delete. QA your own Minions with test_start/test_say/test_resolve — drive the checklist conversation yourself (works even when disabled). Requires the agent allowlist.' },
|
|
403
403
|
trigger: { title: 'Inbound triggers', readOnlyHint: false, destructiveHint: true, openWorldHint: true, description: 'Manage inbound webhook triggers for the ACTIVE PROJECT: an external system (AppSheet bot, GitHub, form tool) POSTs to the trigger URL and the server runs an agent conversation in the project from the stored prompt template + payload. Dispatch by `action`: create (returns URL + secret token ONCE — relay it to the user immediately, not retrievable later), list, update (enable/disable, edit template, daily limit, executor), rotate (new token), test (fire a synthetic delivery), deliveries (audit log), delete; for executor="queue" triggers, pending/claim/complete let a LOCAL agent poll and work queued deliveries. Requires the agent allowlist.' },
|
|
404
|
-
fs: { title: 'Filesystem', readOnlyHint: false, destructiveHint: true, openWorldHint: false, description: 'Navigate Drafted like a local filesystem: /wiki/<path> pages, /skills/<slug> procedures, /projects/<folder?>/<project>/<layer>/<lane>/<file> frames. Verbs: ls, read, write, edit, mv, rm, search.' },
|
|
404
|
+
fs: { title: 'Filesystem', readOnlyHint: false, destructiveHint: true, openWorldHint: false, description: 'Navigate Drafted like a local filesystem: /wiki/<path> pages, /skills/<slug> procedures, /projects/<folder?>/<project>/<layer>/<lane>/<file> frames. Verbs: ls, read, write, edit, mv, rm, search, link, unlink, links.' },
|
|
405
405
|
repo: { title: 'Git repos', readOnlyHint: false, destructiveHint: true, openWorldHint: true, description: 'Registered git repos — the org index of .agents/ skills + identities. A connected repo is the source of truth for its skills; EVERY repo connected to ANY folder in the org is searchable and usable org-wide (no org-level repo — the union is the library). Skills from connected repos are readable via fs(read, path="/skills/<slug>") — fetched from git at read time, always fresh. Authoring a Drafted skill whose slug collides with a repo-indexed skill returns 409 repo_owned pointing at the repo. Dispatch by `action`: list (paginated, compact mode), add (link a repo to a folder, --branch optional), rescan (re-fetch the tracked branch), entries (search the index). Content stays in git; Drafted keeps a read-only index.' },
|
|
406
406
|
};
|
|
407
407
|
|
|
@@ -413,7 +413,7 @@ function isMutatingToolCall(name, args = {}) {
|
|
|
413
413
|
case 'trigger':
|
|
414
414
|
return ['create', 'update', 'rotate', 'delete'].includes(action);
|
|
415
415
|
case 'fs':
|
|
416
|
-
return !['ls', 'read', 'search'].includes(action);
|
|
416
|
+
return !['ls', 'read', 'search', 'links'].includes(action);
|
|
417
417
|
default:
|
|
418
418
|
return false;
|
|
419
419
|
}
|
|
@@ -2823,10 +2823,11 @@ server.resource('info', 'drafted://info', {
|
|
|
2823
2823
|
};
|
|
2824
2824
|
});
|
|
2825
2825
|
|
|
2826
|
-
tool('fs', 'Navigate Drafted like a local filesystem. An org is the top folder: `fs(ls, path="/")` lists the orgs you can address, then `/o/<org>/<root>/...` addresses one of them — the org is part of the path, there is no org switching:\n\n- `/o/<org>/wiki/<path>` — org knowledge pages (markdown, OKF; free nesting; `index.md` at any level is synthesized and read-only)\n- `/o/<org>/skills/<slug>` — reusable procedures (flat: one dir per skill slug, `SKILL.md` + supporting files inside)\n- `/o/<org>/tasks/<lane?>/<file>` — work items. A task IS a frame: `read` renders its `drafted:status:`/`drafted:assignee:` as front matter and `write`/`edit` parse them back into columns, so they are never stored in the body. The keys are namespaced so an ordinary `status:` in your own front matter is left alone. Valid statuses: open, in_progress, scheduled, needs_review, needs_decision, done, failed (an empty `drafted:status:` clears it). Status is a column, not a location — a task moved out of /tasks stays a task.\n- `/o/<org>/projects/<folder?>/<project>/<layer>/<lane>/<file>` — producible frames (folder optional; then exactly layer → lane → file)\n\n(Bare `/wiki`, `/skills`, `/tasks`, `/projects` roots still resolve via the session\'s working org.)\n\nVerbs: `ls` (list a directory), `read` (file content — hashline-annotated for text so `edit` stays surgical), `write` (create/overwrite; extension + layer classify the type: .html design, .md document, .excalidraw diagram, .xlsx/.docx office, images/videos media, .pdf asset, .google-doc/.google-sheet/.google-slide create native Google Workspace files), `edit` (hashline ops for text, element ops for excalidraw, structured ops for office), `mv` (rename/move, cross-project), `rm` (delete), `search` (frames are searched by label AND content, with the matching line returned as a snippet; `fs(search, path="/")` or `path="/o/<org>"` fans out across wiki + skills + projects in one call). `mkdir` creates a project only: use `/projects/<project>` or `/projects/<folder>/<project>`, never a layer path. To create a layer, write its first frame at `/projects/<project>/<new-layer>/<lane>/<file>`.\n\nThe project is resolved from the path itself — no separate "open" step. Guardrails are server-side and unchanged: the org in the path must be the project\'s own org (project paths under /o/<org>/ validate it), the G1 wiki-search gate fires before project mutations, attached-skill gates fire on mutations, anchored frames must be read before editing a layer, `.skillinstall/` is stripped on skill push.', {
|
|
2827
|
-
action: z.enum(['ls', 'read', 'write', 'edit', 'mv', 'rm', 'mkdir', 'search']).describe('Filesystem verb.'),
|
|
2826
|
+
tool('fs', 'Navigate Drafted like a local filesystem. An org is the top folder: `fs(ls, path="/")` lists the orgs you can address, then `/o/<org>/<root>/...` addresses one of them — the org is part of the path, there is no org switching:\n\n- `/o/<org>/wiki/<path>` — org knowledge pages (markdown, OKF; free nesting; `index.md` at any level is synthesized and read-only)\n- `/o/<org>/skills/<slug>` — reusable procedures (flat: one dir per skill slug, `SKILL.md` + supporting files inside)\n- `/o/<org>/tasks/<lane?>/<file>` — work items. A task IS a frame: `read` renders its `drafted:status:`/`drafted:assignee:` as front matter and `write`/`edit` parse them back into columns, so they are never stored in the body. The keys are namespaced so an ordinary `status:` in your own front matter is left alone. Valid statuses: open, in_progress, scheduled, needs_review, needs_decision, done, failed (an empty `drafted:status:` clears it). Status is a column, not a location — a task moved out of /tasks stays a task.\n- `/o/<org>/projects/<folder?>/<project>/<layer>/<lane>/<file>` — producible frames (folder optional; then exactly layer → lane → file)\n\n(Bare `/wiki`, `/skills`, `/tasks`, `/projects` roots still resolve via the session\'s working org.)\n\nVerbs: `ls` (list a directory), `read` (file content — hashline-annotated for text so `edit` stays surgical), `write` (create/overwrite; extension + layer classify the type: .html design, .md document, .excalidraw diagram, .xlsx/.docx office, images/videos media, .pdf asset, .google-doc/.google-sheet/.google-slide create native Google Workspace files), `edit` (hashline ops for text, element ops for excalidraw, structured ops for office), `mv` (rename/move, cross-project), `rm` (delete), `search` (frames are searched by label AND content, with the matching line returned as a snippet; `fs(search, path="/")` or `path="/o/<org>"` fans out across wiki + skills + projects in one call), `link` / `unlink` / `links` (relate one frame to another frame, to a project, or to an external url — `links` lists a frame\'s edges plus its backlinks, and on a project path lists the tasks linked to that project; a link is stored by ID, so `mv` never breaks it). `mkdir` creates a project only: use `/projects/<project>` or `/projects/<folder>/<project>`, never a layer path. To create a layer, write its first frame at `/projects/<project>/<new-layer>/<lane>/<file>`.\n\nThe project is resolved from the path itself — no separate "open" step. Guardrails are server-side and unchanged: the org in the path must be the project\'s own org (project paths under /o/<org>/ validate it), the G1 wiki-search gate fires before project mutations, attached-skill gates fire on mutations, anchored frames must be read before editing a layer, `.skillinstall/` is stripped on skill push.', {
|
|
2827
|
+
action: z.enum(['ls', 'read', 'write', 'edit', 'mv', 'rm', 'mkdir', 'search', 'link', 'unlink', 'links']).describe('Filesystem verb.'),
|
|
2828
2828
|
path: z.string().describe('Drafted path: /o/<org>/wiki/... | /o/<org>/skills/... | /o/<org>/tasks/... | /o/<org>/projects/... (bare /wiki, /skills, /tasks, /projects also work; for mv: source)'),
|
|
2829
|
-
to: z.string().optional().describe('[mv] destination path'),
|
|
2829
|
+
to: z.string().optional().describe('[mv] destination path; [link/unlink] target path — a frame path, or a project path (/o/<org>/projects/<project>) to link a task to a project'),
|
|
2830
|
+
url: z.string().optional().describe('[link/unlink] external target URL, instead of `to` (a link is internal-by-id OR external-by-url, never both)'),
|
|
2830
2831
|
query: z.string().optional().describe('[search] term to match against names/content'),
|
|
2831
2832
|
content: z.string().optional().describe('[write] inline HTML/markdown/text'),
|
|
2832
2833
|
file_path: z.string().optional().describe('[write] absolute path to a local file to upload (stdio only). Under /projects it uploads bytes (images, PDFs, office files); under /wiki and /skills it reads the file as UTF-8 text, so a markdown file on disk can be written straight to a page or a SKILL.md without pasting it.'),
|
|
@@ -2861,10 +2862,10 @@ tool('fs', 'Navigate Drafted like a local filesystem. An org is the top folder:
|
|
|
2861
2862
|
const toScoped = splitOrgScope(stripUrlOrigin(to));
|
|
2862
2863
|
if (toScoped.error) return err(new Error(toScoped.error));
|
|
2863
2864
|
if (toScoped.org && orgFromPath && toScoped.org.toLowerCase() !== orgFromPath.toLowerCase()) {
|
|
2864
|
-
return err(new Error(
|
|
2865
|
+
return err(new Error(`${action} stays within one org — source and destination must address the same org`));
|
|
2865
2866
|
}
|
|
2866
2867
|
if (toScoped.org && !orgFromPath) {
|
|
2867
|
-
return err(new Error(
|
|
2868
|
+
return err(new Error(`${action} to an org-scoped destination needs an org-scoped source too (/o/<org>/... on both sides)`));
|
|
2868
2869
|
}
|
|
2869
2870
|
to = toScoped.path;
|
|
2870
2871
|
}
|
|
@@ -3180,7 +3181,7 @@ tool('fs', 'Navigate Drafted like a local filesystem. An org is the top folder:
|
|
|
3180
3181
|
// so a task moved out of here stays a task.
|
|
3181
3182
|
const isTasksPath = (x) => x === '/tasks' || x === '/tasks/' || String(x || '').startsWith('/tasks/');
|
|
3182
3183
|
if (isTasksPath(p) || isTasksPath(to)) {
|
|
3183
|
-
if (['write', 'edit', 'mv', 'rm'].includes(action)) {
|
|
3184
|
+
if (['write', 'edit', 'mv', 'rm', 'link', 'unlink'].includes(action)) {
|
|
3184
3185
|
// Project-less: the org cannot be guessed (same guard as /wiki and /skills).
|
|
3185
3186
|
await requireBoundOrgForProjectlessMutation(org || orgFromPath);
|
|
3186
3187
|
}
|
|
@@ -3192,7 +3193,7 @@ tool('fs', 'Navigate Drafted like a local filesystem. An org is the top folder:
|
|
|
3192
3193
|
// `fs(ls, path="/tasks")` creating one would put a real "Tasks" project in
|
|
3193
3194
|
// an org the agent never named (DRAFT-36: org is an address, not a guess).
|
|
3194
3195
|
// Writes are guarded above and may create; reads say "nothing here yet".
|
|
3195
|
-
const readOnly = !['write', 'edit', 'mv', 'rm'].includes(action);
|
|
3196
|
+
const readOnly = !['write', 'edit', 'mv', 'rm', 'link', 'unlink'].includes(action);
|
|
3196
3197
|
const sys = await api('POST', '/api/tasks/project', readOnly ? { create: false } : {}, orgHeader)
|
|
3197
3198
|
.catch((e) => (readOnly && e.status === 404 ? null : Promise.reject(e)));
|
|
3198
3199
|
if (!sys?.id) {
|
|
@@ -3479,6 +3480,51 @@ tool('fs', 'Navigate Drafted like a local filesystem. An org is the top folder:
|
|
|
3479
3480
|
const result = await api('DELETE', `/api/fs/${rmPath.replace(/^\//, '')}`, undefined, orgHeader);
|
|
3480
3481
|
return ok(result);
|
|
3481
3482
|
}
|
|
3483
|
+
case 'link':
|
|
3484
|
+
case 'unlink': {
|
|
3485
|
+
if (!filePath) return err(new Error(`${action} requires a source file path: /projects/<project>/<layer>/<lane>/<file>`));
|
|
3486
|
+
const from = lane ? `/${layer}/${lane}/${filename}` : `/${layer}/${filename}`;
|
|
3487
|
+
const body = { from };
|
|
3488
|
+
if (args.url) {
|
|
3489
|
+
body.url = args.url;
|
|
3490
|
+
} else if (to) {
|
|
3491
|
+
// Same destination grammar as mv: /projects/<ref>/<layer>/... is a
|
|
3492
|
+
// frame in another project, /projects/<ref> alone is the project
|
|
3493
|
+
// itself (the "this task belongs to that project" edge), and a bare
|
|
3494
|
+
// /<layer>/... is a frame in this one.
|
|
3495
|
+
const toClean = String(to).replace(/^\/+|\/+$/g, '');
|
|
3496
|
+
const toParts = toClean.replace(/^projects\/?/, '').split('/').filter(Boolean);
|
|
3497
|
+
if (toClean.startsWith('projects/') && toParts.length === 1) {
|
|
3498
|
+
const toMeta = await resolveProjectRef(toParts[0]).catch(() => null);
|
|
3499
|
+
if (!toMeta?.id) return err(new Error(`project not found: ${toParts[0]}`));
|
|
3500
|
+
body.toType = 'project';
|
|
3501
|
+
body.toId = toMeta.id;
|
|
3502
|
+
} else if (toClean.startsWith('projects/') && toParts.length >= 2) {
|
|
3503
|
+
const toRef = toParts[0];
|
|
3504
|
+
const sameProject = projectRef && String(toRef).toLowerCase() === String(projectRef).toLowerCase();
|
|
3505
|
+
if (!sameProject) {
|
|
3506
|
+
const toMeta = await resolveProjectRef(toRef).catch(() => null);
|
|
3507
|
+
if (!toMeta?.id) return err(new Error(`project not found: ${toRef}`));
|
|
3508
|
+
body.toProjectId = toMeta.id;
|
|
3509
|
+
}
|
|
3510
|
+
body.to = '/' + toParts.slice(1).join('/');
|
|
3511
|
+
} else {
|
|
3512
|
+
body.to = toParts.length ? '/' + toParts.join('/') : String(to);
|
|
3513
|
+
}
|
|
3514
|
+
} else {
|
|
3515
|
+
return err(new Error(`${action} needs a target: to="<frame or project path>" or url="https://..."`));
|
|
3516
|
+
}
|
|
3517
|
+
return ok(await api('POST', `/api/fs/${action}`, body, orgHeader));
|
|
3518
|
+
}
|
|
3519
|
+
case 'links': {
|
|
3520
|
+
// On a file: its edges plus its (access-filtered) backlinks. On a
|
|
3521
|
+
// project path: the tasks linked to that project.
|
|
3522
|
+
if (filePath) {
|
|
3523
|
+
const relPath = lane ? `/${layer}/${lane}/${filename}` : `/${layer}/${filename}`;
|
|
3524
|
+
return ok(await api('GET', `/api/fs/links?path=${encodeURIComponent(relPath)}`, undefined, orgHeader));
|
|
3525
|
+
}
|
|
3526
|
+
return ok(await api('GET', '/api/fs/linked-tasks', undefined, orgHeader));
|
|
3527
|
+
}
|
|
3482
3528
|
case 'search': {
|
|
3483
3529
|
// /api/fs/search has never existed — this 404'd on every call (no fs
|
|
3484
3530
|
// route matches a single `/search` segment). /api/search is the real
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "drafted",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.7",
|
|
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": [
|