pi-revit 0.2.18 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +42 -3
- package/README.md +130 -35
- package/bin/pi-revit.js +10 -10
- package/extensions/pi-revit/index.ts +89 -11
- package/package.json +4 -2
- package/scripts/build.ps1 +9 -3
- package/scripts/check-sdk.ps1 +66 -0
- package/scripts/deploy.ps1 +16 -4
- package/skills/pi-revit/SKILL.md +17 -7
- package/src/Revit/BridgeServer.cs +7 -7
- package/src/Revit/ToolRegistry.cs +30 -5
- package/src/Revit/Tools/DocumentGuard.cs +64 -0
- package/src/Revit/Tools/ExecuteCsharp.cs +16 -16
- package/src/Revit/Tools/ExportDocuments.cs +104 -27
- package/src/Revit/Tools/FailureGuard.cs +21 -0
- package/src/Revit/Tools/GetElementDetails.cs +6 -4
- package/src/Revit/Tools/GetElements.cs +10 -20
- package/src/Revit/Tools/GetModelOverview.cs +1 -0
- package/src/Revit/Tools/ManageSelection.cs +26 -17
- package/src/Revit/Tools/SetParameters.cs +10 -8
- package/src/Revit/Tools/ToolSupport.cs +0 -39
- package/workspace/AGENTS.md +40 -9
package/CHANGELOG.md
CHANGED
|
@@ -5,9 +5,48 @@ Format follows [Keep a Changelog](https://keepachangelog.com/); version headers
|
|
|
5
5
|
`## [x.y.z] - YYYY-MM-DD` so tooling (and Pi's changelog parser format) can read them.
|
|
6
6
|
|
|
7
7
|
Every published version gets an entry with **Added** / **Changed** / **Fixed** sections
|
|
8
|
-
describing what the user will notice — not internal refactors.
|
|
9
|
-
|
|
10
|
-
## [0.
|
|
8
|
+
describing what the user will notice — not internal refactors.
|
|
9
|
+
|
|
10
|
+
## [0.3.1] - 2026-09-16
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- The installer checks the selected .NET SDK before installing packages or building the add-in. Missing or older SDKs now produce a clear explanation, the matching Windows x64 SDK download link, and retry instructions. Interactive installs offer to open the download page. Manual builds and deployments also check the SDK before compiling.
|
|
15
|
+
|
|
16
|
+
## [0.3.0] - 2026-09-09
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- `read_revit_result` retrieves complete large tool results in bounded fragments.
|
|
21
|
+
Result IDs belong to the current Pi extension session; saved files remain
|
|
22
|
+
readable by path while available.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- **Breaking:** writes and UI mutations require `expected_document_id` from
|
|
27
|
+
`get_model_overview`'s `project.documentId`. Refresh it after close/reopen or
|
|
28
|
+
bridge restart. A legacy title alone is insufficient; supplied IDs on reads
|
|
29
|
+
are also checked.
|
|
30
|
+
- Default export folders include a model-identity hash, separating same-title
|
|
31
|
+
models. Existing folders remain untouched; explicit output directories work
|
|
32
|
+
as before.
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- Requested values and all returned rows reach Pi instead of only UI/debug
|
|
37
|
+
details. Large-result retrieval preserves each tool's pagination and limits.
|
|
38
|
+
- Scoped display-name filters resolve each element's parameter, including
|
|
39
|
+
matches beyond the first 50 elements; explicit built-in/GUID filters stay optimized.
|
|
40
|
+
- Type-only parameter requests work independently of instance-parameter inclusion.
|
|
41
|
+
- Transaction results distinguish confirmed commit/rollback from incomplete
|
|
42
|
+
cleanup. Failure handling follows the transaction lifecycle; UI and export
|
|
43
|
+
errors disclose effects or files already produced.
|
|
44
|
+
|
|
45
|
+
Update both the Pi package and Revit add-in with Revit closed, then restart Revit
|
|
46
|
+
and start a fresh Pi session. Live verification covered bounded workflows on
|
|
47
|
+
Revit 2025.4.3; Revit 2026/2027 were not tested for this release.
|
|
48
|
+
|
|
49
|
+
## [0.2.18] - 2026-08-21
|
|
11
50
|
|
|
12
51
|
### Fixed
|
|
13
52
|
- When pi starts before Revit and the background rediscovery timer (rather than a `ping`
|
package/README.md
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
# pi-revit
|
|
2
2
|
|
|
3
3
|
Native Revit tools for [Pi](https://pi.dev) — ask about, query, script, and modify the open
|
|
4
|
-
Autodesk Revit model from your terminal.
|
|
5
|
-
|
|
4
|
+
Autodesk Revit model from your terminal.
|
|
5
|
+
|
|
6
6
|
```text
|
|
7
7
|
You: how many levels in the model?
|
|
8
8
|
Pi: calls get_model_overview → "There are 14 levels in the Revit model."
|
|
9
9
|
|
|
10
10
|
You: select all structural columns
|
|
11
|
-
Pi: get_elements → manage_selection → "Selected 222 structural columns."
|
|
11
|
+
Pi: get_model_overview → get_elements → manage_selection → "Selected 222 structural columns."
|
|
12
12
|
|
|
13
13
|
You: rename level 'L1' to 'Ground Floor'
|
|
14
|
-
Pi:
|
|
14
|
+
Pi: get_model_overview → set_parameters → "Done — Level 'L1' is now 'Ground Floor'."
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
## How it works
|
|
@@ -26,7 +26,7 @@ localhost HTTP bridge ← per-start token; connection info in %APPDATA%
|
|
|
26
26
|
headless Revit add-in ← no ribbon, no panels; just a bridge
|
|
27
27
|
│ ExternalEvent queue (Revit API thread)
|
|
28
28
|
▼
|
|
29
|
-
Revit API ←
|
|
29
|
+
Revit API ← tool-owned model transactions; separate UI/file effects
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
The extension discovers its tools from the bridge at startup (retrying in the background until
|
|
@@ -39,19 +39,51 @@ selected LLM provider, like any Pi session.
|
|
|
39
39
|
Be deliberate about pointing an LLM at a real project model. The add-in enforces what it can
|
|
40
40
|
enforce mechanically, and is honest about what it cannot:
|
|
41
41
|
|
|
42
|
-
-
|
|
43
|
-
can
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
42
|
+
- Tool metadata describes its classification; UI actions such as selection and view
|
|
43
|
+
activation can change state even when `write` is false. Confirmation policy belongs
|
|
44
|
+
to the client. Exact document targeting is enforced separately as described below.
|
|
45
|
+
- Parameter writes, C# scripts, temporary isolation, and IFC export own named Revit
|
|
46
|
+
transactions. Failure handling is attached after transaction start, and results
|
|
47
|
+
check transaction outcomes before claiming commit or rollback. `set_parameters`
|
|
48
|
+
can commit a partially successful batch; inspect every failed update and
|
|
49
|
+
`commitWarnings`. An unconfirmed rollback is reported as such.
|
|
49
50
|
- `execute_csharp` is an unrestricted escape hatch by design — scripts have full CLR access.
|
|
50
51
|
Treat it like giving the agent a macro editor, on a model you have saved or can restore.
|
|
51
|
-
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
- `execute_csharp` has a dialog guard that attempts dismissive responses to dialogs
|
|
53
|
+
raised while the script runs. It does not establish that every Revit dialog or
|
|
54
|
+
failure mode can be handled automatically.
|
|
55
|
+
- A model transaction does not undo filesystem output or earlier selection/zoom
|
|
56
|
+
changes. A failed export can leave incomplete files; its error reports the output
|
|
57
|
+
location and observed changed files. An isolation failure reports any earlier
|
|
58
|
+
selection action that already completed.
|
|
59
|
+
|
|
60
|
+
### Target the exact open document
|
|
61
|
+
|
|
62
|
+
Call `get_model_overview` for the intended model and copy `project.documentId`
|
|
63
|
+
unchanged into `expected_document_id` on subsequent operations:
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"expected_document_id": "<project.documentId from the current overview>",
|
|
68
|
+
"updates": [{ "element_id": 12345, "parameter": "ALL_MODEL_INSTANCE_COMMENTS", "value": "Reviewed" }]
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Replace the placeholders with the current document ID and an actual element ID.
|
|
73
|
+
The exact ID is required for `set_parameters`, `execute_csharp`,
|
|
74
|
+
`export_documents`, and `open_view`. It is also required for selection/zoom
|
|
75
|
+
changes and any `manage_selection` call with `isolate_in_view: true`, including
|
|
76
|
+
action `get`. Pure reads may omit it; a supplied ID is always checked.
|
|
77
|
+
|
|
78
|
+
The identity represents one currently open native document in one loaded bridge
|
|
79
|
+
session. It is not a persistent project ID, path, export-folder key, or credential.
|
|
80
|
+
Closing/reopening the document or restarting the bridge invalidates prior IDs.
|
|
81
|
+
Read the intended document's overview again after those transitions. The guard
|
|
82
|
+
checks the actual target on Revit's API thread immediately before execution.
|
|
83
|
+
|
|
84
|
+
Legacy `expected_document` titles remain an optional additional sanity check.
|
|
85
|
+
A title alone no longer satisfies the required guard, even when it matches.
|
|
86
|
+
Clients must refresh discovery and supply the new field after upgrading to 0.3.0.
|
|
55
87
|
|
|
56
88
|
Practical advice: work on saved models, keep worksharing backups/central protection as usual,
|
|
57
89
|
and review the agent's summary of what changed after any write session.
|
|
@@ -75,9 +107,18 @@ npx.cmd -y pi-revit
|
|
|
75
107
|
```
|
|
76
108
|
|
|
77
109
|
This installs the Pi package, builds and deploys the Revit bridge add-in, creates the
|
|
78
|
-
`Documents\pi-revit` workspace, and installs the global `pi-revit` command.
|
|
79
|
-
|
|
80
|
-
|
|
110
|
+
`Documents\pi-revit` workspace, and installs the global `pi-revit` command.
|
|
111
|
+
|
|
112
|
+
The installer first checks the selected .NET SDK. If it is missing or too old,
|
|
113
|
+
installation stops with the required version, a download link, and retry steps.
|
|
114
|
+
Interactive terminals also offer to open the download page. Revit uses a runtime
|
|
115
|
+
to run; compiling this add-in also needs the SDK. For Revit 2027, install the
|
|
116
|
+
[.NET 10 SDK for Windows x64](https://dotnet.microsoft.com/en-us/download/dotnet/10.0),
|
|
117
|
+
reopen PowerShell, and rerun the installer. Existing .NET versions can stay installed.
|
|
118
|
+
If an older SDK is still selected, check `dotnet --list-sdks`, your `PATH`, and any
|
|
119
|
+
`global.json` in the current directory or its parents.
|
|
120
|
+
|
|
121
|
+
Start Revit (click **Always Load** on the unsigned add-in prompt once) and open any
|
|
81
122
|
project. No panel or ribbon appears — the add-in is headless.
|
|
82
123
|
|
|
83
124
|
### Manual npm install
|
|
@@ -114,10 +155,19 @@ git clone https://github.com/Triavision-ai/pi-revit.git
|
|
|
114
155
|
cd pi-revit
|
|
115
156
|
powershell -ExecutionPolicy Bypass -File scripts\deploy.ps1
|
|
116
157
|
pi install ./
|
|
117
|
-
powershell -ExecutionPolicy Bypass -File scripts\setup-workspace.ps1
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
|
|
158
|
+
powershell -ExecutionPolicy Bypass -File scripts\setup-workspace.ps1
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Upgrading to 0.3.0
|
|
162
|
+
|
|
163
|
+
**Breaking change:** writes and UI mutations now require `expected_document_id`.
|
|
164
|
+
Close Revit, update the Pi package and redeploy the add-in using the installation
|
|
165
|
+
steps above, then restart Revit and start a fresh Pi session. Both components must
|
|
166
|
+
be updated. Call `get_model_overview` and copy `project.documentId` into subsequent
|
|
167
|
+
mutating calls; a legacy `expected_document` title alone is insufficient. Refresh
|
|
168
|
+
the ID after closing/reopening a document or restarting Revit.
|
|
169
|
+
|
|
170
|
+
## Use it
|
|
121
171
|
|
|
122
172
|
Open **any terminal** — PowerShell, CMD, or Windows Terminal — and type:
|
|
123
173
|
|
|
@@ -141,12 +191,18 @@ automatically and all Revit session history lives in one predictable place (`pi-
|
|
|
141
191
|
continues the last session). The Revit tools themselves are installed globally in Pi, and the
|
|
142
192
|
extension discovers them live from the bridge inside Revit each time a session starts.
|
|
143
193
|
|
|
144
|
-
**Per model, automatically:** files sort themselves. Exports land in
|
|
145
|
-
`Documents\pi-revit\Models\<model title>\exports
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
194
|
+
**Per model, automatically:** files sort themselves. Exports land in
|
|
195
|
+
`Documents\pi-revit\Models\<model title>--<identity hash>\exports`. The suffix derives
|
|
196
|
+
from the normalized saved-file path, cloud region/project/model identity, or Revit
|
|
197
|
+
Server path. Distinct saved paths therefore use different destinations even when
|
|
198
|
+
their titles or inherited project IDs match. Save As to another path selects a new
|
|
199
|
+
destination. Unsaved models or unavailable persistent identities use a token stable
|
|
200
|
+
only for that open document; their destination may change after reopening.
|
|
201
|
+
|
|
202
|
+
`model.txt` records the identity used. Existing title-only directories remain
|
|
203
|
+
untouched; upgrading does not migrate or merge old exports. An explicit
|
|
204
|
+
`output_dir` still overrides the default. File attribution uses a directory
|
|
205
|
+
snapshot, so avoid unrelated concurrent writers in a shared output directory.
|
|
150
206
|
|
|
151
207
|
Plain `pi` from any folder also works; `pi-revit` just adds the right working folder on top.
|
|
152
208
|
|
|
@@ -165,19 +221,58 @@ Plain `pi` from any folder also works; `pi-revit` just adds the right working fo
|
|
|
165
221
|
| `search_api_docs` | Search the offline Revit API docs (works with no document open) |
|
|
166
222
|
| `execute_csharp` | Run a C# script in one auto-managed transaction — the escape hatch |
|
|
167
223
|
| `capture_view` | PNG snapshot of a view to a temp file (read the returned path to see it) |
|
|
168
|
-
| `export_documents` | PDF/DWG/PNG/IFC export of sheets and views —
|
|
169
|
-
| `get_model_health` | Warnings grouped + worksets, phases, design options audit |
|
|
224
|
+
| `export_documents` | PDF/DWG/PNG/IFC export of sheets and views — sorted into `Models\<title>--<identity hash>\exports` |
|
|
225
|
+
| `get_model_health` | Warnings grouped + worksets, phases, design options audit |
|
|
226
|
+
| `read_revit_result` | Read bounded fragments of a saved large tool result; extension-only, no Revit call |
|
|
227
|
+
|
|
228
|
+
### Read complete results
|
|
229
|
+
|
|
230
|
+
Requested rows and parameter values are included in model-visible tool content.
|
|
231
|
+
Results up to 12,000 characters are complete inline. For a larger result, the Pi
|
|
232
|
+
extension saves the complete tool payload as UTF-8 JSON and returns `result_id`,
|
|
233
|
+
`file_path`, `total_chars`, `complete_inline: false`, and retrieval instructions.
|
|
234
|
+
|
|
235
|
+
Call `read_revit_result` with the returned ID and `offset: 0`, then follow each
|
|
236
|
+
`next_offset` until `has_more` is false. A requested fragment is at most 8,000
|
|
237
|
+
UTF-16 code units; it may be smaller so the escaped response stays within the
|
|
238
|
+
message limit. Concatenate each page's `text` in order. Individual fragments are
|
|
239
|
+
not standalone JSON objects from the original result. Use the returned offsets,
|
|
240
|
+
not byte counts or a guessed increment.
|
|
241
|
+
|
|
242
|
+
Result IDs are registered in memory by the current extension instance. After an
|
|
243
|
+
extension reload or new Pi process, an old ID may no longer resolve; use the
|
|
244
|
+
original absolute `file_path` with Pi's normal `read` tool while that file remains
|
|
245
|
+
available. Saved results live in a unique OS temporary directory, can contain
|
|
246
|
+
model data, and are subject to eventual OS/user cleanup. If saving fails after
|
|
247
|
+
Revit completed an operation, inspect actual model state before retrying a write.
|
|
248
|
+
|
|
249
|
+
Complete payload retrieval does not expand a tool's own query page or declared
|
|
250
|
+
limits. Continue `get_elements`/`get_element_types` pagination separately, and
|
|
251
|
+
check warning-group or projection truncation indicators. A bridge-only client
|
|
252
|
+
must consume `details.payload` for oversized results; `read_revit_result` belongs
|
|
253
|
+
to the Pi extension.
|
|
254
|
+
|
|
255
|
+
Display-name parameter filters now resolve on every element, including inside a
|
|
256
|
+
category/class scope. Explicit built-in IDs and shared GUIDs can retain collector
|
|
257
|
+
optimization. In `get_element_details`, `include.parameters` and
|
|
258
|
+
`include.type_parameters` are independent; disabling instance parameters still
|
|
259
|
+
allows a type-only result.
|
|
170
260
|
|
|
171
261
|
## Limitations — read before using on real projects
|
|
172
262
|
|
|
173
263
|
- **Write tools are unrestricted by design.** `set_parameters` and `execute_csharp` modify the
|
|
174
264
|
open model directly — there is no confirmation prompt and no sandbox. Writes run in named
|
|
175
|
-
transactions
|
|
176
|
-
|
|
177
|
-
|
|
265
|
+
transactions (`execute_csharp` attempts rollback after script failure;
|
|
266
|
+
`set_parameters` commits partial successes and reports each failure). Read the
|
|
267
|
+
actual transaction outcome and failed lists. Script result-projection failure
|
|
268
|
+
can leave a successful edit committed with a `returnValueError`; filesystem and
|
|
269
|
+
UI effects are separate from model rollback.
|
|
178
270
|
- The add-in multi-targets .NET 8 (Revit 2025/2026) and .NET 10 (Revit 2027); `deploy.ps1`
|
|
179
271
|
auto-detects the Revit versions you have installed and builds only the matching framework(s),
|
|
180
|
-
so you only need the SDK for the Revit you run.
|
|
272
|
+
so you only need the SDK for the Revit you run. The 0.3.0 changes were tested live
|
|
273
|
+
on Revit 2025.4.3 (build 25.4.30.30, German UI). Revit 2026/2027 and large-model
|
|
274
|
+
performance were not tested for this release. Export API/file checks do not
|
|
275
|
+
establish full DWG drawing or IFC schema/geometry validation.
|
|
181
276
|
- **One Revit instance at a time** is discoverable (last started wins). When that instance
|
|
182
277
|
closes or crashes, another one that is still running takes the slot over within 30s.
|
|
183
278
|
- A tool call that outlives its timeout is abandoned client-side but may still complete inside
|
package/bin/pi-revit.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
const { spawnSync } = require("node:child_process");
|
|
3
3
|
const fs = require("node:fs");
|
|
4
4
|
const path = require("node:path");
|
|
5
5
|
|
|
6
|
-
const root = path.resolve(__dirname, "..");
|
|
7
|
-
const scriptsDir = path.join(root, "scripts");
|
|
6
|
+
const root = path.resolve(__dirname, "..");
|
|
7
|
+
const scriptsDir = path.join(root, "scripts");
|
|
8
|
+
const packageVersion = require(path.join(root, "package.json")).version;
|
|
9
|
+
const packageSpec = `npm:pi-revit@${packageVersion}`;
|
|
8
10
|
|
|
9
11
|
function usage() {
|
|
10
|
-
console.log(`pi-revit installer\n\nUsage:\n npx.cmd -y pi-revit\n\nWhat it does on Windows:\n 1. Runs: pi install
|
|
12
|
+
console.log(`pi-revit installer\n\nUsage:\n npx.cmd -y pi-revit\n\nWhat it does on Windows:\n 1. Runs: pi install ${packageSpec}\n 2. Builds and deploys the matching Revit bridge add-in\n 3. Creates the Documents\\pi-revit workspace and global pi-revit command\n\nClose Revit before running. Revit 2025, 2026, or 2027 and the matching .NET SDK are required.`);
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
function fail(message) {
|
|
@@ -35,10 +37,10 @@ function runCmd(title, commandLine) {
|
|
|
35
37
|
if (result.status !== 0) process.exit(result.status ?? 1);
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
function runPowerShellScript(scriptName) {
|
|
40
|
+
function runPowerShellScript(scriptName, args = []) {
|
|
39
41
|
const scriptPath = path.join(scriptsDir, scriptName);
|
|
40
42
|
if (!fs.existsSync(scriptPath)) fail(`missing script: ${scriptPath}`);
|
|
41
|
-
run(scriptName, "powershell.exe", ["-ExecutionPolicy", "Bypass", "-File", scriptPath]);
|
|
43
|
+
run(scriptName, "powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", scriptPath, ...args]);
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
function revitIsRunning() {
|
|
@@ -72,9 +74,7 @@ if (!commandExists("pi")) {
|
|
|
72
74
|
fail("the 'pi' command was not found on PATH. Install Pi first: npm install -g --ignore-scripts @earendil-works/pi-coding-agent");
|
|
73
75
|
}
|
|
74
76
|
|
|
75
|
-
|
|
76
|
-
fail("the 'dotnet' command was not found on PATH. Install the .NET SDK required by your Revit version.");
|
|
77
|
-
}
|
|
77
|
+
runPowerShellScript("deploy.ps1", ["-CheckOnly", "-OfferDownload"]);
|
|
78
78
|
|
|
79
79
|
if (revitIsRunning()) {
|
|
80
80
|
waitForEnter();
|
|
@@ -84,7 +84,7 @@ if (revitIsRunning()) {
|
|
|
84
84
|
console.log("pi-revit full installer");
|
|
85
85
|
console.log("This installs the Pi package, deploys the Revit add-in, and creates the workspace/global command.");
|
|
86
86
|
|
|
87
|
-
runCmd("Install the Pi package from npm",
|
|
87
|
+
runCmd("Install the matching Pi package from npm", `pi install ${packageSpec}`);
|
|
88
88
|
runPowerShellScript("deploy.ps1");
|
|
89
89
|
runPowerShellScript("setup-workspace.ps1");
|
|
90
90
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Type, type TSchema } from "typebox";
|
|
3
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
4
5
|
import os from "node:os";
|
|
5
6
|
import path from "node:path";
|
|
6
7
|
import { fileURLToPath } from "node:url";
|
|
@@ -48,6 +49,12 @@ const DEFAULT_TIMEOUT_MS = 30_000;
|
|
|
48
49
|
const LONG_TIMEOUT_MS = 120_000;
|
|
49
50
|
const DISCOVERY_TIMEOUT_MS = 10_000;
|
|
50
51
|
const MAX_MODEL_CONTENT_CHARS = 12_000;
|
|
52
|
+
const MAX_RESULT_PAGE_CHARS = 8_000;
|
|
53
|
+
|
|
54
|
+
// IDs only resolve results created by this extension instance. A caller cannot
|
|
55
|
+
// turn read_revit_result into an arbitrary filesystem read by supplying a path.
|
|
56
|
+
const savedResults = new Map<string, string>();
|
|
57
|
+
let resultDirectory: Promise<string> | undefined;
|
|
51
58
|
|
|
52
59
|
/** Tools with a longer budget; everything else gets DEFAULT_TIMEOUT_MS. The same
|
|
53
60
|
* value is sent to the bridge as timeout_ms and used client-side via AbortSignal. */
|
|
@@ -155,10 +162,87 @@ export async function bridgeRequest(
|
|
|
155
162
|
|
|
156
163
|
export function capText(text: string): string {
|
|
157
164
|
if (text.length <= MAX_MODEL_CONTENT_CHARS) return text;
|
|
158
|
-
const suffix = `... [truncated at ${MAX_MODEL_CONTENT_CHARS} chars
|
|
165
|
+
const suffix = `... [truncated preview at ${MAX_MODEL_CONTENT_CHARS} chars]`;
|
|
159
166
|
return text.slice(0, Math.max(0, MAX_MODEL_CONTENT_CHARS - suffix.length)) + suffix;
|
|
160
167
|
}
|
|
161
168
|
|
|
169
|
+
async function modelContent(name: string, payload: BridgeToolResponse): Promise<{ type: "text"; text: string }[]> {
|
|
170
|
+
const details = payload.details;
|
|
171
|
+
const value = details !== null && typeof details === "object" && Object.hasOwn(details, "payload")
|
|
172
|
+
? (details as { payload: unknown }).payload
|
|
173
|
+
: details;
|
|
174
|
+
// Current and older bridges both carry the full value in details.payload.
|
|
175
|
+
// Pi sends content to the model; details alone is only available to its UI.
|
|
176
|
+
const text = details !== undefined
|
|
177
|
+
? JSON.stringify(value, null, 2) ?? "null"
|
|
178
|
+
: payload.content?.map((block) => block.text).join("\n") ?? "{}";
|
|
179
|
+
if (text.length <= MAX_MODEL_CONTENT_CHARS) return [{ type: "text", text }];
|
|
180
|
+
|
|
181
|
+
const resultId = randomUUID();
|
|
182
|
+
let filePath: string;
|
|
183
|
+
try {
|
|
184
|
+
const directory = await (resultDirectory ??= mkdtemp(path.join(os.tmpdir(), "pi-revit-results-")).catch((error) => {
|
|
185
|
+
// A transient failure must not poison every later large result in this session.
|
|
186
|
+
resultDirectory = undefined;
|
|
187
|
+
throw error;
|
|
188
|
+
}));
|
|
189
|
+
filePath = path.join(directory, `${resultId}.json`);
|
|
190
|
+
await writeFile(filePath, text, { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
191
|
+
} catch (error) {
|
|
192
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
193
|
+
throw new Error(`Revit completed '${name}', but its large result could not be saved locally: ${reason}. Verify model state before retrying a write.`);
|
|
194
|
+
}
|
|
195
|
+
savedResults.set(resultId, filePath);
|
|
196
|
+
return [{ type: "text", text: JSON.stringify({
|
|
197
|
+
result_id: resultId,
|
|
198
|
+
file_path: filePath,
|
|
199
|
+
total_chars: text.length,
|
|
200
|
+
complete_inline: false,
|
|
201
|
+
retrieval: { tool: "read_revit_result", result_id: resultId, offset: 0, limit: MAX_RESULT_PAGE_CHARS },
|
|
202
|
+
instructions: "The complete result is saved locally. Call read_revit_result, then follow next_offset until has_more is false. Each page is a fragment of the saved text, not a standalone result. Offsets count UTF-16 code units. The absolute file can also be opened with read; it remains available after an extension reload, when this session's result ID may no longer resolve.",
|
|
203
|
+
}) }];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function registerResultReader(pi: ExtensionAPI) {
|
|
207
|
+
pi.registerTool({
|
|
208
|
+
name: "read_revit_result",
|
|
209
|
+
label: "Read Saved Revit Result",
|
|
210
|
+
description: "Read a bounded fragment of a large Revit tool result using its opaque result_id. This reads a saved local result and does not contact Revit. Follow next_offset until has_more is false; text fragments concatenate to the complete saved result. Offsets count UTF-16 code units.",
|
|
211
|
+
parameters: Type.Object({
|
|
212
|
+
result_id: Type.String({ description: "Opaque result_id returned by a Revit tool in this extension session." }),
|
|
213
|
+
offset: Type.Optional(Type.Integer({ minimum: 0, description: "Character offset from the previous page's next_offset; default 0." })),
|
|
214
|
+
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: MAX_RESULT_PAGE_CHARS, description: "Maximum characters to return; default 8000. Escaping may require a smaller fragment." })),
|
|
215
|
+
}),
|
|
216
|
+
executionMode: "sequential",
|
|
217
|
+
async execute(_toolCallId, params) {
|
|
218
|
+
const offset = params.offset ?? 0;
|
|
219
|
+
const limit = params.limit ?? MAX_RESULT_PAGE_CHARS;
|
|
220
|
+
if (!Number.isSafeInteger(offset) || offset < 0) throw new Error("offset must be a non-negative integer.");
|
|
221
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_RESULT_PAGE_CHARS)
|
|
222
|
+
throw new Error(`limit must be an integer from 1 to ${MAX_RESULT_PAGE_CHARS}.`);
|
|
223
|
+
const filePath = savedResults.get(params.result_id);
|
|
224
|
+
if (!filePath) throw new Error("Unknown result_id for this extension session. Use the original result's file_path with read if the extension was reloaded.");
|
|
225
|
+
const text = await readFile(filePath, "utf8");
|
|
226
|
+
if (offset > text.length) throw new Error(`offset exceeds this result's ${text.length} characters.`);
|
|
227
|
+
const encode = (count: number) => JSON.stringify({
|
|
228
|
+
result_id: params.result_id, offset, returned_chars: count, total_chars: text.length,
|
|
229
|
+
has_more: offset + count < text.length,
|
|
230
|
+
next_offset: offset + count < text.length ? offset + count : null,
|
|
231
|
+
fragment: true, text: text.slice(offset, offset + count),
|
|
232
|
+
});
|
|
233
|
+
// Bound the actual model message, including JSON escaping and metadata.
|
|
234
|
+
let low = 0;
|
|
235
|
+
let high = Math.min(limit, text.length - offset);
|
|
236
|
+
while (low < high) {
|
|
237
|
+
const count = Math.ceil((low + high) / 2);
|
|
238
|
+
if (encode(count).length <= MAX_MODEL_CONTENT_CHARS) low = count;
|
|
239
|
+
else high = count - 1;
|
|
240
|
+
}
|
|
241
|
+
return { content: [{ type: "text", text: encode(low) }], details: { filePath } };
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
162
246
|
async function runBridgeTool(name: string, args: unknown, signal: AbortSignal | undefined, timeoutMs: number) {
|
|
163
247
|
const payload = (await bridgeRequest(
|
|
164
248
|
`/tools/${encodeURIComponent(name)}/execute`,
|
|
@@ -171,14 +255,7 @@ async function runBridgeTool(name: string, args: unknown, signal: AbortSignal |
|
|
|
171
255
|
timeoutMs,
|
|
172
256
|
)) as BridgeToolResponse;
|
|
173
257
|
|
|
174
|
-
|
|
175
|
-
Array.isArray(payload.content) && payload.content.length > 0
|
|
176
|
-
? payload.content.map((block) =>
|
|
177
|
-
typeof block.text === "string" ? { ...block, text: capText(block.text) } : block,
|
|
178
|
-
)
|
|
179
|
-
: [{ type: "text", text: capText(JSON.stringify(payload.details ?? {})) }];
|
|
180
|
-
|
|
181
|
-
return { content, details: payload.details };
|
|
258
|
+
return { content: await modelContent(name, payload), details: payload.details };
|
|
182
259
|
}
|
|
183
260
|
|
|
184
261
|
function registerBridgeTool(pi: ExtensionAPI, descriptor: BridgeToolDescriptor) {
|
|
@@ -308,6 +385,7 @@ function registerPing(pi: ExtensionAPI, onBridgeAlive?: () => Promise<"ready" |
|
|
|
308
385
|
const REDISCOVERY_INTERVAL_MS = 15_000;
|
|
309
386
|
|
|
310
387
|
export default async function revitConnector(pi: ExtensionAPI) {
|
|
388
|
+
registerResultReader(pi);
|
|
311
389
|
// Self-healing discovery: when pi starts before Revit is ready, the initial
|
|
312
390
|
// GET /tools fails and only ping is registered. Rather than requiring a
|
|
313
391
|
// fresh pi start (/reload does not reliably re-run async registration), a
|
|
@@ -328,7 +406,7 @@ export default async function revitConnector(pi: ExtensionAPI) {
|
|
|
328
406
|
if (descriptors.length === 0) return false;
|
|
329
407
|
for (const descriptor of descriptors) {
|
|
330
408
|
if (!descriptor || typeof descriptor.name !== "string" || !descriptor.name) continue;
|
|
331
|
-
if (descriptor.name === "ping") continue;
|
|
409
|
+
if (descriptor.name === "ping" || descriptor.name === "read_revit_result") continue;
|
|
332
410
|
registerBridgeTool(pi, descriptor);
|
|
333
411
|
}
|
|
334
412
|
bridgeToolsRegistered = true;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-revit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Native Pi connector for Autodesk Revit. Run npx.cmd -y pi-revit for the full Windows install.",
|
|
5
5
|
"author": "Ahmad Altahlawi",
|
|
6
6
|
"license": "MIT",
|
|
@@ -27,7 +27,9 @@
|
|
|
27
27
|
"deploy": "powershell -ExecutionPolicy Bypass -File scripts/deploy.ps1",
|
|
28
28
|
"setup": "powershell -ExecutionPolicy Bypass -File scripts/setup-workspace.ps1",
|
|
29
29
|
"uninstall:revit": "powershell -ExecutionPolicy Bypass -File scripts/uninstall.ps1",
|
|
30
|
-
"test:search": "dotnet run --project tests/search-engine"
|
|
30
|
+
"test:search": "dotnet run --project tests/search-engine",
|
|
31
|
+
"test:installer": "node --test tests/installer/*.test.cjs",
|
|
32
|
+
"test:sdk": "powershell -NoProfile -ExecutionPolicy Bypass -File tests/installer/sdk.test.ps1"
|
|
31
33
|
},
|
|
32
34
|
"files": [
|
|
33
35
|
"bin/",
|
package/scripts/build.ps1
CHANGED
|
@@ -17,9 +17,15 @@ param(
|
|
|
17
17
|
)
|
|
18
18
|
|
|
19
19
|
$ErrorActionPreference = 'Stop'
|
|
20
|
-
$project = Join-Path $PSScriptRoot '..\src\Revit\RevitBridge.csproj'
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
$project = Join-Path $PSScriptRoot '..\src\Revit\RevitBridge.csproj'
|
|
21
|
+
|
|
22
|
+
. (Join-Path $PSScriptRoot 'check-sdk.ps1')
|
|
23
|
+
$frameworks = if ($TargetFramework) { $TargetFramework -split ';' } else {
|
|
24
|
+
([xml](Get-Content $project -Raw)).Project.PropertyGroup.TargetFrameworks | Where-Object { $_ } | ForEach-Object { $_ -split ';' }
|
|
25
|
+
}
|
|
26
|
+
if (-not (Test-PiRevitSdk -TargetFrameworks $frameworks)) { exit 1 }
|
|
27
|
+
|
|
28
|
+
$buildArgs = @('build', $project, '-c', $Configuration)
|
|
23
29
|
|
|
24
30
|
# Stamp the package version into the assembly so the bridge can report which release
|
|
25
31
|
# the deployed add-in came from; the pi extension compares it against its own package
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#Requires -Version 5.1
|
|
2
|
+
|
|
3
|
+
function Test-PiRevitSdk {
|
|
4
|
+
param(
|
|
5
|
+
[string[]]$TargetFrameworks,
|
|
6
|
+
[string]$Context = 'Revit bridge build',
|
|
7
|
+
[switch]$OfferDownload
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
# Check the SDK selected in the build's working directory, not just installed
|
|
11
|
+
# SDKs: global.json or PATH can still select an older SDK.
|
|
12
|
+
$requiredMajor = 0
|
|
13
|
+
foreach ($framework in $TargetFrameworks) {
|
|
14
|
+
if ($framework -notmatch '^net(\d+)\.') {
|
|
15
|
+
throw "Cannot determine the required .NET SDK for '$framework'."
|
|
16
|
+
}
|
|
17
|
+
$requiredMajor = [Math]::Max($requiredMajor, [int]$matches[1])
|
|
18
|
+
}
|
|
19
|
+
if ($requiredMajor -eq 0) { throw 'No target frameworks were supplied for the SDK check.' }
|
|
20
|
+
|
|
21
|
+
$selectedVersion = $null
|
|
22
|
+
if (Get-Command dotnet -ErrorAction SilentlyContinue) {
|
|
23
|
+
try {
|
|
24
|
+
$versionOutput = @(& dotnet --version 2>&1)
|
|
25
|
+
if ($LASTEXITCODE -eq 0) {
|
|
26
|
+
foreach ($line in $versionOutput) {
|
|
27
|
+
if ("$line".Trim() -match '^(\d+)\.\d+\.\d+(?:-[\w.-]+)?$') {
|
|
28
|
+
$selectedVersion = "$line".Trim()
|
|
29
|
+
if ([int]$matches[1] -ge $requiredMajor) { return $true }
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
# A runtime-only installation or an unresolved global.json can make
|
|
36
|
+
# --version fail. Present the same actionable prerequisite message.
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
$downloadUrl = "https://dotnet.microsoft.com/en-us/download/dotnet/$requiredMajor.0"
|
|
41
|
+
Write-Host "`npi-revit prerequisite check: $Context" -ForegroundColor Yellow
|
|
42
|
+
Write-Host "Building this add-in requires the .NET $requiredMajor SDK or a newer compatible SDK."
|
|
43
|
+
if ($selectedVersion) {
|
|
44
|
+
Write-Host "Your build tools currently use .NET SDK $selectedVersion."
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
Write-Host 'No usable .NET SDK could be selected in this terminal.'
|
|
48
|
+
}
|
|
49
|
+
Write-Host 'Revit can run normally with its runtime; compiling the pi-revit add-in also needs the SDK.'
|
|
50
|
+
Write-Host "Install the .NET $requiredMajor SDK for Windows x64 (choose SDK, not Runtime)."
|
|
51
|
+
Write-Host 'You can keep your existing .NET versions installed.'
|
|
52
|
+
Write-Host "Download: $downloadUrl"
|
|
53
|
+
Write-Host 'Then reopen PowerShell and rerun your install or build command.'
|
|
54
|
+
Write-Host 'If the SDK is already installed, check dotnet --list-sdks, PATH, and any global.json selecting an older SDK.'
|
|
55
|
+
|
|
56
|
+
if ($OfferDownload -and [Environment]::UserInteractive -and -not [Console]::IsInputRedirected) {
|
|
57
|
+
try {
|
|
58
|
+
$answer = Read-Host 'Open the SDK download page now? [y/N]'
|
|
59
|
+
if ($answer -match '^(?i:y|yes)$') { Start-Process $downloadUrl | Out-Null }
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
Write-Host "Open the download link above in your browser."
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return $false
|
|
66
|
+
}
|
package/scripts/deploy.ps1
CHANGED
|
@@ -16,13 +16,16 @@ Usage:
|
|
|
16
16
|
scripts\deploy.ps1 # auto-detect + deploy to all installed
|
|
17
17
|
scripts\deploy.ps1 -RevitVersion 2026
|
|
18
18
|
scripts\deploy.ps1 -RevitVersion 2027 -RevitApiPath "D:\Autodesk\Revit 2027"
|
|
19
|
-
scripts\deploy.ps1 -SkipBuild
|
|
19
|
+
scripts\deploy.ps1 -SkipBuild
|
|
20
|
+
scripts\deploy.ps1 -CheckOnly # check prerequisites without installing
|
|
20
21
|
#>
|
|
21
22
|
param(
|
|
22
23
|
[string]$RevitVersion = '',
|
|
23
24
|
[string]$Configuration = 'Release',
|
|
24
25
|
[string]$RevitApiPath = '',
|
|
25
|
-
[switch]$SkipBuild
|
|
26
|
+
[switch]$SkipBuild,
|
|
27
|
+
[switch]$CheckOnly,
|
|
28
|
+
[switch]$OfferDownload
|
|
26
29
|
)
|
|
27
30
|
|
|
28
31
|
$ErrorActionPreference = 'Stop'
|
|
@@ -68,11 +71,20 @@ else {
|
|
|
68
71
|
Write-Host ("Detected Revit: " + (($targets | ForEach-Object { $_.Version }) -join ', ')) -ForegroundColor Cyan
|
|
69
72
|
}
|
|
70
73
|
|
|
71
|
-
#
|
|
74
|
+
# Validate every target before starting any build or deployment.
|
|
75
|
+
if ($CheckOnly -or -not $SkipBuild) {
|
|
76
|
+
. (Join-Path $PSScriptRoot 'check-sdk.ps1')
|
|
77
|
+
$context = 'Detected Revit: ' + (($targets | ForEach-Object { $_.Version }) -join ', ')
|
|
78
|
+
if (-not (Test-PiRevitSdk -TargetFrameworks @($targets.Tfm) -Context $context -OfferDownload:$OfferDownload)) { exit 1 }
|
|
79
|
+
}
|
|
80
|
+
if ($CheckOnly) { exit 0 }
|
|
81
|
+
|
|
82
|
+
# Build once per distinct target framework, compiling against a matching RevitAPI.dll.
|
|
72
83
|
if (-not $SkipBuild) {
|
|
73
84
|
foreach ($group in ($targets | Group-Object Tfm)) {
|
|
74
85
|
$apiPath = ($group.Group | Select-Object -First 1).Path
|
|
75
|
-
& (Join-Path $PSScriptRoot 'build.ps1') -Configuration $Configuration -RevitApiPath $apiPath -TargetFramework $group.Name
|
|
86
|
+
& (Join-Path $PSScriptRoot 'build.ps1') -Configuration $Configuration -RevitApiPath $apiPath -TargetFramework $group.Name
|
|
87
|
+
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
76
88
|
}
|
|
77
89
|
}
|
|
78
90
|
|