drafted 1.19.3 → 1.19.5
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 +46 -6
- package/mcp/test-org-guards.mjs +2 -0
- package/package.json +1 -1
package/mcp/server.mjs
CHANGED
|
@@ -257,7 +257,7 @@ export function splitOrgScope(raw) {
|
|
|
257
257
|
if (!/^\/o\//.test(raw || '')) return { path: raw, org: null };
|
|
258
258
|
const m = String(raw).match(/^\/o\/([^/]+)(?:\/(.*))?$/);
|
|
259
259
|
if (!m || !m[1]) {
|
|
260
|
-
return { error: `Invalid org-scoped path: ${raw} — expected /o/<org>/<root>/... where <root> is wiki, skills, or projects` };
|
|
260
|
+
return { error: `Invalid org-scoped path: ${raw} — expected /o/<org>/<root>/... where <root> is wiki, skills, tasks, or projects` };
|
|
261
261
|
}
|
|
262
262
|
return { path: m[2] ? `/${m[2]}` : '/', org: decodeURIComponent(m[1]) };
|
|
263
263
|
}
|
|
@@ -2793,6 +2793,7 @@ server.resource('info', 'drafted://info', {
|
|
|
2793
2793
|
roots: {
|
|
2794
2794
|
wiki: '/o/<org>/wiki/<path>',
|
|
2795
2795
|
skills: '/o/<org>/skills/<slug>',
|
|
2796
|
+
tasks: '/o/<org>/tasks/<lane?>/<file>',
|
|
2796
2797
|
projects: '/o/<org>/projects/<folder?>/<project>/<layer>/<lane>/<file>',
|
|
2797
2798
|
},
|
|
2798
2799
|
tools: ['fs (ls/read/write/edit/mv/rm/mkdir/search)', 'whoami', 'auth', 'session', 'trigger', 'focus', 'tour', 'screenshot', 'minion'],
|
|
@@ -2801,9 +2802,9 @@ server.resource('info', 'drafted://info', {
|
|
|
2801
2802
|
};
|
|
2802
2803
|
});
|
|
2803
2804
|
|
|
2804
|
-
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>/projects/<folder?>/<project>/<layer>/<lane>/<file>` — producible frames (folder optional; then exactly layer → lane → file)\n\n(Bare `/wiki`, `/skills`, `/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.', {
|
|
2805
|
+
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.', {
|
|
2805
2806
|
action: z.enum(['ls', 'read', 'write', 'edit', 'mv', 'rm', 'mkdir', 'search']).describe('Filesystem verb.'),
|
|
2806
|
-
path: z.string().describe('Drafted path: /o/<org>/wiki/... | /o/<org>/skills/... | /o/<org>/projects/... (bare /wiki, /skills, /projects also work; for mv: source)'),
|
|
2807
|
+
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)'),
|
|
2807
2808
|
to: z.string().optional().describe('[mv] destination path'),
|
|
2808
2809
|
query: z.string().optional().describe('[search] term to match against names/content'),
|
|
2809
2810
|
content: z.string().optional().describe('[write] inline HTML/markdown/text'),
|
|
@@ -2910,12 +2911,13 @@ tool('fs', 'Navigate Drafted like a local filesystem. An org is the top folder:
|
|
|
2910
2911
|
|
|
2911
2912
|
return ok(`Search "${q}"${orgFromPath ? ` in /o/${orgFromPath}` : ''}\n\n${out.join('\n\n')}`);
|
|
2912
2913
|
}
|
|
2913
|
-
if (action !== 'ls') return err(new Error('read/write/edit/mv/rm require a path under /o/<org>/wiki, /o/<org>/skills, or /o/<org>/projects'));
|
|
2914
|
+
if (action !== 'ls') return err(new Error('read/write/edit/mv/rm require a path under /o/<org>/wiki, /o/<org>/skills, /o/<org>/tasks, or /o/<org>/projects'));
|
|
2914
2915
|
if (orgFromPath) {
|
|
2915
|
-
// ls /o/<org> → that org's
|
|
2916
|
+
// ls /o/<org> → that org's roots
|
|
2916
2917
|
return ok([
|
|
2917
2918
|
{ name: 'wiki', type: 'directory', path: `/o/${orgFromPath}/wiki`, hint: 'org knowledge pages (markdown, OKF)' },
|
|
2918
2919
|
{ name: 'skills', type: 'directory', path: `/o/${orgFromPath}/skills`, hint: 'reusable procedures (flat: one dir per skill slug)' },
|
|
2920
|
+
{ name: 'tasks', type: 'directory', path: `/o/${orgFromPath}/tasks`, hint: 'work items — a task is a frame; status/assignee are front matter on read' },
|
|
2919
2921
|
{ name: 'projects', type: 'directory', path: `/o/${orgFromPath}/projects`, hint: '<folder?>/<project>/<layer>/<lane>/<file>' },
|
|
2920
2922
|
]);
|
|
2921
2923
|
}
|
|
@@ -3149,6 +3151,44 @@ tool('fs', 'Navigate Drafted like a local filesystem. An org is the top folder:
|
|
|
3149
3151
|
}
|
|
3150
3152
|
}
|
|
3151
3153
|
|
|
3154
|
+
// ── Root: /tasks/... ───────────────────────────────────────────
|
|
3155
|
+
// /tasks is an ADDRESS, not a fourth store. It resolves to a per-org system
|
|
3156
|
+
// project holding a single `tasks` layer, so every verb below is the ordinary
|
|
3157
|
+
// /projects machinery and a task frame has a real project id and a real path.
|
|
3158
|
+
// status/assignee live in COLUMNS (projected as front matter by read/write),
|
|
3159
|
+
// so a task moved out of here stays a task.
|
|
3160
|
+
const isTasksPath = (x) => x === '/tasks' || x === '/tasks/' || String(x || '').startsWith('/tasks/');
|
|
3161
|
+
if (isTasksPath(p) || isTasksPath(to)) {
|
|
3162
|
+
if (['write', 'edit', 'mv', 'rm'].includes(action)) {
|
|
3163
|
+
// Project-less: the org cannot be guessed (same guard as /wiki and /skills).
|
|
3164
|
+
await requireBoundOrgForProjectlessMutation(org || orgFromPath);
|
|
3165
|
+
}
|
|
3166
|
+
if ((action === 'rm' || action === 'mv') && (p === '/tasks' || p === '/tasks/')) {
|
|
3167
|
+
return err(new Error('/tasks is the org task root — it cannot be moved or deleted. Address a task: /o/<org>/tasks/<file> or /o/<org>/tasks/<lane>/<file>'));
|
|
3168
|
+
}
|
|
3169
|
+
// A READ never creates. Resolving the tasks root is a project create, and on
|
|
3170
|
+
// an unbound multi-org session the org here is an inherited cursor — so
|
|
3171
|
+
// `fs(ls, path="/tasks")` creating one would put a real "Tasks" project in
|
|
3172
|
+
// an org the agent never named (DRAFT-36: org is an address, not a guess).
|
|
3173
|
+
// Writes are guarded above and may create; reads say "nothing here yet".
|
|
3174
|
+
const readOnly = !['write', 'edit', 'mv', 'rm'].includes(action);
|
|
3175
|
+
const sys = await api('POST', '/api/tasks/project', readOnly ? { create: false } : {}, orgHeader)
|
|
3176
|
+
.catch((e) => (readOnly && e.status === 404 ? null : Promise.reject(e)));
|
|
3177
|
+
if (!sys?.id) {
|
|
3178
|
+
if (action === 'ls' || action === 'search') {
|
|
3179
|
+
return ok({ path: p, entries: [], totalAvailable: 0, note: 'This org has no tasks yet — writing a task under /tasks creates the root.' });
|
|
3180
|
+
}
|
|
3181
|
+
if (readOnly) return err(new Error(`Not found: ${p} — this org has no tasks yet.`));
|
|
3182
|
+
return err(new Error('could not resolve the tasks project for this org'));
|
|
3183
|
+
}
|
|
3184
|
+
// Seed the cache by id so the /projects branch resolves it without a lookup
|
|
3185
|
+
// (its /api/projects listing is not org-scoped).
|
|
3186
|
+
projectRefCache.set(sys.id, { id: sys.id, slug: sys.slug, name: sys.name, orgId: sys.orgId, orgSlug: sys.orgSlug });
|
|
3187
|
+
const rewrite = (x) => `/projects/${sys.id}/tasks${x === '/tasks' || x === '/tasks/' ? '' : x.slice('/tasks'.length)}`;
|
|
3188
|
+
if (isTasksPath(p)) p = rewrite(p);
|
|
3189
|
+
if (isTasksPath(to)) to = rewrite(to);
|
|
3190
|
+
}
|
|
3191
|
+
|
|
3152
3192
|
// ── Root: /projects/... ────────────────────────────────────────
|
|
3153
3193
|
if (p.startsWith('/projects')) {
|
|
3154
3194
|
const parts = p.replace(/^\/projects\/?/, '').split('/').filter(Boolean);
|
|
@@ -3471,7 +3511,7 @@ tool('fs', 'Navigate Drafted like a local filesystem. An org is the top folder:
|
|
|
3471
3511
|
return meta ? withProjectOverride(meta, run) : run();
|
|
3472
3512
|
}
|
|
3473
3513
|
|
|
3474
|
-
return err(new Error(`unknown fs path: ${rawPath} — expected /o/<org>/wiki/..., /o/<org>/skills/..., or /o/<org>/projects/... (bare /wiki, /skills, /projects still work)`));
|
|
3514
|
+
return err(new Error(`unknown fs path: ${rawPath} — expected /o/<org>/wiki/..., /o/<org>/skills/..., /o/<org>/tasks/..., or /o/<org>/projects/... (bare /wiki, /skills, /tasks, /projects still work)`));
|
|
3475
3515
|
});
|
|
3476
3516
|
|
|
3477
3517
|
// ── repo tool: registered git repos (org substrate, Phase 4) ──────────
|
package/mcp/test-org-guards.mjs
CHANGED
|
@@ -153,6 +153,8 @@ assert.deepEqual(splitOrgScope('/o/acme/projects/design/beoflow/wireframes/x.htm
|
|
|
153
153
|
assert.deepEqual(splitOrgScope('/o/acme'), { path: '/', org: 'acme' }, 'ls /o/<org> lists that org\'s roots');
|
|
154
154
|
assert.deepEqual(splitOrgScope('/o/acme/'), { path: '/', org: 'acme' }, 'trailing slash on the org root is harmless');
|
|
155
155
|
assert.deepEqual(splitOrgScope('/o/acme%20brand/skills'), { path: '/skills', org: 'acme brand' }, 'org segment is URL-decoded');
|
|
156
|
+
assert.deepEqual(splitOrgScope('/o/acme/tasks'), { path: '/tasks', org: 'acme' }, 'the tasks root is a root like any other');
|
|
157
|
+
assert.deepEqual(splitOrgScope('/o/acme/tasks/sprint-1/ship-it.md'), { path: '/tasks/sprint-1/ship-it.md', org: 'acme' }, 'a lane + file under /tasks strips to the bare root + org');
|
|
156
158
|
assert.deepEqual(splitOrgScope('/wiki/engineering'), { path: '/wiki/engineering', org: null }, 'bare root paths pass through untouched');
|
|
157
159
|
assert.deepEqual(splitOrgScope('/projects/x/y/z.html'), { path: '/projects/x/y/z.html', org: null }, 'bare project paths pass through untouched');
|
|
158
160
|
assert.ok(splitOrgScope('/o/').error, 'a bare /o/ is an invalid org-scoped path');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "drafted",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.5",
|
|
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": [
|