drafted 1.19.31 → 1.19.33
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 +53 -10
- package/package.json +1 -1
- package/src/shared/folder-path.mjs +23 -7
package/mcp/server.mjs
CHANGED
|
@@ -331,9 +331,17 @@ export function matchesAllTerms(parts, query) {
|
|
|
331
331
|
// A hit with no snippet matched on the LABEL, not on content — say which, so the
|
|
332
332
|
// agent knows whether the frame's text is evidence or just its filename.
|
|
333
333
|
// Pure + exported (asserted in mcp/test-search-terms.mjs).
|
|
334
|
-
|
|
334
|
+
/**
|
|
335
|
+
* `org` overrides how the org is SPELLED (use the form the caller addressed, so
|
|
336
|
+
* every section of one result reads the same), and `folderOf` supplies the
|
|
337
|
+
* project's folder — frame hits do not carry it, so without this the path drops
|
|
338
|
+
* a segment and stops matching what fs(ls) prints for the same project.
|
|
339
|
+
*/
|
|
340
|
+
export function formatFrameHits(frames, { limit = 25, snippetChars = 200, org = null, folderOf = null } = {}) {
|
|
335
341
|
return frames.slice(0, limit).map((f) => {
|
|
336
|
-
const
|
|
342
|
+
const folder = folderOf ? folderOf(f.projectId) : '';
|
|
343
|
+
const proj = [folder, f.projectSlug || f.projectId].filter(Boolean).join('/');
|
|
344
|
+
const path = `/o/${org || f.orgSlug || f.orgId}/projects/${proj}/${f.layer}/${f.lane || ''}/${f.label}`.replace(/\/\//g, '/');
|
|
337
345
|
const snip = clip(String(f.snippet || '').replace(/\s+/g, ' ').trim(), snippetChars);
|
|
338
346
|
return snip ? ` ${path}\n ${snip}` : ` ${path} (label match)`;
|
|
339
347
|
}).join('\n');
|
|
@@ -1159,8 +1167,16 @@ function workingOrgSource() {
|
|
|
1159
1167
|
// One `ls` level of folders, from the org's folder list. Defined inside the
|
|
1160
1168
|
// closure because `api` is — it carries this session's auth and org scope.
|
|
1161
1169
|
async function listSubfolders(base, prefix, orgHeader) {
|
|
1162
|
-
const rows = await
|
|
1163
|
-
|
|
1170
|
+
const [rows, repos] = await Promise.all([
|
|
1171
|
+
api('GET', '/api/folders', undefined, orgHeader).catch(() => []),
|
|
1172
|
+
// Best-effort: a failure just omits the git-owned note, never breaks ls.
|
|
1173
|
+
api('GET', '/api/repos', undefined, orgHeader).catch(() => null),
|
|
1174
|
+
]);
|
|
1175
|
+
const byFolder = new Map(
|
|
1176
|
+
(Array.isArray(repos?.repos) ? repos.repos : []).map((r) => [r.folderName || '', `${r.slug}@${r.branch}`])
|
|
1177
|
+
);
|
|
1178
|
+
return subfolderEntries(base, prefix, (Array.isArray(rows) ? rows : []).map((r) => r?.name),
|
|
1179
|
+
(name) => byFolder.get(name) || null);
|
|
1164
1180
|
}
|
|
1165
1181
|
|
|
1166
1182
|
async function api(method, path, body, extraHeaders = {}, _retried = false, _orgHealed = false) {
|
|
@@ -3298,7 +3314,7 @@ tool('fs', 'Navigate Drafted like a local filesystem. A FOLDER is the single con
|
|
|
3298
3314
|
}
|
|
3299
3315
|
const scope = orgFromPath ? `/o/${orgFromPath}` : '';
|
|
3300
3316
|
const leg = async (fn) => { try { return { v: await fn() }; } catch (e) { return { e: e?.message || String(e) }; } };
|
|
3301
|
-
const [wiki, skills, projects, frames] = await Promise.all([
|
|
3317
|
+
const [wiki, skills, projects, frames, repos] = await Promise.all([
|
|
3302
3318
|
leg(() => api('GET', `/api/wiki/search?q=${encodeURIComponent(q)}&limit=10`, undefined, orgHeader)),
|
|
3303
3319
|
leg(() => api('GET', `/api/skills/search?q=${encodeURIComponent(q)}`, undefined, orgHeader)),
|
|
3304
3320
|
leg(() => api('GET', '/api/projects', undefined, orgHeader)),
|
|
@@ -3306,6 +3322,9 @@ tool('fs', 'Navigate Drafted like a local filesystem. A FOLDER is the single con
|
|
|
3306
3322
|
// fact — the row cap is applied server-side, so a client-side filter can be
|
|
3307
3323
|
// handed a page already filled by other orgs' hits.
|
|
3308
3324
|
leg(() => withoutProjectScope(() => api('GET', `/api/search?q=${encodeURIComponent(q)}&limit=15`, undefined, orgHeader))),
|
|
3325
|
+
// Which folders git owns, so the result can say which hits are read-only
|
|
3326
|
+
// in Drafted. One cheap org-scoped call; a failure just omits the note.
|
|
3327
|
+
leg(() => api('GET', '/api/repos', undefined, orgHeader)),
|
|
3309
3328
|
]);
|
|
3310
3329
|
// A root search IS the prior-art search both gates ask for — it read the wiki
|
|
3311
3330
|
// and the skill library. Not crediting it would send the agent back to run
|
|
@@ -3340,17 +3359,41 @@ tool('fs', 'Navigate Drafted like a local filesystem. A FOLDER is the single con
|
|
|
3340
3359
|
// filter reused that empty set, it silently zeroed BOTH sections. A
|
|
3341
3360
|
// search that found nothing because of how you spelled the org is exactly
|
|
3342
3361
|
// the invisible false negative this whole guard exists to prevent.
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3362
|
+
// A printed path must BE the address fs(ls) gives for the same thing —
|
|
3363
|
+
// otherwise copying it from a search result lands somewhere else, or
|
|
3364
|
+
// nowhere. Two ways that broke: the project's folder was dropped
|
|
3365
|
+
// (`projects/wrkout-week-reels` for a project fs(ls) shows at
|
|
3366
|
+
// `projects/Marketing/wrkout-week-reels`), and the org was spelled from
|
|
3367
|
+
// the row while wiki/skills used the form the caller typed — one result,
|
|
3368
|
+
// two spellings of one org.
|
|
3369
|
+
const projectRows = (Array.isArray(projects.v?.projects) ? projects.v.projects : [])
|
|
3370
|
+
.filter(x => x.folder !== '__archived');
|
|
3371
|
+
const folderById = new Map(projectRows.map(x => [x.id, x.folder || '']));
|
|
3372
|
+
const orgAs = orgFromPath || null;
|
|
3373
|
+
section('Projects', projects, () => {
|
|
3374
|
+
const hits = projectRows.filter(x => matchesAllTerms([x.name, x.slug, x.description], q)).slice(0, 10);
|
|
3375
|
+
return hits.length
|
|
3376
|
+
? hits.map(x => ` /o/${orgAs || x.orgSlug || x.orgId}/projects/${[x.folder, x.slug || x.id].filter(Boolean).join('/')}`).join('\n')
|
|
3377
|
+
: '';
|
|
3347
3378
|
});
|
|
3348
3379
|
section('Frames', frames, (v) => {
|
|
3349
3380
|
const hits = Array.isArray(v) ? v : (v?.results || []);
|
|
3350
3381
|
const note = archivedNote(Array.isArray(v) ? 0 : (v?.archivedCount || 0));
|
|
3351
|
-
return hits.length
|
|
3382
|
+
return hits.length
|
|
3383
|
+
? formatFrameHits(hits, { limit: 15, org: orgAs, folderOf: (id) => folderById.get(id) || '' }) + note
|
|
3384
|
+
: (note ? note.trimStart() : '');
|
|
3352
3385
|
});
|
|
3353
3386
|
|
|
3387
|
+
// Which of these hits are read-only in Drafted, and precisely which part.
|
|
3388
|
+
// A link makes the folder's WIKI and SKILLS git-owned (409 repo_owned on
|
|
3389
|
+
// write); its TASKS and PROJECTS write normally. Naming the folder alone
|
|
3390
|
+
// would tell an agent not to attempt a frame write it is entitled to make.
|
|
3391
|
+
const linked = (Array.isArray(repos.v?.repos) ? repos.v.repos : [])
|
|
3392
|
+
.map(r => `${r.folderName ? r.folderName : '(org root)'} → ${r.slug}@${r.branch}`);
|
|
3393
|
+
if (linked.length) {
|
|
3394
|
+
out.push(`Git-owned WIKI + SKILLS (edit those via commit; tasks + projects write normally): ${linked.join('; ')}`);
|
|
3395
|
+
}
|
|
3396
|
+
|
|
3354
3397
|
const searched = orgFromPath || org || null;
|
|
3355
3398
|
// ALWAYS say where this looked. "No matches" is only safe to act on when
|
|
3356
3399
|
// the reader can see the scope it was computed over.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "drafted",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.33",
|
|
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": [
|
|
@@ -120,7 +120,17 @@ export function liftFolderScope(path, to) {
|
|
|
120
120
|
* org's folder names (each of which is its full path). One segment deeper only —
|
|
121
121
|
* `ls` shows a level, not a tree.
|
|
122
122
|
*/
|
|
123
|
-
|
|
123
|
+
/** `repoOf(fullFolderName)` -> "slug@branch" when a repo is linked to that
|
|
124
|
+
* folder, else null.
|
|
125
|
+
*
|
|
126
|
+
* SAY WHAT IS OWNED, NOT "THE FOLDER". A link makes the folder's WIKI and
|
|
127
|
+
* SKILLS git-owned — those answer 409 repo_owned on write. Its TASKS and
|
|
128
|
+
* PROJECTS are untouched and write normally (verified: a frame write into a
|
|
129
|
+
* project inside a linked folder succeeds). A bare "GIT-OWNED" on the folder
|
|
130
|
+
* overclaims, and an agent that believes it will not attempt a frame write it
|
|
131
|
+
* is perfectly entitled to make — a false negative costs more here than the
|
|
132
|
+
* 409 it was meant to prevent. */
|
|
133
|
+
export function subfolderEntries(base, prefix, names, repoOf = null) {
|
|
124
134
|
const depth = prefix ? normalizeFolder(prefix).split('/').length : 0;
|
|
125
135
|
const kids = new Set();
|
|
126
136
|
for (const raw of names || []) {
|
|
@@ -130,12 +140,18 @@ export function subfolderEntries(base, prefix, names) {
|
|
|
130
140
|
const segs = name.split('/');
|
|
131
141
|
if (segs.length > depth) kids.add(segs[depth]);
|
|
132
142
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
143
|
+
const full = (name) => normalizeFolder([prefix, name].filter(Boolean).join('/'));
|
|
144
|
+
return [...kids].sort().map((name) => {
|
|
145
|
+
const repo = repoOf ? repoOf(full(name)) : null;
|
|
146
|
+
return {
|
|
147
|
+
name,
|
|
148
|
+
type: 'directory',
|
|
149
|
+
path: `${String(base || '').replace(/\/+$/, '')}/${name}`,
|
|
150
|
+
hint: repo
|
|
151
|
+
? `folder — carries its own wiki, skills, tasks and projects · wiki + skills GIT-OWNED by ${repo} (edit those via commit); tasks + projects write normally`
|
|
152
|
+
: 'folder — carries its own wiki, skills, tasks and projects',
|
|
153
|
+
};
|
|
154
|
+
});
|
|
139
155
|
}
|
|
140
156
|
|
|
141
157
|
/**
|