drafted 1.19.11 → 1.19.16

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 CHANGED
@@ -198,6 +198,17 @@ function scrubLocalPathMentions(description) {
198
198
  // inside the factory so each HTTP request gets its own isolated server.
199
199
  // Stdio mode uses the `mcpServer` singleton (built once at module load).
200
200
 
201
+ // What an fs(rm) on a project path actually targets. A layer or lane path carries
202
+ // no filename, so before this predicate existed it fell through to the "no file
203
+ // path = archive the project" branch and archived the WHOLE project — returning a
204
+ // normal-looking success. That is how "Ai For Coaches Talk" was lost to an
205
+ // fs(rm) on /slides/deck. Only the bare project path may archive.
206
+ export function rmScope(layer, lane, filename) {
207
+ if (filename) return 'file';
208
+ if (layer || lane) return 'directory';
209
+ return 'project';
210
+ }
211
+
201
212
  // ── Org-ambiguity policy (the one decision core) ─────────────────
202
213
  // The org guard inside the factory plumbs session/HTTP state into this
203
214
  // side-effect-free predicate, which IS the policy (DRAFT-36 "one rule"). Top-level
@@ -3615,7 +3626,19 @@ tool('fs', 'Navigate Drafted like a local filesystem. An org is the top folder:
3615
3626
  // __archived folder. Frame/lane rm still deletes those items, but a
3616
3627
  // whole project is never hard-deleted by an agent — the web UI's
3617
3628
  // Archive bin is where a human permanently deletes.
3618
- if (!filePath) {
3629
+ // A layer or lane path has no filename, so it used to fall through to
3630
+ // the archive branch and archive the WHOLE project — with a
3631
+ // success-shaped response. /slides/deck and /slides are one keystroke
3632
+ // from the project path; only the bare project path may archive.
3633
+ const scope = rmScope(layer, lane, filename);
3634
+ if (scope === 'directory') {
3635
+ return err(new Error(
3636
+ `rm on a ${lane ? 'lane' : 'layer'} path deletes nothing — Drafted has no directory delete. ` +
3637
+ `Remove the frames individually: /projects/<project>/${layer}${lane ? '/' + lane : ''}/<file>. ` +
3638
+ `To archive the whole project, address the project itself: /projects/<project>`
3639
+ ));
3640
+ }
3641
+ if (scope === 'project') {
3619
3642
  const pid = getState().projectId;
3620
3643
  if (!pid) return err(new Error('could not resolve project id for archive'));
3621
3644
  const result = await api('PATCH', `/api/project/${pid}`, { folder: '__archived' }, orgHeader);
@@ -5,7 +5,7 @@ import assert from 'node:assert/strict';
5
5
  import { mkdtempSync } from 'node:fs';
6
6
  import { join } from 'node:path';
7
7
  import { tmpdir } from 'node:os';
8
- import { projectlessMutationNeedsOrg, boundOrgRejected, receiptOrg, splitOrgScope, stripUrlOrigin } from './server.mjs';
8
+ import { projectlessMutationNeedsOrg, boundOrgRejected, receiptOrg, splitOrgScope, stripUrlOrigin, rmScope } from './server.mjs';
9
9
  import { loadPersistedProject, savePersistedProject } from './active-project-store.mjs';
10
10
 
11
11
  // One rule governs create AND fork (a fork is a create). A write proceeds when its
@@ -168,6 +168,16 @@ assert.equal(stripUrlOrigin('/o/acme/wiki/x'), '/o/acme/wiki/x', 'plain paths pa
168
168
  assert.equal(stripUrlOrigin('not a url'), 'not a url', 'non-URL input passes through');
169
169
  assert.equal(stripUrlOrigin('/f/00000000-0000-0000-0000-000000000000'), '/f/00000000-0000-0000-0000-000000000000', '/f/ frame links pass through');
170
170
 
171
+ // fs(rm) scope: only a BARE project path may archive a project. A lane path
172
+ // (/slides/deck) and a layer path (/slides) both parse with filename=null and
173
+ // used to fall through to the archive branch, silently archiving the whole
174
+ // project with a success-shaped response.
175
+ assert.equal(rmScope('slides', 'deck', '12.html'), 'file', 'a full frame path removes that frame');
176
+ assert.equal(rmScope('slides', null, '12.html'), 'file', 'a layer-root file path removes that frame');
177
+ assert.equal(rmScope('slides', 'deck', null), 'directory', 'a LANE path must never archive the project');
178
+ assert.equal(rmScope('slides', null, null), 'directory', 'a LAYER path must never archive the project');
179
+ assert.equal(rmScope(null, null, null), 'project', 'only the bare project path archives the project');
180
+
171
181
  console.log('org-guard policy OK');
172
182
  // Importing server.mjs builds the stdio MCP singleton, which opens a WS reconnect
173
183
  // loop that keeps the event loop alive. Assertions are done — exit deterministically.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drafted",
3
- "version": "1.19.11",
3
+ "version": "1.19.16",
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": [