drafted 1.19.49 → 1.19.51

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/install-mcp.sh CHANGED
@@ -550,47 +550,93 @@ else
550
550
  PLUGIN_SRC=""
551
551
  fi
552
552
 
553
+ # Skills and commands are LINKED, never copied: a copy is a point-in-time snapshot that only
554
+ # refreshes when someone re-runs this installer, so `npm i -g drafted@latest` would leave the
555
+ # installed skill silently behind the package it came from. A link tracks the upgrade.
556
+ #
557
+ # link_plugin_entry is idempotent and non-destructive: a correct link is left alone, a wrong or
558
+ # dangling one is repointed, and a real file/directory (which the user may have written by hand)
559
+ # is moved aside rather than deleted. Backups go OUTSIDE the skills/commands dirs — a
560
+ # `drafted.backup/` sitting next to `drafted/` would register as a second, stale skill.
561
+ DRAFTED_BACKUP_DIR="$HOME/.claude/.drafted-backups"
562
+ link_plugin_entry() {
563
+ local source="$1" target="$2" source_real backup
564
+ source_real="$(cd "$(dirname "$source")" && pwd)/$(basename "$source")"
565
+ if [ -L "$target" ]; then
566
+ if [ "$(readlink "$target")" = "$source_real" ]; then return 0; fi
567
+ rm -f "$target"
568
+ elif [ -e "$target" ]; then
569
+ # Byte-identical to the source: this is a prior copy-install of ours, not user content, and
570
+ # the link reproduces it losslessly. Anything else is backed up.
571
+ if diff -rq "$source_real" "$target" >/dev/null 2>&1; then
572
+ rm -rf "$target"
573
+ else
574
+ mkdir -p "$DRAFTED_BACKUP_DIR"
575
+ backup="$DRAFTED_BACKUP_DIR/$(basename "$target").$(date +%Y%m%d%H%M%S)"
576
+ mv "$target" "$backup"
577
+ echo -e " ${YELLOW}Existing $(basename "$target") moved aside → $backup${RESET}"
578
+ fi
579
+ fi
580
+ ln -s "$source_real" "$target"
581
+ }
582
+
583
+ # Remove links WE made whose source is gone — an entry dropped from the package, or the package
584
+ # uninstalled entirely. A dangling skill dir is worse than a stale one: it looks installed and
585
+ # loads nothing. Matched on the link's own target path so other installers' links are untouched.
586
+ sweep_plugin_links() {
587
+ local dir="$1" marker="$2" link
588
+ [ -d "$dir" ] || return 0
589
+ while IFS= read -r link; do
590
+ case "$(readlink "$link")" in
591
+ *"$marker"*)
592
+ if [ ! -e "$link" ]; then
593
+ rm -f "$link"
594
+ echo -e " ${YELLOW}Removed stale link: $(basename "$link")${RESET}"
595
+ fi
596
+ ;;
597
+ esac
598
+ done < <(find "$dir" -maxdepth 1 -type l 2>/dev/null)
599
+ }
600
+
553
601
  # Skills — Claude Code auto-loads these from ~/.claude/skills/<name>/SKILL.md
602
+ SKILLS_DIR="$HOME/.claude/skills"
603
+ sweep_plugin_links "$SKILLS_DIR" "/plugin/skills/"
554
604
  if [ -n "$PLUGIN_SRC" ] && [ -d "$PLUGIN_SRC/skills" ]; then
555
- SKILLS_DIR="$HOME/.claude/skills"
556
605
  mkdir -p "$SKILLS_DIR"
557
606
  SKILL_COUNT=0
558
607
  for d in "$PLUGIN_SRC/skills/"*/; do
559
608
  [ -d "$d" ] || continue
560
609
  DIRNAME="$(basename "$d")"
561
- rm -rf "$SKILLS_DIR/$DIRNAME"
562
- cp -r "$d" "$SKILLS_DIR/$DIRNAME"
610
+ link_plugin_entry "${d%/}" "$SKILLS_DIR/$DIRNAME"
563
611
  SKILL_COUNT=$((SKILL_COUNT + 1))
564
612
  done
565
- ok "Installed $SKILL_COUNT skill(s) to $SKILLS_DIR"
613
+ ok "Linked $SKILL_COUNT skill(s) into $SKILLS_DIR (source: $PLUGIN_SRC/skills)"
566
614
  else
567
615
  echo -e " ${YELLOW}Skill source not found in package — skipping skills.${RESET}"
568
616
  fi
569
617
 
570
618
  # Commands — namespaced under drafted/ so they register as /drafted:<name>,
571
- # matching the marketplace install and the commands' own cross-references.
619
+ # matching the marketplace install and the commands' own cross-references. Linked per FILE into a
620
+ # real directory (not a linked directory) so discovery is unchanged; the containing dir is no
621
+ # longer rm -rf'd, so anything else a user put there survives.
622
+ CLAUDE_CMD_DIR="$HOME/.claude/commands/drafted"
623
+ CODEX_CMD_DIR="$HOME/.codex/prompts/drafted"
624
+ sweep_plugin_links "$CLAUDE_CMD_DIR" "/plugin/commands/"
625
+ sweep_plugin_links "$CODEX_CMD_DIR" "/plugin/commands/"
572
626
  if [ -n "$PLUGIN_SRC" ] && [ -d "$PLUGIN_SRC/commands" ]; then
573
- CMD_COUNT=0
574
- for f in "$PLUGIN_SRC/commands/"*.md; do
575
- [ -f "$f" ] || continue
576
- CMD_COUNT=$((CMD_COUNT + 1))
577
- done
578
-
579
- if [ "$CLIENT_CLAUDE_CODE" = true ]; then
580
- CLAUDE_CMD_DIR="$HOME/.claude/commands/drafted"
581
- rm -rf "$CLAUDE_CMD_DIR"
582
- mkdir -p "$CLAUDE_CMD_DIR"
583
- cp "$PLUGIN_SRC/commands/"*.md "$CLAUDE_CMD_DIR/" 2>/dev/null || true
584
- ok "Installed $CMD_COUNT command(s) to $CLAUDE_CMD_DIR"
585
- fi
627
+ link_commands_into() {
628
+ local dest="$1" count=0 f
629
+ mkdir -p "$dest"
630
+ for f in "$PLUGIN_SRC/commands/"*.md; do
631
+ [ -f "$f" ] || continue
632
+ link_plugin_entry "$f" "$dest/$(basename "$f")"
633
+ count=$((count + 1))
634
+ done
635
+ ok "Linked $count command(s) into $dest"
636
+ }
586
637
 
587
- if [ "$CLIENT_CODEX" = true ]; then
588
- CODEX_CMD_DIR="$HOME/.codex/prompts/drafted"
589
- rm -rf "$CODEX_CMD_DIR"
590
- mkdir -p "$CODEX_CMD_DIR"
591
- cp "$PLUGIN_SRC/commands/"*.md "$CODEX_CMD_DIR/" 2>/dev/null || true
592
- ok "Installed $CMD_COUNT command(s) to $CODEX_CMD_DIR"
593
- fi
638
+ if [ "$CLIENT_CLAUDE_CODE" = true ]; then link_commands_into "$CLAUDE_CMD_DIR"; fi
639
+ if [ "$CLIENT_CODEX" = true ]; then link_commands_into "$CODEX_CMD_DIR"; fi
594
640
  else
595
641
  echo -e " ${YELLOW}Command source not found in package — skipping commands.${RESET}"
596
642
  fi
package/mcp/server.mjs CHANGED
@@ -4235,7 +4235,10 @@ tool('fs', 'Navigate Drafted like a local filesystem. A FOLDER is the single con
4235
4235
  if (!dest || !dest.meta || !dest.meta.id || dest.rest.length < 2) {
4236
4236
  return err(new Error('readonly link requires to=/projects/<project>/<layer>/<lane>/<file> or a frame UUID'));
4237
4237
  }
4238
- const src = await api('GET', `/api/fs/${dest.rest.join('/')}?projectId=${dest.meta.id}`, undefined, orgHeader);
4238
+ // Scope via header, NOT the query string — api() also appends
4239
+ // projectId for the bound session and two projectId params made
4240
+ // the route resolve both as an array (500).
4241
+ const src = await api('GET', `/api/fs/${dest.rest.join('/')}`, undefined, { ...orgHeader, 'X-Drafted-Project': dest.meta.id });
4239
4242
  if (!src?.id) return err(new Error(`Source frame not found: ${t}`));
4240
4243
  sourceId = src.id;
4241
4244
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drafted",
3
- "version": "1.19.49",
3
+ "version": "1.19.51",
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": [