drafted 1.19.33 → 1.19.35

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.
Files changed (2) hide show
  1. package/mcp/server.mjs +48 -14
  2. package/package.json +1 -1
package/mcp/server.mjs CHANGED
@@ -3275,8 +3275,8 @@ tool('fs', 'Navigate Drafted like a local filesystem. A FOLDER is the single con
3275
3275
  // Name the org, or don't search. See searchNeedsOrg: an unaddressed search
3276
3276
  // returns "no matches" from ONE org and reads as "nowhere", which is the
3277
3277
  // one wrong answer a search can give that nothing announces.
3278
+ const orgs = await getOrgList();
3278
3279
  {
3279
- const orgs = await getOrgList();
3280
3280
  if (searchNeedsOrg({ explicitOrg: org, orgFromPath, orgCount: orgs.length })) {
3281
3281
  // Work is almost always org-specific, and this session usually already
3282
3282
  // knows WHICH — a binding, an open project, or the linked repo of the
@@ -3312,7 +3312,21 @@ tool('fs', 'Navigate Drafted like a local filesystem. A FOLDER is the single con
3312
3312
  ));
3313
3313
  }
3314
3314
  }
3315
- const scope = orgFromPath ? `/o/${orgFromPath}` : '';
3315
+ // THE CANONICAL SLUG, not the spelling the caller used. /o/beoflow resolves
3316
+ // (the org NAME is a resolution rung alongside id and slug) but its slug is
3317
+ // beoflow-mnczl0zt — and fs(ls) already prints that. Echoing the input
3318
+ // instead made ls and search disagree about the address of one thing, which
3319
+ // is the same defect as dropping the folder segment, one axis over.
3320
+ // Computed HERE, before any section renders: section() runs its renderer
3321
+ // immediately, so a later assignment would leave wiki + skills on the old
3322
+ // spelling while projects + frames used the new one.
3323
+ const addressed = orgFromPath || org || null;
3324
+ const canonicalOrg = addressed
3325
+ ? (orgs.find(o => o.id === addressed
3326
+ || (o.slug || '').toLowerCase() === String(addressed).toLowerCase()
3327
+ || (o.name || '').toLowerCase() === String(addressed).toLowerCase())?.slug || addressed)
3328
+ : null;
3329
+ const scope = canonicalOrg ? `/o/${canonicalOrg}` : '';
3316
3330
  const leg = async (fn) => { try { return { v: await fn() }; } catch (e) { return { e: e?.message || String(e) }; } };
3317
3331
  const [wiki, skills, projects, frames, repos] = await Promise.all([
3318
3332
  leg(() => api('GET', `/api/wiki/search?q=${encodeURIComponent(q)}&limit=10`, undefined, orgHeader)),
@@ -3339,16 +3353,38 @@ tool('fs', 'Navigate Drafted like a local filesystem. A FOLDER is the single con
3339
3353
  out.push(body ? `${title}:\n${body}` : `${title}: no matches`);
3340
3354
  };
3341
3355
 
3356
+ // PER-HIT, not a footer. Whether a wiki page or skill is git-owned depends
3357
+ // on the FOLDER it sits in, and two hits with the same-shaped path can
3358
+ // differ: /o/<org>/wiki/x is read-only in an org whose root is linked and
3359
+ // writable in one whose root is not. A summary line at the bottom makes
3360
+ // the reader cross-reference; the mark belongs on the line it describes.
3361
+ const repoByFolder = new Map(
3362
+ (Array.isArray(repos.v?.repos) ? repos.v.repos : []).map(r => [r.folderName || '', `${r.slug}@${r.branch}`])
3363
+ );
3364
+ const gitMark = (folder) => {
3365
+ const r = repoByFolder.get(folder ?? '');
3366
+ return r ? ` [git: ${r} — edit via commit]` : '';
3367
+ };
3368
+
3342
3369
  section('Wiki', wiki, (v) => {
3343
3370
  const hits = (v?.hits || v?.pages || v?.results || []).slice(0, 10);
3344
- return hits.length ? hits.map(h => ` ${scope}${h.folder ? `/${h.folder}` : ''}/wiki/${h.path}${h.title ? ` — ${clip(h.title, 80)}` : ''}`).join('\n') : '';
3371
+ return hits.length ? hits.map(h => ` ${scope}${h.folder ? `/${h.folder}` : ''}/wiki/${h.path}${h.title ? ` — ${clip(h.title, 80)}` : ''}${gitMark(h.folder)}`).join('\n') : '';
3345
3372
  });
3346
3373
  section('Skills', skills, (v) => {
3347
3374
  // Fewer than the other legs on purpose: skills search is fuzzy and its tail
3348
3375
  // is weak matches, which is noise in a fan-out whose job is "where does this
3349
3376
  // live?". fs(search, path="/skills") is the place to go deeper.
3350
3377
  const list = (Array.isArray(v) ? v : (v?.skills || [])).slice(0, 5);
3351
- return list.length ? list.map(s => ` ${scope}/skills/${s.slug} — ${clip(s.description || s.name, 120)}`.trimEnd()).join('\n') : '';
3378
+ return list.length ? list.map(s => {
3379
+ // A repo-indexed hit IS git-owned by construction — it was read out of
3380
+ // a repo index, so it needs no folder lookup. Its branch comes from the
3381
+ // repos leg; falling back to the bare slug keeps the mark honest if
3382
+ // that leg failed.
3383
+ const mark = s.repoEntry
3384
+ ? ` [git: ${[...repoByFolder.values()].find(v2 => v2.startsWith(`${s.repoSlug}@`)) || s.repoSlug} — edit via commit]`
3385
+ : gitMark(s.folder);
3386
+ return ` ${scope}/skills/${s.slug} — ${clip(s.description || s.name, 120)}`.trimEnd() + mark;
3387
+ }).join('\n') : '';
3352
3388
  });
3353
3389
 
3354
3390
  // NO client-side org filter on either leg. Both endpoints scope server-side
@@ -3369,7 +3405,7 @@ tool('fs', 'Navigate Drafted like a local filesystem. A FOLDER is the single con
3369
3405
  const projectRows = (Array.isArray(projects.v?.projects) ? projects.v.projects : [])
3370
3406
  .filter(x => x.folder !== '__archived');
3371
3407
  const folderById = new Map(projectRows.map(x => [x.id, x.folder || '']));
3372
- const orgAs = orgFromPath || null;
3408
+ const orgAs = canonicalOrg;
3373
3409
  section('Projects', projects, () => {
3374
3410
  const hits = projectRows.filter(x => matchesAllTerms([x.name, x.slug, x.description], q)).slice(0, 10);
3375
3411
  return hits.length
@@ -3384,17 +3420,15 @@ tool('fs', 'Navigate Drafted like a local filesystem. A FOLDER is the single con
3384
3420
  : (note ? note.trimStart() : '');
3385
3421
  });
3386
3422
 
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('; ')}`);
3423
+ // No summary footer: every affected line now carries its own mark, and two
3424
+ // mechanisms saying the same thing is one that can disagree. The note below
3425
+ // is the part a per-hit mark cannot express — that the marks are about
3426
+ // wiki/skills only, so a project hit under a linked folder is still writable.
3427
+ if (repoByFolder.size) {
3428
+ out.push('Marked [git: …] = wiki/skill owned by that repo; edit via commit. Projects and tasks in the same folder write normally.');
3395
3429
  }
3396
3430
 
3397
- const searched = orgFromPath || org || null;
3431
+ const searched = canonicalOrg;
3398
3432
  // ALWAYS say where this looked. "No matches" is only safe to act on when
3399
3433
  // the reader can see the scope it was computed over.
3400
3434
  return ok(`Search "${q}" in ${searched ? `/o/${searched}` : 'your only org'}\n\n${out.join('\n\n')}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drafted",
3
- "version": "1.19.33",
3
+ "version": "1.19.35",
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": [