Skip to main content
Glama

nexus_transform_jewel

Transform WorkFlowy outline nodes by applying semantic operations like moving, renaming, creating, deleting, and searching/replacing text within JSON working files.

Instructions

Apply JEWELSTORM semantic operations to a NEXUS working_gem JSON file (PHANTOM GEM working copy). This is the semantic analogue of edit_file for PHANTOM GEM JSON: MOVE_NODE, DELETE_NODE, RENAME_NODE, SET_NOTE, SET_ATTRS, CREATE_NODE, all referencing nodes by jewel_id, plus text-level SEARCH_REPLACE / SEARCH_AND_TAG over name/note fields (substring/whole-word, optional regex, tagging in name and/or note based on matches).

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
jewel_fileYes
operationsYes
dry_runNo
stop_on_errorNo

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault

No arguments

Implementation Reference

  • MCP tool registration for 'nexus_transform_jewel' with parameters and description defining the schema and usage.
        name="nexus_transform_jewel",
        description=(
            "Apply JEWELSTORM semantic operations to a NEXUS working_gem JSON file "
            "(PHANTOM GEM working copy). This is the semantic analogue of edit_file "
            "for PHANTOM GEM JSON: MOVE_NODE, DELETE_NODE, RENAME_NODE, SET_NOTE, "
            "SET_ATTRS, CREATE_NODE, all referencing nodes by jewel_id, plus text-level "
            "SEARCH_REPLACE / SEARCH_AND_TAG over name/note fields (substring/whole-word, "
            "optional regex, tagging in name and/or note based on matches)."
        ),
    )
    def nexus_transform_jewel(
        jewel_file: str,
        operations: list[dict[str, Any]],
        dry_run: bool = False,
        stop_on_error: bool = True,
    ) -> dict:
        """JEWELSTORM transform on a NEXUS working_gem JSON file.
    
        This tool wraps nexus_json_tools.transform_jewel via the WorkFlowyClient,
        providing an MCP-friendly interface for JEWELSTORM operations.
    
        Args:
            jewel_file: Absolute path to working_gem JSON (typically a QUILLSTRIKE
                        working stone derived from phantom_gem.json).
            operations: List of operation dicts. Each must include an "op" key and
                        operation-specific fields (e.g., jewel_id, parent_jewel_id,
                        position, etc.).
            dry_run: If True, simulate only (no file write).
            stop_on_error: If True, abort on first error (no write).
    
        Returns:
            Result dict from transform_jewel with success flag, counts, and errors.
        """
        client = get_client()
        return client.nexus_transform_jewel(
            jewel_file=jewel_file,
            operations=operations,
            dry_run=dry_run,
            stop_on_error=stop_on_error,
        )
  • Core handler function implementing all JEWELSTORM operations (MOVE_NODE, DELETE_NODE, RENAME_NODE, SET_NOTE, CREATE_NODE, SEARCH_REPLACE, etc.) on working_gem JSON using jewel_id handles.
    def transform_jewel(
        jewel_file: str,
        operations: List[Dict[str, Any]],
        dry_run: bool = False,
        stop_on_error: bool = True,
    ) -> Dict[str, Any]:
        """Apply JEWELSTORM semantic operations to a NEXUS working_gem JSON file.
    
        This is the semantic analogue of pattern-based edit_file() for PHANTOM GEM
        / working_gem JSON:
        - Operates purely offline (no Workflowy API calls)
        - Works on a local JSON file produced by JEWELSTRIKE (typically phantom_gem.json
          copied to a working stone)
        - Uses jewel_id as the stable identity handle inside JEWELSTORM
        - Never invents real Workflowy IDs: new nodes are written without an "id"
          field so NEXUS/WEAVE can treat them as CREATE operations
    
        Args:
            jewel_file: Path to working_gem JSON file
            operations: List of operation dictionaries, e.g.:
                {
                  "op": "MOVE_NODE",
                  "jewel_id": "J-001",
                  "new_parent_jewel_id": "J-010",
                  "position": "LAST" | "FIRST" | "BEFORE" | "AFTER",
                  "relative_to_jewel_id": "J-999"  # for BEFORE/AFTER
                }
            dry_run: If True, simulate only (no file write)
            stop_on_error: If True, abort on first error (no write)
    
        Returns:
            Dict with success flag, counts, and error details. Example:
            {
              "success": True,
              "applied_count": 3,
              "dry_run": False,
              "nodes_created": 1,
              "nodes_deleted": 0,
              "nodes_moved": 1,
              "nodes_renamed": 1,
              "notes_updated": 0,
              "attrs_updated": 0,
              "errors": []
            }
        """
        import uuid
    
        # ------- Load JSON safely (convert die/SystemExit into structured error) -------
        try:
            data = load_json(jewel_file)
        except SystemExit as e:  # die() inside load_json uses sys.exit
            return {
                "success": False,
                "applied_count": 0,
                "dry_run": dry_run,
                "nodes_created": 0,
                "nodes_deleted": 0,
                "nodes_moved": 0,
                "nodes_renamed": 0,
                "notes_updated": 0,
                "attrs_updated": 0,
                "errors": [
                    {
                        "index": -1,
                        "op": None,
                        "code": "LOAD_ERROR",
                        "message": f"Failed to load JSON from {jewel_file}: {e}",
                    }
                ],
            }
    
        # Determine editable roots list (supports both export-package dict and bare list)
        if isinstance(data, dict) and isinstance(data.get("nodes"), list):
            original_roots = data["nodes"]
        elif isinstance(data, list):
            original_roots = data
        else:
            return {
                "success": False,
                "applied_count": 0,
                "dry_run": dry_run,
                "nodes_created": 0,
                "nodes_deleted": 0,
                "nodes_moved": 0,
                "nodes_renamed": 0,
                "notes_updated": 0,
                "attrs_updated": 0,
                "errors": [
                    {
                        "index": -1,
                        "op": None,
                        "code": "FORMAT_ERROR",
                        "message": "JSON must be a dict with 'nodes' or a bare list of nodes for transform_jewel.",
                    }
                ],
            }
    
        # Work on a deep copy so we never mutate the original unless we succeed
        roots: List[JsonDict] = copy.deepcopy(original_roots)
    
        # ------- Indexing: jewel_id-based identity -------
        by_jewel_id: Dict[str, JsonDict] = {}
        parent_by_jewel_id: Dict[str, Optional[str]] = {}
        existing_ids: Set[str] = set()
    
        def _ensure_children_list(node: JsonDict) -> List[JsonDict]:
            children = node.get("children")
            if not isinstance(children, list):
                children = []
                node["children"] = children
            return children
    
        def _register_node(node: JsonDict, parent_jid: Optional[str]) -> None:
            """Register node and its subtree in jewel_id index.
    
            Identity:
            - Prefer "jewel_id" if present
            - Fallback to "id" for older files (pre-JEWELSTRIKE)
            """
            jewel_id = node.get("jewel_id") or node.get("id")
            if jewel_id:
                if jewel_id in by_jewel_id:
                    raise ValueError(f"Duplicate jewel_id/id {jewel_id!r} in JSON tree")
                by_jewel_id[jewel_id] = node
                parent_by_jewel_id[jewel_id] = parent_jid
                existing_ids.add(jewel_id)
    
            children = node.get("children") or []
            if not isinstance(children, list):
                children = []
                node["children"] = children
            for child in children:
                if isinstance(child, dict):
                    _register_node(child, jewel_id)
    
        for root in roots:
            if isinstance(root, dict):
                _register_node(root, None)
    
        def _new_jewel_id() -> str:
            """Generate a fresh, human-friendly jewel_id that cannot collide."""
            while True:
                candidate = "J-" + uuid.uuid4().hex[:8]
                if candidate not in existing_ids:
                    existing_ids.add(candidate)
                    return candidate
    
        def _resolve_to_jewel_id(identifier: str) -> str:
            """Auto-resolve Workflowy UUID to jewel_id if needed.
            
            If identifier looks like a Workflowy UUID (standard UUID format, not J-NNN),
            search the tree for a node with that id and return its jewel_id.
            
            If identifier is already a jewel_id (J-NNN format), return as-is.
            
            Args:
                identifier: Either a jewel_id (J-NNN) or Workflowy UUID
                
            Returns:
                jewel_id to use for the operation
                
            Raises:
                ValueError: If UUID not found in tree
            """
            import re
            
            # Check if it's a standard UUID format (8-4-4-4-12 hex pattern)
            uuid_pattern = re.compile(r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', re.IGNORECASE)
            
            if uuid_pattern.match(identifier):
                # It's a Workflowy UUID - search for node with this id
                for jewel_id, node in by_jewel_id.items():
                    if node.get("id") == identifier:
                        return jewel_id
                # Not found
                raise ValueError(
                    f"Node with Workflowy UUID '{identifier}' not found in GEM. "
                    "(Auto-resolved from jewel_id parameter - agent can use Workflowy UUIDs directly)"
                )
            else:
                # Already a jewel_id (J-NNN format) or other format - return as-is
                return identifier
    
    
        def _resolve_to_jewel_id(identifier: str) -> str:
            """Auto-resolve Workflowy UUID to jewel_id if needed.
            
            If identifier looks like a Workflowy UUID (standard UUID format, not J-NNN),
            search the tree for a node with that id and return its jewel_id.
            
            If identifier is already a jewel_id (J-NNN format), return as-is.
            
            Args:
                identifier: Either a jewel_id (J-NNN) or Workflowy UUID
                
            Returns:
                jewel_id to use for the operation
                
            Raises:
                ValueError: If UUID not found in tree
            """
            import re
            
            # Check if it's a standard UUID format (8-4-4-4-12 hex pattern)
            uuid_pattern = re.compile(r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', re.IGNORECASE)
            
            if uuid_pattern.match(identifier):
                # It's a Workflowy UUID - search for node with this id
                for jewel_id, node in by_jewel_id.items():
                    if node.get("id") == identifier:
                        return jewel_id
                # Not found
                raise ValueError(
                    f"Node with Workflowy UUID '{identifier}' not found in GEM. "
                    "(Auto-resolved from jewel_id parameter - agent can use Workflowy UUIDs directly)"
                )
            else:
                # Already a jewel_id (J-NNN format) or other format - return as-is
                return identifier
    
    
        def _get_node_and_parent_list(jewel_id: str) -> Tuple[JsonDict, Optional[str], List[JsonDict]]:
            # Auto-resolve Workflowy UUID to jewel_id if needed
            jewel_id = _resolve_to_jewel_id(jewel_id)
            
            node = by_jewel_id.get(jewel_id)
            if node is None:
                raise ValueError(f"Node with jewel_id/id {jewel_id!r} not found")
            parent_jid = parent_by_jewel_id.get(jewel_id)
            if parent_jid is None:
                siblings = roots
            else:
                parent_node = by_jewel_id.get(parent_jid)
                if parent_node is None:
                    raise ValueError(f"Parent with jewel_id/id {parent_jid!r} not found for node {jewel_id!r}")
                siblings = _ensure_children_list(parent_node)
            return node, parent_jid, siblings
    
        def _assert_no_cycle(node_jewel_id: str, new_parent_jewel_id: Optional[str]) -> None:
            cur = new_parent_jewel_id
            while cur is not None:
                if cur == node_jewel_id:
                    raise ValueError(
                        f"Cannot move node {node_jewel_id!r} under its own descendant {new_parent_jewel_id!r}"
                    )
                cur = parent_by_jewel_id.get(cur)
    
        def _remove_subtree_from_index(node: JsonDict) -> None:
            jid = node.get("jewel_id") or node.get("id")
            if jid:
                by_jewel_id.pop(jid, None)
                parent_by_jewel_id.pop(jid, None)
                existing_ids.discard(jid)
            for child in node.get("children") or []:
                if isinstance(child, dict):
                    _remove_subtree_from_index(child)
    
        def _build_subtree_from_spec(spec: Dict[str, Any]) -> JsonDict:
            """Build a new subtree from a CREATE_NODE spec (Pattern A).
    
            Spec keys used:
              - name (required)
              - note (optional)
              - attrs or data (optional, mapped to node['data'])
              - jewel_id (optional; if omitted, auto-generated)
              - children (optional list of nested specs)
              - parent_id (optional, ignored - will be set during insertion)
            """
            if "name" not in spec:
                raise ValueError("CREATE_NODE spec requires 'name' field in node object")
    
            node: JsonDict = {"name": spec["name"]}
    
            if "note" in spec:
                node["note"] = spec["note"]
    
            attrs = spec.get("attrs") or spec.get("data")
            if isinstance(attrs, dict):
                node["data"] = copy.deepcopy(attrs)
    
            # Assign jewel_id (never assign real Workflowy id here)
            jewel_id = spec.get("jewel_id")
            if jewel_id is None:
                jewel_id = _new_jewel_id()
            else:
                if jewel_id in existing_ids:
                    raise ValueError(f"CREATE_NODE requested duplicate jewel_id {jewel_id!r}")
                existing_ids.add(jewel_id)
            node["jewel_id"] = jewel_id
    
            # Recursively build children
            children_specs = spec.get("children") or []
            children_nodes: List[JsonDict] = []
            for child_spec in children_specs:
                if isinstance(child_spec, dict):
                    child_node = _build_subtree_from_spec(child_spec)
                    children_nodes.append(child_node)
            node["children"] = children_nodes
            return node
    
        def _classify_children_for_destructive_ops(node: JsonDict) -> Dict[str, Any]:
            """Classify immediate children for destructive JEWELSTORM ops.
    
            Returns a dict with:
                children: list of child dict nodes
                children_status: str
                truncated: bool
                has_loaded_ether_children: bool  # child has a real Workflowy id
                has_new_children: bool           # child has no id yet (JEWEL-only)
                category: "EMPTY" | "JEWEL_ONLY" | "ETHER_ONLY" | "MIXED"
            """
            children_raw = node.get("children") or []
            children = [c for c in children_raw if isinstance(c, dict)]
            children_status = node.get("children_status") or "complete"
            truncated = children_status in {"truncated_by_depth", "truncated_by_count"}
            has_loaded_ether_children = any(c.get("id") is not None for c in children)
            has_new_children = any(c.get("id") is None for c in children)
    
            if not children:
                category = "EMPTY"
            elif (not has_loaded_ether_children) and has_new_children and not truncated:
                category = "JEWEL_ONLY"
            elif has_loaded_ether_children and not has_new_children:
                category = "ETHER_ONLY"
            else:
                category = "MIXED"
    
            return {
                "children": children,
                "children_status": children_status,
                "truncated": truncated,
                "has_loaded_ether_children": has_loaded_ether_children,
                "has_new_children": has_new_children,
                "category": category,
            }
    
        # ------- Apply operations -------
        applied_count = 0
        nodes_created = 0
        nodes_deleted = 0
        nodes_moved = 0
        nodes_renamed = 0
        notes_updated = 0
        attrs_updated = 0
        errors: List[Dict[str, Any]] = []
    
        for idx, op in enumerate(operations or []):
            op_type_raw = op.get("op") or op.get("operation")
            if not op_type_raw:
                err = {
                    "index": idx,
                    "op": op,
                    "code": "MISSING_OP",
                    "message": "Operation missing 'op' field",
                }
                errors.append(err)
                if stop_on_error:
                    break
                continue
    
            op_type = str(op_type_raw).upper()
    
            try:
                # MOVE_NODE
                if op_type == "MOVE_NODE":
                    src_jid = op.get("jewel_id")
                    if not src_jid:
                        raise ValueError("MOVE_NODE requires 'jewel_id'")
    
                    position = str(op.get("position", "LAST")).upper()
                    # Normalize synonyms
                    if position == "BOTTOM":
                        position = "LAST"
                    if position == "TOP":
                        position = "FIRST"
                    rel_jid = op.get("relative_to_jewel_id")
                    if rel_jid:
                        rel_jid = _resolve_to_jewel_id(rel_jid)
                    new_parent_jid = op.get("new_parent_jewel_id")
    
                    if position in {"BEFORE", "AFTER"}:
                        if not rel_jid:
                            raise ValueError(
                                "MOVE_NODE with position BEFORE/AFTER requires 'relative_to_jewel_id'"
                            )
                        # Target parent/siblings come from relative node
                        _, rel_parent_jid, rel_siblings = _get_node_and_parent_list(rel_jid)
                        target_parent_jid = rel_parent_jid
                        target_list = rel_siblings
                    else:
                        # FIRST/LAST under explicit parent (or root when parent None)
                        target_parent_jid = new_parent_jid
                        if new_parent_jid is None:
                            target_list = roots
                        else:
                            parent_node = by_jewel_id.get(new_parent_jid)
                            if parent_node is None:
                                raise ValueError(f"New parent jewel_id/id {new_parent_jid!r} not found")
                            target_list = _ensure_children_list(parent_node)
    
                    # Cycle check
                    _assert_no_cycle(src_jid, target_parent_jid)
    
                    node, old_parent_jid, old_siblings = _get_node_and_parent_list(src_jid)
    
                    # Remove from old siblings
                    if node in old_siblings:
                        old_siblings.remove(node)
    
                    # Insert into new location
                    if position == "FIRST":
                        target_list.insert(0, node)
                    elif position == "LAST":
                        target_list.append(node)
                    elif position in {"BEFORE", "AFTER"}:
                        rel_node, _, _ = _get_node_and_parent_list(rel_jid)  # type: ignore[arg-type]
                        try:
                            rel_index = target_list.index(rel_node)
                        except ValueError as e:
                            raise ValueError(
                                f"Relative node {rel_jid!r} not found under chosen parent for MOVE_NODE"
                            ) from e
                        insert_index = rel_index if position == "BEFORE" else rel_index + 1
                        target_list.insert(insert_index, node)
                    else:
                        raise ValueError(f"Unsupported MOVE_NODE position {position!r}")
    
                    # Update parent mapping
                    parent_by_jewel_id[src_jid] = target_parent_jid
                    nodes_moved += 1
    
                # DELETE_NODE
                elif op_type == "DELETE_NODE":
                    jid = op.get("jewel_id")
                    if not jid:
                        raise ValueError("DELETE_NODE requires 'jewel_id'")
    
                    delete_from_ether = bool(op.get("delete_from_ether"))
                    mode_raw = op.get("mode")
                    mode = str(mode_raw).upper() if mode_raw is not None else "SMART"
    
                    node, parent_jid, siblings = _get_node_and_parent_list(jid)
                    info = _classify_children_for_destructive_ops(node)
                    children = info["children"]
                    category = info["category"]
    
                    # Legacy strict mode: preserve original FAIL_IF_HAS_CHILDREN semantics
                    if mode == "FAIL_IF_HAS_CHILDREN":
                        if children:
                            raise ValueError(
                                f"DELETE_NODE {jid!r} refused: node has children and mode=FAIL_IF_HAS_CHILDREN"
                            )
                    else:
                        # SMART semantics (default when mode not provided)
                        if category == "MIXED":
                            raise ValueError(
                                f"DELETE_NODE {jid!r} refused: mixed new/ETHER/truncated children not supported; "
                                "delete or move children individually first"
                            )
                        if category == "ETHER_ONLY" and not delete_from_ether:
                            raise ValueError(
                                f"DELETE_NODE {jid!r} refused: node has ETHER-backed children; "
                                "set delete_from_ether=True to delete subtree in Workflowy"
                            )
                        # EMPTY and JEWEL_ONLY are always allowed.
    
                    # Remove from siblings and index
                    if node in siblings:
                        siblings.remove(node)
                    _remove_subtree_from_index(node)
                    nodes_deleted += 1
    
                # DELETE_ALL_CHILDREN
                elif op_type == "DELETE_ALL_CHILDREN":
                    jid = op.get("jewel_id")
                    if not jid:
                        raise ValueError("DELETE_ALL_CHILDREN requires 'jewel_id'")
    
                    delete_from_ether = bool(op.get("delete_from_ether"))
                    mode_raw = op.get("mode")
                    mode = str(mode_raw).upper() if mode_raw is not None else "SMART"
    
                    node = by_jewel_id.get(jid)
                    if node is None:
                        raise ValueError(f"Node with jewel_id/id {jid!r} not found")
    
                    info = _classify_children_for_destructive_ops(node)
                    children = info["children"]
                    category = info["category"]
                    truncated = info["truncated"]
    
                    # Nothing to do
                    if not children and not truncated:
                        pass
                    else:
                        # For DELETE_ALL_CHILDREN we do not support truncated children sets in v1
                        if truncated:
                            raise ValueError(
                                f"DELETE_ALL_CHILDREN {jid!r} refused: children set is truncated; "
                                "re-GLIMPSE with full children or delete the node instead"
                            )
    
                        if mode == "FAIL_IF_HAS_CHILDREN":
                            if children:
                                raise ValueError(
                                    f"DELETE_ALL_CHILDREN {jid!r} refused: node has children and "
                                    "mode=FAIL_IF_HAS_CHILDREN"
                                )
                        else:
                            if category == "MIXED":
                                raise ValueError(
                                    f"DELETE_ALL_CHILDREN {jid!r} refused: mixed new/ETHER children not supported; "
                                    "delete or move children individually first"
                                )
                            if category == "ETHER_ONLY" and not delete_from_ether:
                                raise ValueError(
                                    f"DELETE_ALL_CHILDREN {jid!r} refused: children are ETHER-backed; "
                                    "set delete_from_ether=True to delete them in Workflowy"
                                )
                            # EMPTY and JEWEL_ONLY are always allowed here.
    
                        # Allowed path: remove all current children from index and node
                        for child in list(children):
                            _remove_subtree_from_index(child)
                        node["children"] = []
                        nodes_deleted += len(children)
    
                # RENAME_NODE
                elif op_type == "RENAME_NODE":
                    jid = op.get("jewel_id")
                    if not jid:
                        raise ValueError("RENAME_NODE requires 'jewel_id'")
                    new_name = op.get("new_name")
                    if new_name is None:
                        raise ValueError("RENAME_NODE requires 'new_name'")
    
                    node = by_jewel_id.get(jid)
                    if node is None:
                        raise ValueError(f"Node with jewel_id/id {jid!r} not found")
                    node["name"] = new_name
                    nodes_renamed += 1
    
                # SET_NOTE
                elif op_type == "SET_NOTE":
                    jid = op.get("jewel_id")
                    if not jid:
                        raise ValueError("SET_NOTE requires 'jewel_id'")
                    new_note = op.get("new_note")
                    # Allow empty string / None to clear
    
                    node = by_jewel_id.get(jid)
                    if node is None:
                        raise ValueError(f"Node with jewel_id/id {jid!r} not found")
                    node["note"] = new_note
                    notes_updated += 1
    
                # SET_ATTRS
                elif op_type == "SET_ATTRS":
                    jid = op.get("jewel_id")
                    if not jid:
                        raise ValueError("SET_ATTRS requires 'jewel_id'")
                    attrs = op.get("attrs") or {}
                    if not isinstance(attrs, dict):
                        raise ValueError("SET_ATTRS 'attrs' must be a dict")
    
                    node = by_jewel_id.get(jid)
                    if node is None:
                        raise ValueError(f"Node with jewel_id/id {jid!r} not found")
    
                    data = node.get("data")
                    if not isinstance(data, dict):
                        data = {}
    
                    allowed_keys = {"completed", "layoutMode", "priority", "tags"}
                    for key, value in attrs.items():
                        if key not in allowed_keys:
                            raise ValueError(f"Unsupported attr key {key!r} in SET_ATTRS")
                        if value is None:
                            data.pop(key, None)
                        else:
                            data[key] = value
    
                    if data:
                        node["data"] = data
                    elif "data" in node:
                        del node["data"]
    
                    attrs_updated += 1
    
                # CREATE_NODE (Pattern A with optional jewel_id)
                elif op_type == "CREATE_NODE":
                    parent_jid = op.get("parent_jewel_id")
                    position = str(op.get("position", "LAST")).upper()
                    # Normalize synonyms
                    if position == "BOTTOM":
                        position = "LAST"
                    if position == "TOP":
                        position = "FIRST"
                    rel_jid = op.get("relative_to_jewel_id")
                    if rel_jid:
                        rel_jid = _resolve_to_jewel_id(rel_jid)
    
                    if parent_jid is None:
                        parent_children = roots
                        target_parent_jid = None
                    else:
                        parent_node = by_jewel_id.get(parent_jid)
                        if parent_node is None:
                            raise ValueError(f"CREATE_NODE parent_jewel_id {parent_jid!r} not found")
                        parent_children = _ensure_children_list(parent_node)
                        target_parent_jid = parent_jid
    
                    # Build node spec for subtree
                    # Two formats supported:
                    # 1. Compact: node fields at operation level (name, note, children, etc.)
                    # 2. Wrapped: node fields inside "node" key
                    if "node" in op:
                        spec = op["node"]
                    else:
                        # Remove op-specific keys to get node spec
                        spec = {k: v for k, v in op.items() if k not in {
                            "op",
                            "operation",
                            "parent_jewel_id",
                            "position",
                            "relative_to_jewel_id",
                        }}
    
                    new_node = _build_subtree_from_spec(spec)
    
                    # Register subtree in indexes with correct parent mapping
                    _register_node(new_node, target_parent_jid)
    
                    # Insert relative to siblings
                    if position == "FIRST":
                        parent_children.insert(0, new_node)
                    elif position == "LAST":
                        parent_children.append(new_node)
                    elif position in {"BEFORE", "AFTER"}:
                        if not rel_jid:
                            raise ValueError(
                                "CREATE_NODE with position BEFORE/AFTER requires 'relative_to_jewel_id'"
                            )
                        rel_node, _, rel_siblings = _get_node_and_parent_list(rel_jid)  # type: ignore[arg-type]
                        # Force siblings to be the same list as parent_children
                        if rel_siblings is not parent_children:
                            raise ValueError(
                                "CREATE_NODE BEFORE/AFTER relative_to_jewel_id must share the same parent"
                            )
                        try:
                            rel_index = parent_children.index(rel_node)
                        except ValueError as e:
                            raise ValueError(
                                f"Relative node {rel_jid!r} not found under chosen parent for CREATE_NODE"
                            ) from e
                        insert_index = rel_index if position == "BEFORE" else rel_index + 1
                        parent_children.insert(insert_index, new_node)
                    else:
                        raise ValueError(f"Unsupported CREATE_NODE position {position!r}")
    
                    nodes_created += 1
    
                # SET_ATTRS_BY_PATH (path-based attribute update, used for JEWEL UUID injection)
                elif op_type == "SET_ATTRS_BY_PATH":
                    path = op.get("path")
                    attrs = op.get("attrs") or {}
                    if not isinstance(path, list) or not path:
                        raise ValueError("SET_ATTRS_BY_PATH requires non-empty 'path' list")
                    if not isinstance(attrs, dict):
                        raise ValueError("SET_ATTRS_BY_PATH 'attrs' must be a dict")
    
                    # Navigate by index path from roots
                    current_list: List[JsonDict] = roots
                    target_node: Optional[JsonDict] = None
                    for level, idx in enumerate(path):
                        if not isinstance(idx, int):
                            raise ValueError(
                                f"SET_ATTRS_BY_PATH path index at position {level} must be int, got {type(idx).__name__}"
                            )
                        if idx < 0 or idx >= len(current_list):
                            raise ValueError(
                                f"SET_ATTRS_BY_PATH path index {idx} out of range at position {level}"
                            )
                        target_node = current_list[idx]
                        if level < len(path) - 1:
                            children = target_node.get("children")
                            if not isinstance(children, list):
                                raise ValueError(
                                    f"SET_ATTRS_BY_PATH path descends into non-list children at position {level}"
                                )
                            current_list = children
    
                    if target_node is None:
                        raise ValueError("SET_ATTRS_BY_PATH could not resolve target node from path")
    
                    for key, value in attrs.items():
                        if key == "id":
                            if value is None:
                                target_node.pop("id", None)
                            else:
                                if not isinstance(value, str):
                                    raise ValueError(
                                        "SET_ATTRS_BY_PATH 'id' value must be a string when not None"
                                    )
                                target_node["id"] = value
                        else:
                            raise ValueError(
                                f"Unsupported attr key {key!r} in SET_ATTRS_BY_PATH (only 'id' is currently allowed)"
                            )
    
                    attrs_updated += 1
    
                else:
                    raise ValueError(f"Unknown operation type {op_type!r}")
    
                applied_count += 1
    
            except Exception as e:  # noqa: BLE001
                errors.append(
                    {
                        "index": idx,
                        "op": op,
                        "code": "OP_ERROR",
                        "message": str(e),
                    }
                )
                if stop_on_error:
                    break
    
        # ------- Persist changes (if not dry-run and no stop-on-error failure) -------
        if not dry_run and (not stop_on_error or not errors):
            # Attach modified roots back to original structure
            if isinstance(data, dict) and isinstance(data.get("nodes"), list):
                data["nodes"] = roots
                # Recalculate counts for readability in the JEWEL working_gem,
                # but DO NOT touch children_status here. Truncation semantics belong
                # to the NEXUS/TERRAIN layer (shimmering/enchanted terrain).
                wrapper = {"nodes": roots}
                recalc_all_counts_gem(wrapper)
            elif isinstance(data, list):
                data = roots  # type: ignore[assignment]
                wrapper = {"nodes": roots}
                recalc_all_counts_gem(wrapper)
    
            save_json(jewel_file, data)  # type: ignore[arg-type]
    
        return {
            "success": len(errors) == 0,
            "applied_count": applied_count,
            "dry_run": dry_run,
            "nodes_created": nodes_created,
            "nodes_deleted": nodes_deleted,
            "nodes_moved": nodes_moved,
            "nodes_renamed": nodes_renamed,
            "notes_updated": notes_updated,
            "attrs_updated": attrs_updated,
            "errors": errors,
        }
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full burden of behavioral disclosure. It mentions operations like DELETE_NODE and SEARCH_REPLACE, which imply mutation and potential data loss, but doesn't explicitly state whether changes are destructive, reversible, or require specific permissions. It also doesn't cover error handling, rate limits, or response format, leaving significant gaps in behavioral understanding for a tool with multiple operations and parameters.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness3/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, dense sentence that packs in many details (e.g., operation types, file context, text-level features). While it avoids unnecessary words, it's not front-loaded with the core purpose and could benefit from clearer structuring (e.g., separating operation listing from context explanation). Some phrases like 'PHANTOM GEM working copy' are jargon-heavy without explanation, reducing clarity.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's complexity (4 parameters, multiple operations, no annotations, but with an output schema), the description provides a basic overview but lacks completeness. It covers the high-level purpose and operation types but misses key details: parameter meanings, behavioral traits (e.g., mutation effects), and how to use the tool effectively. The output schema may help with return values, but the description doesn't compensate for the gaps in schema coverage and missing annotations.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters2/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The schema description coverage is 0%, so the schema provides no parameter details. The description mentions 'jewel_file' and 'operations' implicitly by referring to applying operations to a JSON file, but doesn't explain what 'jewel_file' is (e.g., file path or identifier) or the structure of 'operations' (e.g., how to specify MOVE_NODE vs. DELETE_NODE). It also doesn't address 'dry_run' or 'stop_on_error' parameters, leaving most of the 4 parameters inadequately documented.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose: applying semantic operations to a NEXUS working_gem JSON file. It specifies the verb 'apply' and the resource 'working_gem JSON file', and lists specific operations like MOVE_NODE, DELETE_NODE, etc. However, it doesn't explicitly differentiate from sibling tools like 'nexus_anchor_gems' or 'nexus_weave_enchanted_async', which might also manipulate similar data structures.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description implies usage by stating it's for 'PHANTOM GEM working copy' and 'semantic analogue of edit_file for PHANTOM GEM JSON', suggesting it's for editing JSON files in this specific context. However, it lacks explicit guidance on when to use this tool versus alternatives (e.g., 'nexus_anchor_gems' or 'workflowy_etch'), and doesn't mention prerequisites or exclusions, leaving the agent to infer context from the tool name and description alone.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/daniel347x/workflowy-mcp-fixed'

If you have feedback or need assistance with the MCP directory API, please join our Discord server