nodecanvas-cli 0.1.0__tar.gz

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 (99) hide show
  1. nodecanvas_cli-0.1.0/.claude-plugin/marketplace.json +14 -0
  2. nodecanvas_cli-0.1.0/.claude-plugin/plugin.json +12 -0
  3. nodecanvas_cli-0.1.0/.github/workflows/ci.yml +31 -0
  4. nodecanvas_cli-0.1.0/.github/workflows/release.yml +36 -0
  5. nodecanvas_cli-0.1.0/.gitignore +10 -0
  6. nodecanvas_cli-0.1.0/CHANGELOG.md +20 -0
  7. nodecanvas_cli-0.1.0/LICENSE +21 -0
  8. nodecanvas_cli-0.1.0/PKG-INFO +228 -0
  9. nodecanvas_cli-0.1.0/README.md +197 -0
  10. nodecanvas_cli-0.1.0/pyproject.toml +53 -0
  11. nodecanvas_cli-0.1.0/scripts/roundtrip_check.py +30 -0
  12. nodecanvas_cli-0.1.0/skills/nodecanvas-cli/SKILL.md +256 -0
  13. nodecanvas_cli-0.1.0/skills/nodecanvas-cli/references/ops.md +160 -0
  14. nodecanvas_cli-0.1.0/skills/nodecanvas-cli/scripts/nodecanvas-cli +15 -0
  15. nodecanvas_cli-0.1.0/src/nodecanvas_cli/__init__.py +1 -0
  16. nodecanvas_cli-0.1.0/src/nodecanvas_cli/__main__.py +5 -0
  17. nodecanvas_cli-0.1.0/src/nodecanvas_cli/assets.py +627 -0
  18. nodecanvas_cli-0.1.0/src/nodecanvas_cli/bindings.py +332 -0
  19. nodecanvas_cli-0.1.0/src/nodecanvas_cli/build.py +696 -0
  20. nodecanvas_cli-0.1.0/src/nodecanvas_cli/builtin.py +425 -0
  21. nodecanvas_cli-0.1.0/src/nodecanvas_cli/cli.py +330 -0
  22. nodecanvas_cli-0.1.0/src/nodecanvas_cli/config.py +57 -0
  23. nodecanvas_cli-0.1.0/src/nodecanvas_cli/container.py +215 -0
  24. nodecanvas_cli-0.1.0/src/nodecanvas_cli/csharp.py +521 -0
  25. nodecanvas_cli-0.1.0/src/nodecanvas_cli/dialogue.py +422 -0
  26. nodecanvas_cli-0.1.0/src/nodecanvas_cli/discover.py +155 -0
  27. nodecanvas_cli-0.1.0/src/nodecanvas_cli/explain.py +540 -0
  28. nodecanvas_cli-0.1.0/src/nodecanvas_cli/fsjson.py +100 -0
  29. nodecanvas_cli-0.1.0/src/nodecanvas_cli/layout.py +259 -0
  30. nodecanvas_cli-0.1.0/src/nodecanvas_cli/lint.py +421 -0
  31. nodecanvas_cli-0.1.0/src/nodecanvas_cli/mcp_server.py +305 -0
  32. nodecanvas_cli-0.1.0/src/nodecanvas_cli/model.py +236 -0
  33. nodecanvas_cli-0.1.0/src/nodecanvas_cli/notation.txt +6 -0
  34. nodecanvas_cli-0.1.0/src/nodecanvas_cli/ops.py +789 -0
  35. nodecanvas_cli-0.1.0/src/nodecanvas_cli/owners.py +833 -0
  36. nodecanvas_cli-0.1.0/src/nodecanvas_cli/project.py +168 -0
  37. nodecanvas_cli-0.1.0/src/nodecanvas_cli/refactor.py +398 -0
  38. nodecanvas_cli-0.1.0/src/nodecanvas_cli/refs.py +108 -0
  39. nodecanvas_cli-0.1.0/src/nodecanvas_cli/render.py +612 -0
  40. nodecanvas_cli-0.1.0/src/nodecanvas_cli/scaffold.py +261 -0
  41. nodecanvas_cli-0.1.0/src/nodecanvas_cli/schema.py +451 -0
  42. nodecanvas_cli-0.1.0/src/nodecanvas_cli/spec.py +363 -0
  43. nodecanvas_cli-0.1.0/src/nodecanvas_cli/typenames.py +193 -0
  44. nodecanvas_cli-0.1.0/src/nodecanvas_cli/unity_verify.cs.txt +135 -0
  45. nodecanvas_cli-0.1.0/src/nodecanvas_cli/verify.py +372 -0
  46. nodecanvas_cli-0.1.0/src/nodecanvas_cli/views.py +230 -0
  47. nodecanvas_cli-0.1.0/src/nodecanvas_cli/workspace.py +165 -0
  48. nodecanvas_cli-0.1.0/src/nodecanvas_cli/yamlscalar.py +235 -0
  49. nodecanvas_cli-0.1.0/tests/conftest.py +38 -0
  50. nodecanvas_cli-0.1.0/tests/fixtures/generate.py +841 -0
  51. nodecanvas_cli-0.1.0/tests/fixtures/project/Assets/Blackboards/Shared.asset +18 -0
  52. nodecanvas_cli-0.1.0/tests/fixtures/project/Assets/Blackboards/Shared.asset.meta +4 -0
  53. nodecanvas_cli-0.1.0/tests/fixtures/project/Assets/Game/Bot.cs +36 -0
  54. nodecanvas_cli-0.1.0/tests/fixtures/project/Assets/Game/Bot.cs.meta +2 -0
  55. nodecanvas_cli-0.1.0/tests/fixtures/project/Assets/Game/GuardOwner.cs +4 -0
  56. nodecanvas_cli-0.1.0/tests/fixtures/project/Assets/Game/GuardOwner.cs.meta +2 -0
  57. nodecanvas_cli-0.1.0/tests/fixtures/project/Assets/Graphs/Bot_BT.asset +34 -0
  58. nodecanvas_cli-0.1.0/tests/fixtures/project/Assets/Graphs/Bot_BT.asset.meta +8 -0
  59. nodecanvas_cli-0.1.0/tests/fixtures/project/Assets/Graphs/Bot_FSM.asset +29 -0
  60. nodecanvas_cli-0.1.0/tests/fixtures/project/Assets/Graphs/Bot_FSM.asset.meta +8 -0
  61. nodecanvas_cli-0.1.0/tests/fixtures/project/Assets/Graphs/Broken_BT.asset +25 -0
  62. nodecanvas_cli-0.1.0/tests/fixtures/project/Assets/Graphs/Broken_BT.asset.meta +8 -0
  63. nodecanvas_cli-0.1.0/tests/fixtures/project/Assets/Graphs/Patrol_BT.asset +27 -0
  64. nodecanvas_cli-0.1.0/tests/fixtures/project/Assets/Graphs/Patrol_BT.asset.meta +8 -0
  65. nodecanvas_cli-0.1.0/tests/fixtures/project/Assets/Graphs/Sub_BT.asset +27 -0
  66. nodecanvas_cli-0.1.0/tests/fixtures/project/Assets/Graphs/Sub_BT.asset.meta +8 -0
  67. nodecanvas_cli-0.1.0/tests/fixtures/project/Assets/Graphs/Talk_DT.asset +29 -0
  68. nodecanvas_cli-0.1.0/tests/fixtures/project/Assets/Graphs/Talk_DT.asset.meta +8 -0
  69. nodecanvas_cli-0.1.0/tests/fixtures/project/Assets/Prefabs/Guard.prefab +75 -0
  70. nodecanvas_cli-0.1.0/tests/fixtures/project/Assets/Prefabs/Guard.prefab.meta +4 -0
  71. nodecanvas_cli-0.1.0/tests/fixtures/project/Assets/Prefabs/Sentry.prefab +37 -0
  72. nodecanvas_cli-0.1.0/tests/fixtures/project/Assets/Prefabs/Sentry.prefab.meta +4 -0
  73. nodecanvas_cli-0.1.0/tests/fixtures/project/Assets/Scenes/World.unity +91 -0
  74. nodecanvas_cli-0.1.0/tests/fixtures/project/Assets/Scenes/World.unity.meta +4 -0
  75. nodecanvas_cli-0.1.0/tests/fixtures/project/Assets/Stubs/NodeCanvasStubs.cs +423 -0
  76. nodecanvas_cli-0.1.0/tests/fixtures/project/Assets/Stubs/NodeCanvasStubs.cs.meta +2 -0
  77. nodecanvas_cli-0.1.0/tests/fixtures/project/ProjectSettings/ProjectVersion.txt +1 -0
  78. nodecanvas_cli-0.1.0/tests/snapshots/Bot_BT.txt +12 -0
  79. nodecanvas_cli-0.1.0/tests/snapshots/Bot_FSM.txt +9 -0
  80. nodecanvas_cli-0.1.0/tests/snapshots/Broken_BT.txt +11 -0
  81. nodecanvas_cli-0.1.0/tests/snapshots/Patrol_BT.txt +8 -0
  82. nodecanvas_cli-0.1.0/tests/snapshots/Sentry.txt +3 -0
  83. nodecanvas_cli-0.1.0/tests/snapshots/Sub_BT.txt +6 -0
  84. nodecanvas_cli-0.1.0/tests/snapshots/Talk_DT.txt +16 -0
  85. nodecanvas_cli-0.1.0/tests/test_apply.py +622 -0
  86. nodecanvas_cli-0.1.0/tests/test_assets.py +129 -0
  87. nodecanvas_cli-0.1.0/tests/test_codec.py +73 -0
  88. nodecanvas_cli-0.1.0/tests/test_csharp.py +105 -0
  89. nodecanvas_cli-0.1.0/tests/test_dialogue.py +140 -0
  90. nodecanvas_cli-0.1.0/tests/test_explain.py +80 -0
  91. nodecanvas_cli-0.1.0/tests/test_lint.py +274 -0
  92. nodecanvas_cli-0.1.0/tests/test_mcp.py +68 -0
  93. nodecanvas_cli-0.1.0/tests/test_owners.py +116 -0
  94. nodecanvas_cli-0.1.0/tests/test_read.py +134 -0
  95. nodecanvas_cli-0.1.0/tests/test_refactor.py +168 -0
  96. nodecanvas_cli-0.1.0/tests/test_scaffold.py +60 -0
  97. nodecanvas_cli-0.1.0/tests/test_spec.py +42 -0
  98. nodecanvas_cli-0.1.0/tests/test_verify.py +131 -0
  99. nodecanvas_cli-0.1.0/uv.lock +1154 -0
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "nodecanvas-cli",
3
+ "description": "NodeCanvas graphs (Behaviour Trees, FSMs, Dialogue Trees) for coding agents.",
4
+ "owner": {
5
+ "name": "Aleksandr Esipov"
6
+ },
7
+ "plugins": [
8
+ {
9
+ "name": "nodecanvas-cli",
10
+ "source": "./",
11
+ "description": "Read, search, lint and edit NodeCanvas graphs (Behaviour Trees, FSMs, Dialogue Trees) in Unity projects."
12
+ }
13
+ ]
14
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "nodecanvas-cli",
3
+ "version": "0.1.0",
4
+ "description": "Read, search, lint and edit NodeCanvas graphs (Behaviour Trees, FSMs, Dialogue Trees) in Unity projects through the nodecanvas-cli command, without opening Unity.",
5
+ "author": {
6
+ "name": "Aleksandr Esipov"
7
+ },
8
+ "homepage": "https://github.com/Primeer/nodecanvas-cli",
9
+ "repository": "https://github.com/Primeer/nodecanvas-cli",
10
+ "license": "MIT",
11
+ "keywords": ["unity", "nodecanvas", "behaviour-tree", "fsm", "dialogue-tree", "game-ai"]
12
+ }
@@ -0,0 +1,31 @@
1
+ name: CI
2
+ on: [push, pull_request]
3
+ jobs:
4
+ test:
5
+ runs-on: ubuntu-latest
6
+ strategy:
7
+ matrix:
8
+ python: ["3.9", "3.10", "3.11", "3.12", "3.13"]
9
+ steps:
10
+ - uses: actions/checkout@v4
11
+ - uses: actions/setup-python@v5
12
+ with:
13
+ python-version: ${{ matrix.python }}
14
+ - run: pip install -e ".[dev]"
15
+ - run: pytest -q
16
+ build:
17
+ runs-on: ubuntu-latest
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+ - uses: actions/setup-python@v5
21
+ with:
22
+ python-version: "3.12"
23
+ - run: pip install build twine
24
+ - run: python -m build
25
+ - run: twine check --strict dist/*
26
+ - name: Smoke-test the wheel
27
+ run: |
28
+ python -m venv /tmp/w && /tmp/w/bin/pip install dist/*.whl
29
+ /tmp/w/bin/nodecanvas-cli --version
30
+ /tmp/w/bin/nodecanvas-cli --project tests/fixtures/project list
31
+ /tmp/w/bin/python -c "import nodecanvas_cli.verify as v; assert v.SNIPPET.exists()"
@@ -0,0 +1,36 @@
1
+ name: Release
2
+ # Publishes to PyPI through Trusted Publishing (no tokens in the repository).
3
+ # One-time setup on pypi.org: add a trusted publisher for this repository,
4
+ # workflow "release.yml", environment "pypi".
5
+ on:
6
+ push:
7
+ tags: ["v*"]
8
+ jobs:
9
+ build:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: actions/setup-python@v5
14
+ with:
15
+ python-version: "3.12"
16
+ - name: Tag matches the package version
17
+ run: |
18
+ v=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
19
+ test "v$v" = "${GITHUB_REF_NAME}" || { echo "tag ${GITHUB_REF_NAME} != v$v"; exit 1; }
20
+ - run: pip install build twine && python -m build && twine check --strict dist/*
21
+ - uses: actions/upload-artifact@v4
22
+ with:
23
+ name: dist
24
+ path: dist/
25
+ publish:
26
+ needs: build
27
+ runs-on: ubuntu-latest
28
+ environment: pypi
29
+ permissions:
30
+ id-token: write
31
+ steps:
32
+ - uses: actions/download-artifact@v4
33
+ with:
34
+ name: dist
35
+ path: dist/
36
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,10 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .venv/
6
+ dist/
7
+ build/
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+ .DS_Store
@@ -0,0 +1,20 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 — 2026-10-12
4
+
5
+ First public release.
6
+
7
+ - `list`, `show`, `vars`, `node`, `diff`: compact views of Behaviour Trees, FSMs and Dialogue Trees in graph
8
+ assets and in graphs bound to GraphOwners in prefabs and scenes.
9
+ - `refs`, `why`, `paths`, `stats`: reverse lookup of members, subtrees, variables and task types across all
10
+ graphs; conditions on the way to a node; every call of a member or write of a variable.
11
+ - `lint`: missing types, dangling blackboard references, broken reflection bindings, subgraph mappings,
12
+ connection limits, unreachable nodes, owner and global variables.
13
+ - `apply`: atomic, validated batches of edit operations with short value forms and `--dry-run` diffs;
14
+ `new`, `extract`, `inline`, `copy`, `refactor rename-member|rename-type|retarget`, `scaffold`.
15
+ - `dt export` / `dt import` / `dt lint`: dialogue lines and translations to CSV/JSON and back.
16
+ - `owners`, `blackboards`, `types`, `members`.
17
+ - `verify --unity`: load the edited graphs in the open Unity Editor and report what NodeCanvas sees.
18
+ - `mcp`: stdio MCP server (`nodecanvas-cli[mcp]`, Python 3.10+).
19
+ - Claude Code plugin with a skill and a bundled launcher.
20
+ - Byte-exact round-trip of unchanged graphs; Unity's YAML folding and FullSerializer key order on save.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aleksandr Esipov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,228 @@
1
+ Metadata-Version: 2.5
2
+ Name: nodecanvas-cli
3
+ Version: 0.1.0
4
+ Summary: Read, query, lint and edit NodeCanvas graphs (Behaviour Trees, FSMs, Dialogue Trees) in Unity projects from the command line
5
+ Project-URL: Homepage, https://github.com/Primeer/nodecanvas-cli
6
+ Project-URL: Issues, https://github.com/Primeer/nodecanvas-cli/issues
7
+ Project-URL: Changelog, https://github.com/Primeer/nodecanvas-cli/blob/main/CHANGELOG.md
8
+ Author: Aleksandr Esipov
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: ai-agents,behaviour-tree,cli,dialogue-tree,fsm,nodecanvas,unity
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Games/Entertainment
23
+ Classifier: Topic :: Software Development :: Libraries
24
+ Requires-Python: >=3.9
25
+ Provides-Extra: dev
26
+ Requires-Dist: mcp>=2.3; (python_version >= '3.10') and extra == 'dev'
27
+ Requires-Dist: pytest>=7; extra == 'dev'
28
+ Provides-Extra: mcp
29
+ Requires-Dist: mcp>=2.3; (python_version >= '3.10') and extra == 'mcp'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # nodecanvas-cli
33
+
34
+ Read, search, lint and edit [NodeCanvas](https://nodecanvas.paradoxnotion.com/) graphs — Behaviour Trees,
35
+ FSMs and Dialogue Trees — in a Unity project from the command line, without opening Unity.
36
+
37
+ Built for coding agents (it ships as a Claude Code plugin) and just as usable by hand.
38
+
39
+ NodeCanvas stores a graph as one long JSON string inside a Unity YAML asset. A 120-node behaviour tree
40
+ is ~85 KB of folded JSON with GUID-linked variables and index-linked nodes. `nodecanvas-cli` shows the same tree
41
+ in ~10 KB of readable text, answers "where is this used?", and applies edits as validated batches that
42
+ are written back exactly the way Unity writes them.
43
+
44
+ ```
45
+ $ nodecanvas-cli show Bot_BT
46
+ Bot_BT [BT, repeat] Assets/Graphs/Bot_BT.asset nodes=10
47
+ vars: Self:Bot(pub) Target:Unit Alert:bool Cool time:float Speed:float
48
+ #0 Selector
49
+ ├─#1 ? @Self.IsDead
50
+ ├─#2 Sequencer "Fight"
51
+ │ ├─#3 ? !$Alert
52
+ │ ├─#4 Cooldown [coolDownTime=$Cool time]
53
+ │ │ └─#5 @Self.Attack($Target)
54
+ │ └─#6 ProbabilitySelector
55
+ │ ├─(w=1.0) #7 @Self.MoveTo((1.0, 0, -2.5))
56
+ │ └─(w=3.0) #8 SubTree → Sub_BT {Self←$Self, Alert→$Alert}
57
+ └─[off] #9 $Speed *= 2.0 // double the speed
58
+ ```
59
+
60
+ ## Features
61
+
62
+ - **show** — compact tree (BT), state list with ordered transitions (FSM), or script (Dialogue Tree).
63
+ Default values are shown with their real meaning, read from the project's own C# sources.
64
+ - **refs** — reverse lookup across all graphs: who calls `Bot.Attack`, which graphs use a
65
+ subtree, a variable, or a task type. Graphs bind C# members by name and break silently on renames;
66
+ this finds them.
67
+ - **lint** — missing task/variable types, dangling blackboard references, reflection bindings to members
68
+ that no longer exist, missing subgraphs and stale variable mappings, too many children for a node
69
+ type, unreachable nodes.
70
+ - **apply** — atomic batches of edit operations: add / remove / move / wrap / unwrap / replace nodes, set
71
+ fields, connect and disable, blackboard variables, subgraph mappings, FSM transitions, dialogue
72
+ choices. Short value forms (`"$Var"`, enum names, `[x, y, z]`, `{"call": "Type.Method", ...}`) are
73
+ converted using the C# field types. Batches that introduce lint errors are not saved.
74
+ - **dt export / import / lint** — dialogue lines (statements, choices, translations) to CSV/JSON for writers
75
+ and translators and back, keyed by node UIDs, with conflict detection; checks for empty lines,
76
+ dangling choices and jumps, unknown actors and missing translations.
77
+ - **why / paths / stats** — why control reaches a node (conditions along the path from the root, FSM
78
+ transitions in, dialogue choices), the same for every call of a member or write of a variable, and
79
+ graph statistics.
80
+ - **owners / blackboards** — which prefabs and scenes run a graph (GraphOwner components, exposed
81
+ variable overrides, owner blackboards), graphs assigned nowhere, and Blackboard / GlobalBlackboard /
82
+ AssetBlackboard contents; lint checks global variables and owner overrides against them.
83
+ - **types / members** — discover task and node types with their fields, defaults and enum members, and the
84
+ properties/methods reflection tasks can call.
85
+ - Works on graph assets (`.asset`) and graphs bound to a `GraphOwner` in prefabs and scenes.
86
+ - Byte-exact: unchanged graphs round-trip identically; edited graphs are re-serialized with Unity's
87
+ YAML folding and FullSerializer's key order, so diffs stay minimal.
88
+ - **verify --unity** — after editing, let the open Unity Editor import, deserialize and validate the graphs
89
+ (through the [Unity CLI](https://docs.unity.com/) and the project's `com.unity.pipeline` package) and
90
+ report NodeCanvas errors, Missing types, broken reflection bindings and console messages.
91
+ - Pure Python ≥ 3.9, no dependencies, no Unity required (except for `verify --unity`); the optional MCP
92
+ server needs `nodecanvas-cli[mcp]`.
93
+
94
+ ## Install
95
+
96
+ ```bash
97
+ pipx install nodecanvas-cli # once published to PyPI
98
+ uvx --from git+https://github.com/Primeer/nodecanvas-cli nodecanvas-cli --help # straight from the repository
99
+ python -m nodecanvas_cli --help # from a checkout, with src/ on PYTHONPATH
100
+ ```
101
+
102
+ The Claude Code plugin also bundles a launcher, `skills/nodecanvas-cli/scripts/nodecanvas-cli`, which runs the CLI
103
+ from the plugin's own sources with any Python 3.9+.
104
+
105
+ ### Claude Code plugin
106
+
107
+ The repository is also a Claude Code plugin marketplace containing the `nodecanvas-cli` plugin, which adds a
108
+ skill that teaches the agent when and how to use the CLI:
109
+
110
+ ```
111
+ /plugin marketplace add Primeer/nodecanvas-cli
112
+ /plugin install nodecanvas-cli@nodecanvas-cli
113
+ ```
114
+
115
+ ### MCP server (Cursor, Codex, Gemini and other agents)
116
+
117
+ ```bash
118
+ pipx install "nodecanvas-cli[mcp]" # Python 3.10+
119
+ nodecanvas-cli --project /path/to/UnityProject mcp
120
+ ```
121
+
122
+ A stdio MCP server whose tools wrap the commands (`show`, `node`, `refs`, `lint`, `apply`, `refactor_*`,
123
+ `why`, `owners`, …) and return the same compact text. Tools that write files (`apply`, `refactor_*`,
124
+ `new_graph`, `extract`, `inline`, `copy`) default to `dry_run=true`; pass `dry_run=false` to save.
125
+ Example client config:
126
+
127
+ ```json
128
+ {"mcpServers": {"nodecanvas-cli": {"command": "nodecanvas-cli", "args": ["--project", "/path/to/UnityProject", "mcp"]}}}
129
+ ```
130
+
131
+ ## Usage
132
+
133
+ Run inside a Unity project (any subdirectory) or pass `--project <root>`.
134
+
135
+ ```bash
136
+ nodecanvas-cli list [--kind bt|fsm|dt]
137
+ nodecanvas-cli show <graph> [--node N] [--depth D] [--expand]
138
+ nodecanvas-cli vars <graph>
139
+ nodecanvas-cli node <graph> <id>... [--raw] # spec form accepted by apply (or raw JSON)
140
+ nodecanvas-cli diff <graph> [--rev HEAD] # compact view vs a git revision
141
+ nodecanvas-cli refs <Type.Member | GraphName | $Variable | TaskType>
142
+ nodecanvas-cli dt export [graph...] [--locales en,ru] [--format csv|json] [-o file] | dt import <file> | dt lint
143
+ nodecanvas-cli why <graph> <node> [--expand] # conditions on the way from the root to a node
144
+ nodecanvas-cli paths [graph] --to <Type.Member | $Var> [--reads]
145
+ nodecanvas-cli stats [graph] [--json]
146
+ nodecanvas-cli scaffold action|condition|decorator|composite <Name> --dir Assets/... [--field "x:BBParameter<float>=1"]
147
+ nodecanvas-cli owners [<graph> | --all] # GraphOwners in prefabs/scenes, parent graphs
148
+ nodecanvas-cli blackboards [--global]
149
+ nodecanvas-cli lint [graph...] [--min info|warning|error] [--json]
150
+ nodecanvas-cli types [query] [--role action|condition|bt|composite|decorator|fsm|dt]
151
+ nodecanvas-cli members <Type> [filter]
152
+ nodecanvas-cli refactor rename-member|rename-type|retarget ... [--dry-run]
153
+ nodecanvas-cli new <path.asset> --kind bt|fsm|dt [--var "Name:type[=value][:pub]"]
154
+ nodecanvas-cli extract <graph> <node> <new.asset> | inline <graph> <node> | copy <src> <node> <dst> <parent>
155
+ nodecanvas-cli apply <graph> <ops.json | -> [--dry-run] [--force]
156
+ nodecanvas-cli verify --unity [graph... | --changed] [--resave] # load the graphs in the open Unity Editor
157
+ ```
158
+
159
+ `<graph>` is a path, an asset name, a unique prefix, or `Prefab.prefab:GameObject` for bound graphs;
160
+ `--kind bt|fsm|dt` disambiguates names shared by graphs of different kinds.
161
+
162
+ Layout of created/moved nodes can be tuned with `nodecanvas-cli.json` in the Unity project root:
163
+
164
+ ```json
165
+ {"layout": {"grid": 20, "level_gap": 200, "sibling_gap": 300}, "scaffold": {"comments": false}}
166
+ ```
167
+
168
+ ### Editing example
169
+
170
+ ```json
171
+ [
172
+ {"op": "add", "parent": "3", "index": 0, "id": "w",
173
+ "node": {"$type": "ActionNode", "_action": {"$type": "Wait", "waitTime": "$Delay"}}},
174
+ {"op": "wrap", "node": "w", "with": {"$type": "Cooldown", "coolDownTime": 2}},
175
+ {"op": "add", "parent": "3", "node": {"$type": "ConditionNode",
176
+ "_condition": {"check": "Bot.DistanceTo", "args": ["$Target"], "agent": "$Owner", "compare": ">", "value": "$Range"}}},
177
+ {"op": "var-add", "name": "Range", "type": "float", "value": 4},
178
+ {"op": "remove", "node": "12", "recursive": true}
179
+ ]
180
+ ```
181
+
182
+ ```bash
183
+ nodecanvas-cli apply Bot_BT ops.json --dry-run # prints a before/after diff of the compact view
184
+ nodecanvas-cli apply Bot_BT ops.json
185
+ ```
186
+
187
+ All operations and value forms: [skills/nodecanvas-cli/references/ops.md](skills/nodecanvas-cli/references/ops.md).
188
+
189
+ ## How it works
190
+
191
+ - **Container codec** — finds `_serializedGraph` (assets) or `_boundGraphSerialization` (GraphOwner
192
+ components) and decodes Unity's folded single-quoted YAML scalar; encoding reproduces Unity's folding
193
+ rule exactly, so untouched graphs stay byte-identical.
194
+ - **FullSerializer JSON** — numbers keep their original text and keys their order; new objects are
195
+ written in FullSerializer's field order. Semantics of omitted values follow FullSerializer: a missing
196
+ field keeps its C# initializer, while a serialized parameter without `_value` holds `default(T)`.
197
+ - **Schema from sources** — NodeCanvas ships as source code, so nodecanvas-cli scans the project's C# files
198
+ (lazily, with an mtime cache in `~/.cache/nodecanvas-cli`) for task/node fields, initializers, enums,
199
+ display names, connection limits and the methods available to reflection tasks. Without NodeCanvas
200
+ sources, reading and editing still work with reduced detail.
201
+ - **Normalization** — after an edit, nodes are renumbered the way the NodeCanvas editor does
202
+ (depth-first from the root), `$id`s are written only on referenced nodes, and BT children keep a
203
+ left-to-right position order matching their execution order.
204
+
205
+ ## Limitations
206
+
207
+ - NodeCanvas 3.x serialization only.
208
+ - Graphs serialized to an external JSON file (`_externalSerializationFile`) are not supported.
209
+ - C# scanning is heuristic (no compiler): unusual declarations may be missed; lint reports unresolved
210
+ members as warnings when a type's base classes live outside the project sources.
211
+ - Close the graph in the NodeCanvas editor (or let Unity reimport) before editing it externally.
212
+
213
+ ## Development
214
+
215
+ ```bash
216
+ pip install -e ".[dev]"
217
+ pytest
218
+ python tests/fixtures/generate.py # regenerate synthetic fixtures
219
+ UPDATE_SNAPSHOTS=1 pytest # refresh show snapshots
220
+ python scripts/roundtrip_check.py <UnityProject> # verify byte-exact round-trip on a real project
221
+ ```
222
+
223
+ Tests run against synthetic graphs and small stubs of the NodeCanvas API surface
224
+ (`tests/fixtures/project/Assets/Stubs`); NodeCanvas itself is a commercial asset and is not included.
225
+
226
+ ## License
227
+
228
+ MIT. NodeCanvas is a product of Paradox Notion; this project is not affiliated with it.
@@ -0,0 +1,197 @@
1
+ # nodecanvas-cli
2
+
3
+ Read, search, lint and edit [NodeCanvas](https://nodecanvas.paradoxnotion.com/) graphs — Behaviour Trees,
4
+ FSMs and Dialogue Trees — in a Unity project from the command line, without opening Unity.
5
+
6
+ Built for coding agents (it ships as a Claude Code plugin) and just as usable by hand.
7
+
8
+ NodeCanvas stores a graph as one long JSON string inside a Unity YAML asset. A 120-node behaviour tree
9
+ is ~85 KB of folded JSON with GUID-linked variables and index-linked nodes. `nodecanvas-cli` shows the same tree
10
+ in ~10 KB of readable text, answers "where is this used?", and applies edits as validated batches that
11
+ are written back exactly the way Unity writes them.
12
+
13
+ ```
14
+ $ nodecanvas-cli show Bot_BT
15
+ Bot_BT [BT, repeat] Assets/Graphs/Bot_BT.asset nodes=10
16
+ vars: Self:Bot(pub) Target:Unit Alert:bool Cool time:float Speed:float
17
+ #0 Selector
18
+ ├─#1 ? @Self.IsDead
19
+ ├─#2 Sequencer "Fight"
20
+ │ ├─#3 ? !$Alert
21
+ │ ├─#4 Cooldown [coolDownTime=$Cool time]
22
+ │ │ └─#5 @Self.Attack($Target)
23
+ │ └─#6 ProbabilitySelector
24
+ │ ├─(w=1.0) #7 @Self.MoveTo((1.0, 0, -2.5))
25
+ │ └─(w=3.0) #8 SubTree → Sub_BT {Self←$Self, Alert→$Alert}
26
+ └─[off] #9 $Speed *= 2.0 // double the speed
27
+ ```
28
+
29
+ ## Features
30
+
31
+ - **show** — compact tree (BT), state list with ordered transitions (FSM), or script (Dialogue Tree).
32
+ Default values are shown with their real meaning, read from the project's own C# sources.
33
+ - **refs** — reverse lookup across all graphs: who calls `Bot.Attack`, which graphs use a
34
+ subtree, a variable, or a task type. Graphs bind C# members by name and break silently on renames;
35
+ this finds them.
36
+ - **lint** — missing task/variable types, dangling blackboard references, reflection bindings to members
37
+ that no longer exist, missing subgraphs and stale variable mappings, too many children for a node
38
+ type, unreachable nodes.
39
+ - **apply** — atomic batches of edit operations: add / remove / move / wrap / unwrap / replace nodes, set
40
+ fields, connect and disable, blackboard variables, subgraph mappings, FSM transitions, dialogue
41
+ choices. Short value forms (`"$Var"`, enum names, `[x, y, z]`, `{"call": "Type.Method", ...}`) are
42
+ converted using the C# field types. Batches that introduce lint errors are not saved.
43
+ - **dt export / import / lint** — dialogue lines (statements, choices, translations) to CSV/JSON for writers
44
+ and translators and back, keyed by node UIDs, with conflict detection; checks for empty lines,
45
+ dangling choices and jumps, unknown actors and missing translations.
46
+ - **why / paths / stats** — why control reaches a node (conditions along the path from the root, FSM
47
+ transitions in, dialogue choices), the same for every call of a member or write of a variable, and
48
+ graph statistics.
49
+ - **owners / blackboards** — which prefabs and scenes run a graph (GraphOwner components, exposed
50
+ variable overrides, owner blackboards), graphs assigned nowhere, and Blackboard / GlobalBlackboard /
51
+ AssetBlackboard contents; lint checks global variables and owner overrides against them.
52
+ - **types / members** — discover task and node types with their fields, defaults and enum members, and the
53
+ properties/methods reflection tasks can call.
54
+ - Works on graph assets (`.asset`) and graphs bound to a `GraphOwner` in prefabs and scenes.
55
+ - Byte-exact: unchanged graphs round-trip identically; edited graphs are re-serialized with Unity's
56
+ YAML folding and FullSerializer's key order, so diffs stay minimal.
57
+ - **verify --unity** — after editing, let the open Unity Editor import, deserialize and validate the graphs
58
+ (through the [Unity CLI](https://docs.unity.com/) and the project's `com.unity.pipeline` package) and
59
+ report NodeCanvas errors, Missing types, broken reflection bindings and console messages.
60
+ - Pure Python ≥ 3.9, no dependencies, no Unity required (except for `verify --unity`); the optional MCP
61
+ server needs `nodecanvas-cli[mcp]`.
62
+
63
+ ## Install
64
+
65
+ ```bash
66
+ pipx install nodecanvas-cli # once published to PyPI
67
+ uvx --from git+https://github.com/Primeer/nodecanvas-cli nodecanvas-cli --help # straight from the repository
68
+ python -m nodecanvas_cli --help # from a checkout, with src/ on PYTHONPATH
69
+ ```
70
+
71
+ The Claude Code plugin also bundles a launcher, `skills/nodecanvas-cli/scripts/nodecanvas-cli`, which runs the CLI
72
+ from the plugin's own sources with any Python 3.9+.
73
+
74
+ ### Claude Code plugin
75
+
76
+ The repository is also a Claude Code plugin marketplace containing the `nodecanvas-cli` plugin, which adds a
77
+ skill that teaches the agent when and how to use the CLI:
78
+
79
+ ```
80
+ /plugin marketplace add Primeer/nodecanvas-cli
81
+ /plugin install nodecanvas-cli@nodecanvas-cli
82
+ ```
83
+
84
+ ### MCP server (Cursor, Codex, Gemini and other agents)
85
+
86
+ ```bash
87
+ pipx install "nodecanvas-cli[mcp]" # Python 3.10+
88
+ nodecanvas-cli --project /path/to/UnityProject mcp
89
+ ```
90
+
91
+ A stdio MCP server whose tools wrap the commands (`show`, `node`, `refs`, `lint`, `apply`, `refactor_*`,
92
+ `why`, `owners`, …) and return the same compact text. Tools that write files (`apply`, `refactor_*`,
93
+ `new_graph`, `extract`, `inline`, `copy`) default to `dry_run=true`; pass `dry_run=false` to save.
94
+ Example client config:
95
+
96
+ ```json
97
+ {"mcpServers": {"nodecanvas-cli": {"command": "nodecanvas-cli", "args": ["--project", "/path/to/UnityProject", "mcp"]}}}
98
+ ```
99
+
100
+ ## Usage
101
+
102
+ Run inside a Unity project (any subdirectory) or pass `--project <root>`.
103
+
104
+ ```bash
105
+ nodecanvas-cli list [--kind bt|fsm|dt]
106
+ nodecanvas-cli show <graph> [--node N] [--depth D] [--expand]
107
+ nodecanvas-cli vars <graph>
108
+ nodecanvas-cli node <graph> <id>... [--raw] # spec form accepted by apply (or raw JSON)
109
+ nodecanvas-cli diff <graph> [--rev HEAD] # compact view vs a git revision
110
+ nodecanvas-cli refs <Type.Member | GraphName | $Variable | TaskType>
111
+ nodecanvas-cli dt export [graph...] [--locales en,ru] [--format csv|json] [-o file] | dt import <file> | dt lint
112
+ nodecanvas-cli why <graph> <node> [--expand] # conditions on the way from the root to a node
113
+ nodecanvas-cli paths [graph] --to <Type.Member | $Var> [--reads]
114
+ nodecanvas-cli stats [graph] [--json]
115
+ nodecanvas-cli scaffold action|condition|decorator|composite <Name> --dir Assets/... [--field "x:BBParameter<float>=1"]
116
+ nodecanvas-cli owners [<graph> | --all] # GraphOwners in prefabs/scenes, parent graphs
117
+ nodecanvas-cli blackboards [--global]
118
+ nodecanvas-cli lint [graph...] [--min info|warning|error] [--json]
119
+ nodecanvas-cli types [query] [--role action|condition|bt|composite|decorator|fsm|dt]
120
+ nodecanvas-cli members <Type> [filter]
121
+ nodecanvas-cli refactor rename-member|rename-type|retarget ... [--dry-run]
122
+ nodecanvas-cli new <path.asset> --kind bt|fsm|dt [--var "Name:type[=value][:pub]"]
123
+ nodecanvas-cli extract <graph> <node> <new.asset> | inline <graph> <node> | copy <src> <node> <dst> <parent>
124
+ nodecanvas-cli apply <graph> <ops.json | -> [--dry-run] [--force]
125
+ nodecanvas-cli verify --unity [graph... | --changed] [--resave] # load the graphs in the open Unity Editor
126
+ ```
127
+
128
+ `<graph>` is a path, an asset name, a unique prefix, or `Prefab.prefab:GameObject` for bound graphs;
129
+ `--kind bt|fsm|dt` disambiguates names shared by graphs of different kinds.
130
+
131
+ Layout of created/moved nodes can be tuned with `nodecanvas-cli.json` in the Unity project root:
132
+
133
+ ```json
134
+ {"layout": {"grid": 20, "level_gap": 200, "sibling_gap": 300}, "scaffold": {"comments": false}}
135
+ ```
136
+
137
+ ### Editing example
138
+
139
+ ```json
140
+ [
141
+ {"op": "add", "parent": "3", "index": 0, "id": "w",
142
+ "node": {"$type": "ActionNode", "_action": {"$type": "Wait", "waitTime": "$Delay"}}},
143
+ {"op": "wrap", "node": "w", "with": {"$type": "Cooldown", "coolDownTime": 2}},
144
+ {"op": "add", "parent": "3", "node": {"$type": "ConditionNode",
145
+ "_condition": {"check": "Bot.DistanceTo", "args": ["$Target"], "agent": "$Owner", "compare": ">", "value": "$Range"}}},
146
+ {"op": "var-add", "name": "Range", "type": "float", "value": 4},
147
+ {"op": "remove", "node": "12", "recursive": true}
148
+ ]
149
+ ```
150
+
151
+ ```bash
152
+ nodecanvas-cli apply Bot_BT ops.json --dry-run # prints a before/after diff of the compact view
153
+ nodecanvas-cli apply Bot_BT ops.json
154
+ ```
155
+
156
+ All operations and value forms: [skills/nodecanvas-cli/references/ops.md](skills/nodecanvas-cli/references/ops.md).
157
+
158
+ ## How it works
159
+
160
+ - **Container codec** — finds `_serializedGraph` (assets) or `_boundGraphSerialization` (GraphOwner
161
+ components) and decodes Unity's folded single-quoted YAML scalar; encoding reproduces Unity's folding
162
+ rule exactly, so untouched graphs stay byte-identical.
163
+ - **FullSerializer JSON** — numbers keep their original text and keys their order; new objects are
164
+ written in FullSerializer's field order. Semantics of omitted values follow FullSerializer: a missing
165
+ field keeps its C# initializer, while a serialized parameter without `_value` holds `default(T)`.
166
+ - **Schema from sources** — NodeCanvas ships as source code, so nodecanvas-cli scans the project's C# files
167
+ (lazily, with an mtime cache in `~/.cache/nodecanvas-cli`) for task/node fields, initializers, enums,
168
+ display names, connection limits and the methods available to reflection tasks. Without NodeCanvas
169
+ sources, reading and editing still work with reduced detail.
170
+ - **Normalization** — after an edit, nodes are renumbered the way the NodeCanvas editor does
171
+ (depth-first from the root), `$id`s are written only on referenced nodes, and BT children keep a
172
+ left-to-right position order matching their execution order.
173
+
174
+ ## Limitations
175
+
176
+ - NodeCanvas 3.x serialization only.
177
+ - Graphs serialized to an external JSON file (`_externalSerializationFile`) are not supported.
178
+ - C# scanning is heuristic (no compiler): unusual declarations may be missed; lint reports unresolved
179
+ members as warnings when a type's base classes live outside the project sources.
180
+ - Close the graph in the NodeCanvas editor (or let Unity reimport) before editing it externally.
181
+
182
+ ## Development
183
+
184
+ ```bash
185
+ pip install -e ".[dev]"
186
+ pytest
187
+ python tests/fixtures/generate.py # regenerate synthetic fixtures
188
+ UPDATE_SNAPSHOTS=1 pytest # refresh show snapshots
189
+ python scripts/roundtrip_check.py <UnityProject> # verify byte-exact round-trip on a real project
190
+ ```
191
+
192
+ Tests run against synthetic graphs and small stubs of the NodeCanvas API surface
193
+ (`tests/fixtures/project/Assets/Stubs`); NodeCanvas itself is a commercial asset and is not included.
194
+
195
+ ## License
196
+
197
+ MIT. NodeCanvas is a product of Paradox Notion; this project is not affiliated with it.
@@ -0,0 +1,53 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.18"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "nodecanvas-cli"
7
+ version = "0.1.0"
8
+ description = "Read, query, lint and edit NodeCanvas graphs (Behaviour Trees, FSMs, Dialogue Trees) in Unity projects from the command line"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.9"
12
+ authors = [{ name = "Aleksandr Esipov" }]
13
+ keywords = ["unity", "nodecanvas", "behaviour-tree", "fsm", "dialogue-tree", "cli", "ai-agents"]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3 :: Only",
17
+ "Programming Language :: Python :: 3.9",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Operating System :: OS Independent",
23
+ "Intended Audience :: Developers",
24
+ "Environment :: Console",
25
+ "Topic :: Games/Entertainment",
26
+ "Topic :: Software Development :: Libraries",
27
+ ]
28
+ dependencies = []
29
+
30
+ [project.urls]
31
+ Homepage = "https://github.com/Primeer/nodecanvas-cli"
32
+ Issues = "https://github.com/Primeer/nodecanvas-cli/issues"
33
+ Changelog = "https://github.com/Primeer/nodecanvas-cli/blob/main/CHANGELOG.md"
34
+
35
+ [project.scripts]
36
+ nodecanvas-cli = "nodecanvas_cli.cli:main"
37
+
38
+ [project.optional-dependencies]
39
+ mcp = ["mcp>=2.3; python_version >= '3.10'"]
40
+ dev = ["pytest>=7", "mcp>=2.3; python_version >= '3.10'"]
41
+
42
+ [tool.hatch.build.targets.wheel]
43
+ packages = ["src/nodecanvas_cli"]
44
+
45
+ [tool.pytest.ini_options]
46
+ testpaths = ["tests"]
47
+
48
+ [tool.ruff]
49
+ line-length = 120
50
+ target-version = "py39"
51
+
52
+ [tool.ruff.lint]
53
+ select = ["E", "F", "W", "I"]
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env python3
2
+ """Load and re-store every graph of a Unity project; report files that change."""
3
+
4
+ import pathlib
5
+ import sys
6
+
7
+ sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "src"))
8
+ from nodecanvas_cli.container import UnityFile
9
+ from nodecanvas_cli.project import Project, find_project_root
10
+
11
+ proj = Project(find_project_root(pathlib.Path(sys.argv[1]) if len(sys.argv) > 1 else None))
12
+ ok = total = 0
13
+ for rel in proj.containers:
14
+ f = UnityFile(proj.abs(rel), rel)
15
+ original = list(f.lines)
16
+ slots = f.slots()
17
+ for slot in slots:
18
+ total += 1
19
+ g = f.load(slot)
20
+ f.store(g)
21
+ if f.lines == original:
22
+ ok += len(slots)
23
+ else:
24
+ for i, (a, b) in enumerate(zip(original, f.lines)):
25
+ if a != b:
26
+ print(rel, "line", i + 1, "\n O:", a[:150], "\n N:", b[:150])
27
+ break
28
+ else:
29
+ print(rel, "line count", len(original), len(f.lines))
30
+ print(f"{ok}/{total} graphs round-trip identically")