Add 15 new MCP tools to sx-tree: project-wide search, smart editing, dev workflow
New comprehension tools: - sx_find_across: search pattern across all .sx files in a directory - sx_comp_list: list all definitions (defcomp/defisland/defmacro/defpage/define) - sx_comp_usage: find all uses of a component across files - sx_diff: structural diff between two .sx files (ADDED/REMOVED/CHANGED) - sx_eval: REPL — evaluate SX expressions in the MCP server env Smart read_tree enhancements: - Auto-summarise large files (>200 lines) - focus param: expand only matching subtrees, collapse rest - max_depth/max_lines/offset for depth limiting and pagination Smart editing tools: - sx_rename_symbol: rename all occurrences of a symbol in a file - sx_replace_by_pattern: find+replace first/all pattern matches - sx_insert_near: insert before/after a pattern match (top-level) - sx_rename_across: rename symbol across all .sx files (with dry_run) - sx_write_file: create .sx files with parse validation Development tools: - sx_pretty_print: reformat .sx files with indentation (also used by all edit tools) - sx_build: build JS bundle or OCaml binary - sx_test: run test suites with structured pass/fail results - sx_format_check: lint for empty bindings, missing bodies, duplicate params - sx_macroexpand: evaluate expressions with a file's macro definitions loaded Also: updated hook to block Write on .sx files, added custom explore agent. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -57,3 +57,89 @@
|
||||
(define list-insert :effects () (fn (lst idx val) (concat (slice lst 0 idx) (list val) (slice lst idx))))
|
||||
|
||||
(define list-remove :effects () (fn (lst idx) (concat (slice lst 0 idx) (slice lst (+ idx 1)))))
|
||||
|
||||
(define tree-diff :effects () (fn (exprs-a exprs-b) (let ((nodes-a (if (list? exprs-a) exprs-a (list exprs-a))) (nodes-b (if (list? exprs-b) exprs-b (list exprs-b))) (results (list))) (diff-children nodes-a nodes-b (list) results) (if (empty? results) "No differences" (join "\n" results)))))
|
||||
|
||||
(define diff-children :effects () (fn (list-a list-b path results) (let ((len-a (len list-a)) (len-b (len list-b)) (min-len (if (< len-a len-b) len-a len-b))) (for-each (fn (i) (diff-node (nth list-a i) (nth list-b i) (concat path (list i)) results)) (range 0 min-len)) (when (> len-b min-len) (for-each (fn (i) (append! results (str "ADDED " (path-str (concat path (list i))) " " (node-summary-short (nth list-b i))))) (range min-len len-b))) (when (> len-a min-len) (for-each (fn (i) (append! results (str "REMOVED " (path-str (concat path (list i))) " " (node-summary-short (nth list-a i))))) (range min-len len-a))))))
|
||||
|
||||
(define diff-node :effects () (fn (a b path results) (cond (and (list? a) (list? b)) (diff-children a b path results) (and (not (list? a)) (not (list? b))) (when (not (= (node-display a) (node-display b))) (append! results (str "CHANGED " (path-str path) " " (node-display a) " → " (node-display b)))) :else (append! results (str "CHANGED " (path-str path) " " (node-summary-short a) " → " (node-summary-short b))))))
|
||||
|
||||
(define path-prefix? :effects () (fn (prefix path) (if (> (len prefix) (len path)) false (let ((result true)) (for-each (fn (i) (when (not (= (nth prefix i) (nth path i))) (set! result false))) (range 0 (len prefix))) result))))
|
||||
|
||||
(define path-on-match-route? :effects () (fn (path match-paths) (let ((found false)) (for-each (fn (i) (when (not found) (let ((mp (first (nth match-paths i)))) (when (or (path-prefix? path mp) (path-prefix? mp path)) (set! found true))))) (range 0 (len match-paths))) found)))
|
||||
|
||||
(define annotate-focused :effects () (fn (exprs pattern) (let ((nodes (if (list? exprs) exprs (list exprs))) (match-paths (find-all nodes pattern)) (result (list))) (for-each (fn (i) (annotate-node-focused (nth nodes i) (list i) 0 match-paths result)) (range 0 (len nodes))) (join "\n" result))))
|
||||
|
||||
(define annotate-node-focused :effects () (fn (node path depth match-paths result) (let ((indent (join "" (map (fn (_) " ") (range 0 depth)))) (label (path-str path))) (if (list? node) (if (empty? node) (append! result (str indent label " ()")) (let ((head (first node)) (head-str (node-display head)) (on-route (path-on-match-route? path match-paths))) (if on-route (do (append! result (str indent label " (" head-str)) (for-each (fn (i) (annotate-node-focused (nth node i) (concat path (list i)) (+ depth 1) match-paths result)) (range 1 (len node))) (append! result (str indent " )"))) (append! result (str indent label " (" head-str (if (> (len node) 1) (str " ... " (- (len node) 1) " children") "") ")"))))) (append! result (str indent label " " (node-display node)))))))
|
||||
|
||||
(define annotate-paginated :effects () (fn (exprs offset limit) (let ((nodes (if (list? exprs) exprs (list exprs))) (all-lines (list))) (for-each (fn (i) (annotate-node (nth nodes i) (list i) 0 all-lines)) (range 0 (len nodes))) (let ((total (len all-lines)) (end (if (> (+ offset limit) total) total (+ offset limit))) (sliced (slice all-lines offset end)) (header (str ";; Lines " offset "-" end " of " total (if (< end total) " (more available)" " (complete)")))) (str header "\n" (join "\n" sliced))))))
|
||||
|
||||
(define rename-symbol :effects () (fn (exprs old-name new-name) (let ((nodes (if (list? exprs) exprs (list exprs)))) (map (fn (node) (rename-in-node node old-name new-name)) nodes))))
|
||||
|
||||
(define rename-in-node :effects () (fn (node old-name new-name) (cond (and (= (type-of node) "symbol") (= (symbol-name node) old-name)) (make-symbol new-name) (list? node) (map (fn (child) (rename-in-node child old-name new-name)) node) :else node)))
|
||||
|
||||
(define count-renames :effects () (fn (exprs old-name) (let ((nodes (if (list? exprs) exprs (list exprs))) (hits (list))) (count-in-node nodes old-name hits) (len hits))))
|
||||
|
||||
(define count-in-node :effects () (fn (node old-name hits) (cond (and (= (type-of node) "symbol") (= (symbol-name node) old-name)) (append! hits true) (list? node) (for-each (fn (child) (count-in-node child old-name hits)) node) :else nil)))
|
||||
|
||||
(define replace-by-pattern :effects () (fn (exprs pattern new-source) (let ((nodes (if (list? exprs) exprs (list exprs))) (matches (find-all nodes pattern))) (if (empty? matches) {:error (str "No nodes matching pattern: " pattern)} (let ((target-path (first (first matches))) (fragment (sx-parse new-source))) (if (empty? fragment) {:error (str "Failed to parse new source: " new-source)} (let ((new-node (first fragment)) (result (tree-set nodes target-path new-node))) (if (nil? result) {:error (str "Failed to set node at path " (path-str target-path))} {:ok result :path target-path}))))))))
|
||||
|
||||
(define replace-all-by-pattern :effects () (fn (exprs pattern new-source) (let ((nodes (if (list? exprs) exprs (list exprs))) (matches (find-all nodes pattern)) (fragment (sx-parse new-source))) (if (empty? matches) {:error (str "No nodes matching pattern: " pattern)} (if (empty? fragment) {:error (str "Failed to parse new source: " new-source)} (let ((new-node (first fragment)) (current nodes) (count 0)) (for-each (fn (i) (let ((idx (- (- (len matches) 1) i)) (match (nth matches idx)) (target-path (first match)) (result (tree-set current target-path new-node))) (when (not (nil? result)) (set! current result) (set! count (+ count 1))))) (range 0 (len matches))) {:count count :ok current}))))))
|
||||
|
||||
(define insert-near-pattern :effects () (fn (exprs pattern position new-source) (let ((nodes (if (list? exprs) exprs (list exprs))) (matches (find-all nodes pattern))) (if (empty? matches) {:error (str "No nodes matching pattern: " pattern)} (let ((match-path (first (first matches))) (fragment (sx-parse new-source))) (if (empty? fragment) {:error (str "Failed to parse new source: " new-source)} (if (empty? match-path) {:error "Cannot insert near root node"} (let ((top-idx (first match-path)) (insert-idx (if (= position "after") (+ top-idx 1) top-idx)) (new-node (first fragment)) (new-tree (list-insert nodes insert-idx new-node))) {:ok new-tree :path (list insert-idx)}))))))))
|
||||
|
||||
;; --- Format / lint checks ---
|
||||
|
||||
(define lint-file :effects ()
|
||||
(fn (exprs)
|
||||
(let ((nodes (if (list? exprs) exprs (list exprs)))
|
||||
(warnings (list)))
|
||||
(for-each (fn (i) (lint-node (nth nodes i) (list i) warnings))
|
||||
(range 0 (len nodes)))
|
||||
warnings)))
|
||||
|
||||
(define lint-node :effects ()
|
||||
(fn (node path warnings)
|
||||
(when (list? node)
|
||||
(when (not (empty? node))
|
||||
(let ((head (first node))
|
||||
(head-name (if (= (type-of head) "symbol") (symbol-name head) "")))
|
||||
;; Empty let/letrec bindings
|
||||
(when (or (= head-name "let") (= head-name "letrec"))
|
||||
(when (>= (len node) 2)
|
||||
(let ((bindings (nth node 1)))
|
||||
(when (and (list? bindings) (empty? bindings))
|
||||
(append! warnings
|
||||
(str "WARN " (path-str path) ": " head-name " with empty bindings"))))))
|
||||
;; defcomp/defisland with too few args
|
||||
(when (or (= head-name "defcomp") (= head-name "defisland"))
|
||||
(when (< (len node) 4)
|
||||
(append! warnings
|
||||
(str "ERROR " (path-str path) ": " head-name " needs (name params body), has "
|
||||
(- (len node) 1) " args"))))
|
||||
;; define with no body
|
||||
(when (= head-name "define")
|
||||
(let ((effective-len (len (filter (fn (x) (not (= (type-of x) "keyword"))) (rest node)))))
|
||||
(when (< effective-len 2)
|
||||
(append! warnings
|
||||
(str "WARN " (path-str path) ": define with no body")))))
|
||||
;; Duplicate keys in keyword args
|
||||
(when (or (= head-name "defcomp") (= head-name "defisland"))
|
||||
(when (>= (len node) 3)
|
||||
(let ((params (nth node 2)))
|
||||
(when (list? params)
|
||||
(let ((seen (list)))
|
||||
(for-each (fn (p)
|
||||
(when (= (type-of p) "symbol")
|
||||
(let ((pname (symbol-name p)))
|
||||
(when (and (not (= pname "&key"))
|
||||
(not (= pname "&rest"))
|
||||
(not (starts-with? pname "&")))
|
||||
(when (contains? seen pname)
|
||||
(append! warnings
|
||||
(str "ERROR " (path-str path) ": duplicate param " pname)))
|
||||
(append! seen pname)))))
|
||||
params))))))
|
||||
;; Recurse into children
|
||||
(for-each (fn (i) (lint-node (nth node i) (concat path (list i)) warnings))
|
||||
(range 0 (len node))))))))
|
||||
|
||||
Reference in New Issue
Block a user