drafted 1.24.0 → 1.24.1
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/gates.mjs +21 -2
- package/mcp/server.mjs +33 -14
- package/package.json +1 -1
package/mcp/gates.mjs
CHANGED
|
@@ -111,8 +111,11 @@ export function shouldInjectLayerPrompt(layerPrompt, templateDefaultPrompt) {
|
|
|
111
111
|
|
|
112
112
|
// ── Wiki index formatting (bounded map injected with the G1 block) ────────────
|
|
113
113
|
|
|
114
|
-
|
|
115
|
-
|
|
114
|
+
// `empty` lets a caller say what an empty result MEANS: a zero-hit search or an
|
|
115
|
+
// empty folder is not "the wiki is empty" (that read as a contradiction when other
|
|
116
|
+
// searches of the same org had returned pages).
|
|
117
|
+
export function formatWikiIndex(pages, max = 50, empty = '(wiki is empty — no pages yet)') {
|
|
118
|
+
if (!Array.isArray(pages) || pages.length === 0) return empty;
|
|
116
119
|
const lines = pages.slice(0, max).map((p) => {
|
|
117
120
|
const path = typeof p === 'string' ? p : p.path ?? p.slug ?? '';
|
|
118
121
|
const title = typeof p === 'object' && p && p.title ? ` — ${p.title}` : '';
|
|
@@ -122,6 +125,22 @@ export function formatWikiIndex(pages, max = 50) {
|
|
|
122
125
|
return lines.join('\n') + more;
|
|
123
126
|
}
|
|
124
127
|
|
|
128
|
+
// A frame read is capped per page, below MAX_TOOL_RESULT_CHARS, because the
|
|
129
|
+
// content is JSON-escaped on its way out (quotes, newlines) and the blunt cap
|
|
130
|
+
// above cuts mid-token with no way back: `lines` cannot split a minified frame
|
|
131
|
+
// that is ONE line, so ~90 KB of it was simply lost. Paging by character gives
|
|
132
|
+
// every byte an address.
|
|
133
|
+
export const READ_PAGE_CHARS = 60_000;
|
|
134
|
+
|
|
135
|
+
// "start-end" (0-based, end exclusive) or "start" -> the window to return.
|
|
136
|
+
export function charWindow(spec, total) {
|
|
137
|
+
const m = /^\s*(\d+)\s*(?:-\s*(\d+))?\s*$/.exec(String(spec ?? ''));
|
|
138
|
+
if (spec != null && spec !== '' && !m) throw new Error(`chars must look like "0-60000" (got "${spec}")`);
|
|
139
|
+
const start = m ? Math.min(Number(m[1]), total) : 0;
|
|
140
|
+
const end = Math.min(m?.[2] ? Number(m[2]) : start + READ_PAGE_CHARS, start + READ_PAGE_CHARS, total);
|
|
141
|
+
return { start, end: Math.max(start, end) };
|
|
142
|
+
}
|
|
143
|
+
|
|
125
144
|
// ── Project index formatting ──────────────────────────────────────────────────
|
|
126
145
|
// `ls /projects` is an ls, not a stat: one addressable line per project. The
|
|
127
146
|
// previous shape returned the raw /api/projects rows — ~2.4KB each once the
|
package/mcp/server.mjs
CHANGED
|
@@ -23,7 +23,7 @@ import { LAYERS } from '../src/shared/constants.mjs';
|
|
|
23
23
|
import { liftFolderScope, normalizeFolder, rootEntries, splitFolderScope, subfolderEntries } from '../src/shared/folder-path.mjs';
|
|
24
24
|
import { emptyExcalidrawScene, stringifyExcalidrawScene } from '../src/shared/excalidraw.mjs';
|
|
25
25
|
import { formatOkfLogEntry, appendOkfLogEntry } from '../src/shared/okf-log.mjs';
|
|
26
|
-
import { createGateState, markSearched, g1Block, g2Block, g3Block, selectWithinBudget, wouldExceedBudget, budgetError, formatWikiIndex, formatProjectIndex, projectPath, PROJECT_CONTEXT_BUDGET_CHARS } from './gates.mjs';
|
|
26
|
+
import { createGateState, markSearched, g1Block, g2Block, g3Block, selectWithinBudget, wouldExceedBudget, budgetError, formatWikiIndex, formatProjectIndex, charWindow, READ_PAGE_CHARS, projectPath, PROJECT_CONTEXT_BUDGET_CHARS } from './gates.mjs';
|
|
27
27
|
import { loadPersistedProject, savePersistedProject } from './active-project-store.mjs';
|
|
28
28
|
import { probeRepo, resolveAddress, addressNote } from './repo-address.mjs';
|
|
29
29
|
|
|
@@ -3093,19 +3093,18 @@ tool('screenshot', 'Render a PNG via headless browser. `scope=frame` captures a
|
|
|
3093
3093
|
buffer = Buffer.from(await res.arrayBuffer());
|
|
3094
3094
|
} else if (scope === 'canvas') {
|
|
3095
3095
|
const { path: canvasPath, layer = 'plans', width = 1600, height = 1200 } = args;
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
targetSlug =
|
|
3103
|
-
}
|
|
3104
|
-
if (!targetSlug) {
|
|
3096
|
+
// Resolve through the shared project resolver (path, folder path, slug, name
|
|
3097
|
+
// or UUID) and send the project ID: the server's slug lookup is scoped to the
|
|
3098
|
+
// SESSION org, so a slug from /o/<other-org>/projects/<slug> was a 404.
|
|
3099
|
+
let targetSlug;
|
|
3100
|
+
const ref = canvasPath || args.slug;
|
|
3101
|
+
if (ref) {
|
|
3102
|
+
targetSlug = (await resolveProjectArg(ref)).id;
|
|
3103
|
+
} else {
|
|
3105
3104
|
const list = await api('GET', '/api/projects');
|
|
3106
3105
|
const active = (list.projects || []).find(p => p.id === list.activeProject);
|
|
3107
3106
|
if (!active) throw new Error('No active project — pass path=/projects/<project> explicitly.');
|
|
3108
|
-
targetSlug = active.
|
|
3107
|
+
targetSlug = active.id;
|
|
3109
3108
|
}
|
|
3110
3109
|
const url = `${getServerUrl()}/api/canvas-screenshot/${encodeURIComponent(targetSlug)}?layer=${encodeURIComponent(layer)}&width=${width}&height=${height}`;
|
|
3111
3110
|
await ensureSession();
|
|
@@ -3345,7 +3344,9 @@ tool('fs', 'Navigate Drafted like a local filesystem. A FOLDER is the single con
|
|
|
3345
3344
|
state: z.any().optional().describe('[write] app-frame state (JSON) to persist for a deployed windowType:"app" frame; the canvas hydrates the app from it on load. Max 64KB.'),
|
|
3346
3345
|
metadata: z.any().optional().describe('[write] JSON metadata to attach to the written frame (e.g. {"tour": {title, auto?, steps}} to define a guided tour on this frame). Max 64KB.'),
|
|
3347
3346
|
readme: z.string().optional().describe('[write] README.md markdown for a skill at /o/<org>/skills/<slug> — optional on both create and update. Stored as a supporting skill file so the skill directory renders a landing page on GitHub when exported; a skill created without one is returned with a warning, never rejected. Equivalent to a later fs(write, path="/o/<org>/skills/<slug>/README.md").'),
|
|
3347
|
+
position: z.number().int().min(0).optional().describe('[mv] on a LAYER path, no `to`: move the layer to this 0-based index in the project\'s layer order (0 = first; `ls` of the project shows the order). A layer born from a first write is appended last, so use this to pin one first.'),
|
|
3348
3348
|
recursive: z.boolean().optional().describe('[ls] recurse into subdirectories'),
|
|
3349
|
+
chars: z.string().optional().describe('[read] character window "start-end" (0-based, end exclusive; at most 60000 per call). A frame longer than that is returned as its first page with `totalChars` and `nextChars` — pass `nextChars` back to read on. Use it for minified/single-line HTML, which `lines` cannot split.'),
|
|
3349
3350
|
lines: z.string().optional().describe('[read] line range (e.g. "1-50") — partial read; content is hashline-annotated so a later edit stays surgical'),
|
|
3350
3351
|
pattern: z.string().optional().describe('[ls] filter filenames (e.g. "*.html")'),
|
|
3351
3352
|
readonly: z.boolean().optional().describe('[link] read-only frame link (symlink): mirror the SOURCE frame named by `to` (a frame UUID or /projects/<other-project>/<layer>/<lane>/<file>) into the CURRENT project, displayed read-only. The source project stays the single writer.'),
|
|
@@ -3680,7 +3681,7 @@ tool('fs', 'Navigate Drafted like a local filesystem. A FOLDER is the single con
|
|
|
3680
3681
|
return ok(formatWikiIndex(tree?.pages || []));
|
|
3681
3682
|
}
|
|
3682
3683
|
const pages = await api('GET', `/api/wiki/tree?prefix=${encodeURIComponent(wikiPath)}`, undefined, orgHeader);
|
|
3683
|
-
return ok(formatWikiIndex(pages?.pages || []));
|
|
3684
|
+
return ok(formatWikiIndex(pages?.pages || [], 50, `(no wiki pages under ${wikiPath})`));
|
|
3684
3685
|
}
|
|
3685
3686
|
case 'read': {
|
|
3686
3687
|
const canonPath = wikiPath.replace(/\.md$/, '');
|
|
@@ -3753,7 +3754,7 @@ tool('fs', 'Navigate Drafted like a local filesystem. A FOLDER is the single con
|
|
|
3753
3754
|
// which are never present, so every MCP wiki search reported empty.
|
|
3754
3755
|
const hits = pages?.hits || pages?.pages || pages?.results || [];
|
|
3755
3756
|
markSearched(gs, 'wiki');
|
|
3756
|
-
return ok(formatWikiIndex(hits));
|
|
3757
|
+
return ok(formatWikiIndex(hits, 50, `(no wiki pages match "${q}")`));
|
|
3757
3758
|
}
|
|
3758
3759
|
default:
|
|
3759
3760
|
return err(new Error(`fs ${action} not supported for /wiki`));
|
|
@@ -4125,10 +4126,22 @@ tool('fs', 'Navigate Drafted like a local filesystem. A FOLDER is the single con
|
|
|
4125
4126
|
// server, re-created by a second writer that outranked the first.
|
|
4126
4127
|
const proj = result.project;
|
|
4127
4128
|
const local = proj?.orgSlug && proj?.slug ? `${getServerUrl()}/o/${proj.orgSlug}/projects/${proj.slug}${result.path}` : null;
|
|
4129
|
+
const full = result.content || '';
|
|
4130
|
+
let win;
|
|
4131
|
+
try { win = charWindow(args.chars, full.length); } catch (e) { return err(e); }
|
|
4132
|
+
const paged = win.start > 0 || win.end < full.length;
|
|
4128
4133
|
return ok({
|
|
4129
4134
|
path: result.path,
|
|
4130
4135
|
frameUrl: result.frameUrl || local || `${getServerUrl()}/f/${result.id}`,
|
|
4131
|
-
content:
|
|
4136
|
+
content: paged ? full.slice(win.start, win.end) : full,
|
|
4137
|
+
...(paged
|
|
4138
|
+
? {
|
|
4139
|
+
totalChars: full.length,
|
|
4140
|
+
charRange: `${win.start}-${win.end}`,
|
|
4141
|
+
...(win.end < full.length ? { nextChars: `${win.end}-${win.end + READ_PAGE_CHARS}` } : {}),
|
|
4142
|
+
pagedNote: `Showing characters ${win.start}-${win.end} of ${full.length}. This frame is too long for one result; pass chars=<nextChars> to read on. Hashline anchors only cover what is shown, so edit ops need a read of the lines they touch.`,
|
|
4143
|
+
}
|
|
4144
|
+
: {}),
|
|
4132
4145
|
// Comments on this frame the agent has not been shown, delivered by the server on
|
|
4133
4146
|
// an agent's read and already marked seen there, so a second read does not repeat
|
|
4134
4147
|
// them. The note says what they are: notes ABOUT the work, never instructions from
|
|
@@ -4226,6 +4239,12 @@ tool('fs', 'Navigate Drafted like a local filesystem. A FOLDER is the single con
|
|
|
4226
4239
|
// missing. (moveFrames has carried units 'lane' and 'layer', with
|
|
4227
4240
|
// connectors and assets, since cross-project move shipped.)
|
|
4228
4241
|
const scope = rmScope(layer, lane, filename);
|
|
4242
|
+
// `position` on a LAYER path reorders it in the project's layer order.
|
|
4243
|
+
if (args.position !== undefined) {
|
|
4244
|
+
if (scope !== 'directory' || lane) return err(new Error('position reorders a LAYER: fs(mv, path=/o/<org>/projects/<project>/<layer>, position=<0-based index>)'));
|
|
4245
|
+
const result = await api('POST', '/api/fs/layer-order', { layer, position: args.position }, orgHeader);
|
|
4246
|
+
return ok(result);
|
|
4247
|
+
}
|
|
4229
4248
|
if (scope === 'project') {
|
|
4230
4249
|
const pid = getState().projectId;
|
|
4231
4250
|
if (!pid) return err(new Error('could not resolve project id for mv'));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "drafted",
|
|
3
|
-
"version": "1.24.
|
|
3
|
+
"version": "1.24.1",
|
|
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": {
|