drafted 1.11.18 → 1.11.19

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 +41 -19
  2. package/package.json +1 -1
package/mcp/server.mjs CHANGED
@@ -981,25 +981,6 @@ function setMcpActiveProject(projectId, meta = null) {
981
981
  if (meta?.orgId) sess.boundOrgId = meta.orgId;
982
982
  }
983
983
 
984
- // Clear the active project if its orgId no longer matches the current org.
985
- // Catches the silent drift where org switches (via get_org switch, browser
986
- // tab, parallel agent, etc.) leave the MCP session pointing at a project
987
- // the API can no longer see — which surfaces as confusing "Project not
988
- // found" errors on otherwise-valid tool calls.
989
- async function ensureProjectMatchesOrg() {
990
- const s = getState();
991
- const meta = s.projectMeta;
992
- if (!s.projectId || !meta?.orgId) return;
993
- const currentOrgId = await getCurrentOrgId();
994
- if (currentOrgId && currentOrgId !== meta.orgId) {
995
- s.projectId = null;
996
- s.projectMeta = null;
997
- const sess = getSessionState();
998
- sess.activeProjectId = null;
999
- sess.activeProjectMeta = null;
1000
- }
1001
- }
1002
-
1003
984
  // Returns { id, slug, name, orgId } for the project this MCP session most
1004
985
  // recently opened — what frame mutations will actually target. Echoed on
1005
986
  // every mutation so silent cross-project drift is visible.
@@ -1101,6 +1082,36 @@ async function getCurrentOrgContext() {
1101
1082
  } catch { return null; }
1102
1083
  }
1103
1084
 
1085
+ // Project-less mutations (wiki, skills) have no resource to derive the org from.
1086
+ // If the agent never bound an org this session (no project opened, no get_org
1087
+ // switch) and passed no explicit org, refuse to GUESS when the user belongs to
1088
+ // more than one org — otherwise the write silently lands in whatever org the
1089
+ // session happened to inherit (the "wiki write into the wrong org" failure).
1090
+ // Single-org users are unambiguous and proceed untouched. (DRAFT-36 Phase 4.)
1091
+ async function requireBoundOrgForProjectlessMutation(explicitOrg) {
1092
+ if (explicitOrg) return;
1093
+ const sess = getSessionState();
1094
+ if (sess.boundOrgId) return; // bound via project open / get_org switch
1095
+ if (getState().projectId) return; // an active project implies its org
1096
+ let count = sess.cachedOrgCount;
1097
+ if (count == null) {
1098
+ try {
1099
+ const d = await api('GET', '/api/orgs');
1100
+ count = (d.orgs || d || []).length;
1101
+ sess.cachedOrgCount = count;
1102
+ } catch { return; } // can't determine membership — don't block a legit write
1103
+ }
1104
+ if (count > 1) {
1105
+ throw new Error(
1106
+ `Refusing to guess the org: no project is open and no org was bound this ` +
1107
+ `session, but you belong to ${count} orgs — a project-less wiki/skill write ` +
1108
+ `would land in whichever org the session inherited. Call ` +
1109
+ `get_org(action="switch", orgId=...) or project(action="open") first ` +
1110
+ `(skill calls may also pass org=...).`
1111
+ );
1112
+ }
1113
+ }
1114
+
1104
1115
  async function getOrgSkills(orgId) {
1105
1116
  if (!orgId) return [];
1106
1117
  // No cache: gate freshness > the ~5ms HTTP roundtrip. Otherwise an attach
@@ -2886,6 +2897,11 @@ tool('skill', 'Manage the Drafted skill library. Skills are reusable prompts/gui
2886
2897
  }, async (args) => {
2887
2898
  try {
2888
2899
  const { action } = args;
2900
+ // Org-creating/editing skill actions on the project-less surface must have a
2901
+ // bound org (or explicit org=) when the user is multi-org. (DRAFT-36 Phase 4.)
2902
+ if (['add', 'update', 'remove', 'push', 'fork'].includes(action)) {
2903
+ await requireBoundOrgForProjectlessMutation(args.org);
2904
+ }
2889
2905
  switch (action) {
2890
2906
  case 'search': {
2891
2907
  const { query, tags, scope = 'all', limit, offset, compact } = args;
@@ -3120,6 +3136,12 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
3120
3136
  try {
3121
3137
  const { action } = args;
3122
3138
 
3139
+ // Project-less wiki mutations must have a bound org when the user is
3140
+ // multi-org, so a write never silently lands in an inherited org. (Ph4.)
3141
+ if (['write', 'edit', 'mv', 'rm', 'bulk-write'].includes(action)) {
3142
+ await requireBoundOrgForProjectlessMutation(undefined);
3143
+ }
3144
+
3123
3145
  // Resolve org context once for the wiki tool. Each MCP session is bound
3124
3146
  // to one org (parallel agents on different orgs is supported by design).
3125
3147
  const orgCtx = await getCurrentOrgContext();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drafted",
3
- "version": "1.11.18",
3
+ "version": "1.11.19",
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": [