pi-revit 0.2.18 → 0.3.0
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 +36 -3
- package/README.md +118 -32
- package/bin/pi-revit.js +7 -5
- package/extensions/pi-revit/index.ts +95 -17
- package/package.json +3 -2
- package/skills/pi-revit/SKILL.md +21 -11
- package/src/Revit/BridgeServer.cs +9 -9
- package/src/Revit/ToolRegistry.cs +32 -7
- package/src/Revit/Tools/DocumentGuard.cs +64 -0
- package/src/Revit/Tools/ExecuteCsharp.cs +23 -23
- package/src/Revit/Tools/ExportDocuments.cs +121 -44
- package/src/Revit/Tools/FailureGuard.cs +26 -5
- package/src/Revit/Tools/GetElementDetails.cs +10 -8
- package/src/Revit/Tools/GetElements.cs +17 -27
- package/src/Revit/Tools/GetModelOverview.cs +2 -1
- package/src/Revit/Tools/ManageSelection.cs +35 -26
- package/src/Revit/Tools/SetParameters.cs +14 -12
- package/src/Revit/Tools/ToolSupport.cs +1 -40
- package/workspace/AGENTS.md +45 -14
package/CHANGELOG.md
CHANGED
|
@@ -5,9 +5,42 @@ 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.0] - 2026-09-09
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `read_revit_result` retrieves complete large tool results in bounded fragments.
|
|
15
|
+
Result IDs belong to the current Pi extension session; saved files remain
|
|
16
|
+
readable by path while available.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- **Breaking:** writes and UI mutations require `expected_document_id` from
|
|
21
|
+
`get_model_overview`'s `project.documentId`. Refresh it after close/reopen or
|
|
22
|
+
bridge restart. A legacy title alone is insufficient; supplied IDs on reads
|
|
23
|
+
are also checked.
|
|
24
|
+
- Default export folders include a model-identity hash, separating same-title
|
|
25
|
+
models. Existing folders remain untouched; explicit output directories work
|
|
26
|
+
as before.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- Requested values and all returned rows reach Pi instead of only UI/debug
|
|
31
|
+
details. Large-result retrieval preserves each tool's pagination and limits.
|
|
32
|
+
- Scoped display-name filters resolve each element's parameter, including
|
|
33
|
+
matches beyond the first 50 elements; explicit built-in/GUID filters stay optimized.
|
|
34
|
+
- Type-only parameter requests work independently of instance-parameter inclusion.
|
|
35
|
+
- Transaction results distinguish confirmed commit/rollback from incomplete
|
|
36
|
+
cleanup. Failure handling follows the transaction lifecycle; UI and export
|
|
37
|
+
errors disclose effects or files already produced.
|
|
38
|
+
|
|
39
|
+
Update both the Pi package and Revit add-in with Revit closed, then restart Revit
|
|
40
|
+
and start a fresh Pi session. Live verification covered bounded workflows on
|
|
41
|
+
Revit 2025.4.3; Revit 2026/2027 were not tested for this release.
|
|
42
|
+
|
|
43
|
+
## [0.2.18] - 2026-08-21
|
|
11
44
|
|
|
12
45
|
### Fixed
|
|
13
46
|
- 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.
|
|
@@ -114,10 +146,19 @@ git clone https://github.com/Triavision-ai/pi-revit.git
|
|
|
114
146
|
cd pi-revit
|
|
115
147
|
powershell -ExecutionPolicy Bypass -File scripts\deploy.ps1
|
|
116
148
|
pi install ./
|
|
117
|
-
powershell -ExecutionPolicy Bypass -File scripts\setup-workspace.ps1
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
|
|
149
|
+
powershell -ExecutionPolicy Bypass -File scripts\setup-workspace.ps1
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Upgrading to 0.3.0
|
|
153
|
+
|
|
154
|
+
**Breaking change:** writes and UI mutations now require `expected_document_id`.
|
|
155
|
+
Close Revit, update the Pi package and redeploy the add-in using the installation
|
|
156
|
+
steps above, then restart Revit and start a fresh Pi session. Both components must
|
|
157
|
+
be updated. Call `get_model_overview` and copy `project.documentId` into subsequent
|
|
158
|
+
mutating calls; a legacy `expected_document` title alone is insufficient. Refresh
|
|
159
|
+
the ID after closing/reopening a document or restarting Revit.
|
|
160
|
+
|
|
161
|
+
## Use it
|
|
121
162
|
|
|
122
163
|
Open **any terminal** — PowerShell, CMD, or Windows Terminal — and type:
|
|
123
164
|
|
|
@@ -141,12 +182,18 @@ automatically and all Revit session history lives in one predictable place (`pi-
|
|
|
141
182
|
continues the last session). The Revit tools themselves are installed globally in Pi, and the
|
|
142
183
|
extension discovers them live from the bridge inside Revit each time a session starts.
|
|
143
184
|
|
|
144
|
-
**Per model, automatically:** files sort themselves. Exports land in
|
|
145
|
-
`Documents\pi-revit\Models\<model title>\exports
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
185
|
+
**Per model, automatically:** files sort themselves. Exports land in
|
|
186
|
+
`Documents\pi-revit\Models\<model title>--<identity hash>\exports`. The suffix derives
|
|
187
|
+
from the normalized saved-file path, cloud region/project/model identity, or Revit
|
|
188
|
+
Server path. Distinct saved paths therefore use different destinations even when
|
|
189
|
+
their titles or inherited project IDs match. Save As to another path selects a new
|
|
190
|
+
destination. Unsaved models or unavailable persistent identities use a token stable
|
|
191
|
+
only for that open document; their destination may change after reopening.
|
|
192
|
+
|
|
193
|
+
`model.txt` records the identity used. Existing title-only directories remain
|
|
194
|
+
untouched; upgrading does not migrate or merge old exports. An explicit
|
|
195
|
+
`output_dir` still overrides the default. File attribution uses a directory
|
|
196
|
+
snapshot, so avoid unrelated concurrent writers in a shared output directory.
|
|
150
197
|
|
|
151
198
|
Plain `pi` from any folder also works; `pi-revit` just adds the right working folder on top.
|
|
152
199
|
|
|
@@ -165,19 +212,58 @@ Plain `pi` from any folder also works; `pi-revit` just adds the right working fo
|
|
|
165
212
|
| `search_api_docs` | Search the offline Revit API docs (works with no document open) |
|
|
166
213
|
| `execute_csharp` | Run a C# script in one auto-managed transaction — the escape hatch |
|
|
167
214
|
| `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 |
|
|
215
|
+
| `export_documents` | PDF/DWG/PNG/IFC export of sheets and views — sorted into `Models\<title>--<identity hash>\exports` |
|
|
216
|
+
| `get_model_health` | Warnings grouped + worksets, phases, design options audit |
|
|
217
|
+
| `read_revit_result` | Read bounded fragments of a saved large tool result; extension-only, no Revit call |
|
|
218
|
+
|
|
219
|
+
### Read complete results
|
|
220
|
+
|
|
221
|
+
Requested rows and parameter values are included in model-visible tool content.
|
|
222
|
+
Results up to 12,000 characters are complete inline. For a larger result, the Pi
|
|
223
|
+
extension saves the complete tool payload as UTF-8 JSON and returns `result_id`,
|
|
224
|
+
`file_path`, `total_chars`, `complete_inline: false`, and retrieval instructions.
|
|
225
|
+
|
|
226
|
+
Call `read_revit_result` with the returned ID and `offset: 0`, then follow each
|
|
227
|
+
`next_offset` until `has_more` is false. A requested fragment is at most 8,000
|
|
228
|
+
UTF-16 code units; it may be smaller so the escaped response stays within the
|
|
229
|
+
message limit. Concatenate each page's `text` in order. Individual fragments are
|
|
230
|
+
not standalone JSON objects from the original result. Use the returned offsets,
|
|
231
|
+
not byte counts or a guessed increment.
|
|
232
|
+
|
|
233
|
+
Result IDs are registered in memory by the current extension instance. After an
|
|
234
|
+
extension reload or new Pi process, an old ID may no longer resolve; use the
|
|
235
|
+
original absolute `file_path` with Pi's normal `read` tool while that file remains
|
|
236
|
+
available. Saved results live in a unique OS temporary directory, can contain
|
|
237
|
+
model data, and are subject to eventual OS/user cleanup. If saving fails after
|
|
238
|
+
Revit completed an operation, inspect actual model state before retrying a write.
|
|
239
|
+
|
|
240
|
+
Complete payload retrieval does not expand a tool's own query page or declared
|
|
241
|
+
limits. Continue `get_elements`/`get_element_types` pagination separately, and
|
|
242
|
+
check warning-group or projection truncation indicators. A bridge-only client
|
|
243
|
+
must consume `details.payload` for oversized results; `read_revit_result` belongs
|
|
244
|
+
to the Pi extension.
|
|
245
|
+
|
|
246
|
+
Display-name parameter filters now resolve on every element, including inside a
|
|
247
|
+
category/class scope. Explicit built-in IDs and shared GUIDs can retain collector
|
|
248
|
+
optimization. In `get_element_details`, `include.parameters` and
|
|
249
|
+
`include.type_parameters` are independent; disabling instance parameters still
|
|
250
|
+
allows a type-only result.
|
|
170
251
|
|
|
171
252
|
## Limitations — read before using on real projects
|
|
172
253
|
|
|
173
254
|
- **Write tools are unrestricted by design.** `set_parameters` and `execute_csharp` modify the
|
|
174
255
|
open model directly — there is no confirmation prompt and no sandbox. Writes run in named
|
|
175
|
-
transactions
|
|
176
|
-
|
|
177
|
-
|
|
256
|
+
transactions (`execute_csharp` attempts rollback after script failure;
|
|
257
|
+
`set_parameters` commits partial successes and reports each failure). Read the
|
|
258
|
+
actual transaction outcome and failed lists. Script result-projection failure
|
|
259
|
+
can leave a successful edit committed with a `returnValueError`; filesystem and
|
|
260
|
+
UI effects are separate from model rollback.
|
|
178
261
|
- The add-in multi-targets .NET 8 (Revit 2025/2026) and .NET 10 (Revit 2027); `deploy.ps1`
|
|
179
262
|
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.
|
|
263
|
+
so you only need the SDK for the Revit you run. The 0.3.0 changes were tested live
|
|
264
|
+
on Revit 2025.4.3 (build 25.4.30.30, German UI). Revit 2026/2027 and large-model
|
|
265
|
+
performance were not tested for this release. Export API/file checks do not
|
|
266
|
+
establish full DWG drawing or IFC schema/geometry validation.
|
|
181
267
|
- **One Revit instance at a time** is discoverable (last started wins). When that instance
|
|
182
268
|
closes or crashes, another one that is still running takes the slot over within 30s.
|
|
183
269
|
- 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) {
|
|
@@ -84,7 +86,7 @@ if (revitIsRunning()) {
|
|
|
84
86
|
console.log("pi-revit full installer");
|
|
85
87
|
console.log("This installs the Pi package, deploys the Revit add-in, and creates the workspace/global command.");
|
|
86
88
|
|
|
87
|
-
runCmd("Install the Pi package from npm",
|
|
89
|
+
runCmd("Install the matching Pi package from npm", `pi install ${packageSpec}`);
|
|
88
90
|
runPowerShellScript("deploy.ps1");
|
|
89
91
|
runPowerShellScript("setup-workspace.ps1");
|
|
90
92
|
|
|
@@ -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";
|
|
@@ -47,7 +48,13 @@ interface BridgeToolDescriptor {
|
|
|
47
48
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
48
49
|
const LONG_TIMEOUT_MS = 120_000;
|
|
49
50
|
const DISCOVERY_TIMEOUT_MS = 10_000;
|
|
50
|
-
const MAX_MODEL_CONTENT_CHARS = 12_000;
|
|
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. */
|
|
@@ -153,11 +160,88 @@ export async function bridgeRequest(
|
|
|
153
160
|
return payload;
|
|
154
161
|
}
|
|
155
162
|
|
|
156
|
-
export function capText(text: string): string {
|
|
157
|
-
if (text.length <= MAX_MODEL_CONTENT_CHARS) return text;
|
|
158
|
-
const suffix = `... [truncated at ${MAX_MODEL_CONTENT_CHARS} chars
|
|
159
|
-
return text.slice(0, Math.max(0, MAX_MODEL_CONTENT_CHARS - suffix.length)) + suffix;
|
|
160
|
-
}
|
|
163
|
+
export function capText(text: string): string {
|
|
164
|
+
if (text.length <= MAX_MODEL_CONTENT_CHARS) return text;
|
|
165
|
+
const suffix = `... [truncated preview at ${MAX_MODEL_CONTENT_CHARS} chars]`;
|
|
166
|
+
return text.slice(0, Math.max(0, MAX_MODEL_CONTENT_CHARS - suffix.length)) + suffix;
|
|
167
|
+
}
|
|
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
|
+
}
|
|
161
245
|
|
|
162
246
|
async function runBridgeTool(name: string, args: unknown, signal: AbortSignal | undefined, timeoutMs: number) {
|
|
163
247
|
const payload = (await bridgeRequest(
|
|
@@ -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) {
|
|
@@ -307,7 +384,8 @@ function registerPing(pi: ExtensionAPI, onBridgeAlive?: () => Promise<"ready" |
|
|
|
307
384
|
|
|
308
385
|
const REDISCOVERY_INTERVAL_MS = 15_000;
|
|
309
386
|
|
|
310
|
-
export default async function revitConnector(pi: ExtensionAPI) {
|
|
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.0",
|
|
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,8 @@
|
|
|
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 tests/installer/installer.test.cjs"
|
|
31
32
|
},
|
|
32
33
|
"files": [
|
|
33
34
|
"bin/",
|
package/skills/pi-revit/SKILL.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pi-revit
|
|
3
|
-
description: Work with the open Autodesk Revit model through the Revit bridge tools (ping, get_model_overview, get_elements, get_element_details, get_element_types, manage_selection, open_view, set_parameters, search_api_docs, execute_csharp, capture_view, export_documents, get_model_health). Use when the user asks about the Revit project, its elements, parameters, selection, or wants to change, script, capture, or export the model.
|
|
3
|
+
description: Work with the open Autodesk Revit model through the Revit bridge tools (ping, get_model_overview, get_elements, get_element_details, get_element_types, manage_selection, open_view, set_parameters, search_api_docs, execute_csharp, capture_view, export_documents, get_model_health) and retrieve saved results with read_revit_result. Use when the user asks about the Revit project, its elements, parameters, selection, or wants to change, script, capture, or export the model.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Revit
|
|
7
7
|
|
|
8
|
-
Work with the live Revit model. The
|
|
8
|
+
Work with the live Revit model. The bridge targets Revit 2025, 2026, and 2027; the 0.3.0 changes were tested live on Revit 2025. Bridge document tools require Revit running with a project open; `ping` and `search_api_docs` work without a document. `read_revit_result` reads an already saved result locally without contacting Revit.
|
|
9
9
|
|
|
10
10
|
## Tool selection
|
|
11
11
|
|
|
@@ -23,31 +23,41 @@ Work with the live Revit model. The tools call a headless bridge add-in inside R
|
|
|
23
23
|
| Everything else (create, delete, move, views, sheets, tagging, ...) | `execute_csharp` |
|
|
24
24
|
| PNG snapshot of a view (visual QA) | `capture_view` (advanced) |
|
|
25
25
|
| PDF/DWG/PNG/IFC file export | `export_documents` (advanced) |
|
|
26
|
-
| Warnings / model quality audit | `get_model_health` (advanced) |
|
|
26
|
+
| Warnings / model quality audit | `get_model_health` (advanced) |
|
|
27
|
+
| Continue a saved large tool result | `read_revit_result` (local, no Revit call) |
|
|
27
28
|
|
|
28
29
|
Workflow guidance:
|
|
29
30
|
|
|
30
|
-
- Call `get_model_overview`
|
|
31
|
+
- Call `get_model_overview` for the intended open model before changing it. Copy `project.documentId` unchanged into `expected_document_id` for `set_parameters`, `execute_csharp`, `export_documents`, `open_view`, and selection/zoom changes. `manage_selection` also requires it whenever `isolate_in_view: true`, even with action `get`. Pure reads may omit it; a supplied ID is always checked. A matching legacy `expected_document` title alone is insufficient.
|
|
32
|
+
- The ID belongs to one open document in one bridge session. Refresh it after closing/reopening the model or restarting Revit. If the guard rejects a call, activate the intended model and obtain its overview again; do not blindly substitute the currently active model's ID.
|
|
31
33
|
- `get_elements` is the listing/counting primitive (`count_only: true` for bare counts). It returns identity fields only (id, name, category, typeName, levelId); read parameter values with `get_element_details`. Prefer a `category` or `of_class` scope when filtering by a parameter's display name.
|
|
32
34
|
- The selection pipeline is `get_elements` -> ids -> `manage_selection` (action `set`); there is no inline filter on selection.
|
|
33
|
-
- `set_parameters`
|
|
35
|
+
- `set_parameters` handles bulk parameter writes and renames (the `Name` parameter covers levels, views, sheets, types). It can commit a partially successful batch: inspect every failed update, `commitWarnings`, and the reported transaction outcome before describing what changed.
|
|
34
36
|
- Parameter display names are LOCALIZED: in a non-English Revit UI, `Mark`, `Comments`, and every other display name appear under their translated names. When a display-name lookup or `parameter_names` filter finds nothing, or the document may be non-English, use the language-independent `BuiltInParameter` enum name instead (e.g. `ALL_MODEL_MARK` for Mark, `ALL_MODEL_INSTANCE_COMMENTS` for Comments) — `set_parameters`, `get_element_details.parameter_names`, and `get_elements` filter rules all accept them, and `get_element_details` reports each parameter's `builtInParameter` name for discovery.
|
|
35
37
|
- Before writing `execute_csharp` code, verify unfamiliar classes/members with `search_api_docs` (works with no document open; first query builds the index and takes a few seconds). The top match carries its remarks, parameter docs, and returns inline, and every public API enum value is searchable — trust the result over guessing or web search; narrow the query to promote a different match into the top slot.
|
|
36
|
-
- `export_documents`
|
|
38
|
+
- `export_documents` defaults to `Documents\pi-revit\Models\<model title>--<identity hash>\exports`. Use its returned `outputDir` and file paths as authoritative; do not construct a destination from the title or opaque `project.documentId`. Saved paths and cloud/server identities determine the folder; unsaved/unavailable identities use a session fallback. Save As can select a new folder. Pass `output_dir` when the user requests a different destination. Existing title-only folders remain untouched.
|
|
39
|
+
- Large tool payloads return a `result_id`, `file_path`, and continuation instructions. Call `read_revit_result` with that ID and `offset: 0`, then follow `next_offset` until `has_more` is false. Concatenate `text` fragments in order; each fragment is not a standalone JSON result. Offsets count UTF-16 code units. IDs last for the current extension instance; after reload, use the returned absolute file path with `read` while the file exists. Retrieval does not replace the original query's pagination or remove its limits.
|
|
37
40
|
|
|
38
41
|
## execute_csharp playbook
|
|
39
42
|
|
|
40
43
|
- Globals: `doc` (Document), `uidoc` (UIDocument), `uiapp` (UIApplication), and `Dump(value)` to record intermediates into the result's `dumps[]`.
|
|
41
|
-
- The
|
|
44
|
+
- The script runs inside one backend-owned transaction. Do not start another transaction on `doc` (sub-transactions are allowed). The tool checks commit status and attempts rollback on script failure; read its actual outcome instead of assuming rollback succeeded. Result projection can fail after a successful commit and report `returnValueError`. Filesystem and UI effects are separate from model rollback.
|
|
42
45
|
- Scripts must be fully synchronous: `await`/`async` is rejected at compile time; never block on `Task.Result`/`.Wait()`.
|
|
43
46
|
- Return primitives, strings, or anonymous objects/lists; raw Revit API objects are projected to compact shapes (Element -> `{id,name,category,typeName,levelId}`, ElementId -> number, XYZ -> `{x,y,z}`).
|
|
44
47
|
- Lengths are internal units (decimal feet) — convert with `UnitUtils.ConvertToInternalUnits`/`ConvertFromInternalUnits`.
|
|
45
|
-
- Common pitfalls: call `FamilySymbol.Activate()` before `NewFamilyInstance`; use collector-level filtering (`OfCategory`/`OfClass`/`WhereElementIsNotElementType`) and bounded loops
|
|
48
|
+
- Common pitfalls: call `FamilySymbol.Activate()` before `NewFamilyInstance`; use collector-level filtering (`OfCategory`/`OfClass`/`WhereElementIsNotElementType`) and bounded loops. The budget is 120s and Revit cannot be interrupted mid-script. The dialog guard attempts dismissive responses and reports `suppressedDialogs`; it cannot guarantee handling every modal dialog.
|
|
46
49
|
- `capture_view` returns a `filePath` to a temp PNG, never image data — open it with the read tool to actually see it.
|
|
47
50
|
|
|
48
|
-
## Failure modes
|
|
49
|
-
|
|
51
|
+
## Failure modes
|
|
52
|
+
|
|
53
|
+
- **Identity rejected**: no tool action was performed. Verify the intended active model, refresh `project.documentId`, and pass `expected_document_id` unchanged.
|
|
54
|
+
- **Partial UI/export effects**: selection or zoom can already have changed when isolation fails. Export errors can leave incomplete files, and IFC commit warnings are returned. Inspect the reported effects and output paths; model rollback does not remove files or reverse earlier UI actions.
|
|
55
|
+
- **Large-result save failed**: Revit may already have completed the operation. Verify its effects before retrying a write.
|
|
50
56
|
- **Bridge not reachable** ("Revit bridge is not available" / "Could not reach the Revit bridge"): Revit is not running or the add-in did not load. Ask the user to start Revit, then retry `ping`.
|
|
51
57
|
- **HTTP 409 / "No active Revit document is open."** (`hasActiveDocument: false`): Revit is running but no project is open. Ask the user to open a project, then retry. This fails immediately; do not wait or retry blindly.
|
|
52
58
|
- **Timeout** ("Revit did not answer within Ns", 30s default / 120s for execute_csharp, capture_view, export_documents): Revit is busy or showing a modal dialog. An already-started tool still runs to completion in Revit — verify model state (e.g. `get_elements`) before re-issuing a write.
|
|
53
|
-
- **Cancelled**: same caveat — the bridge cannot abort queued or running work, so verify model state before retrying a write tool.
|
|
59
|
+
- **Cancelled**: same caveat — the bridge cannot abort queued or running work, so verify model state before retrying a write tool.
|
|
60
|
+
|
|
61
|
+
## Upgrading from 0.2.x
|
|
62
|
+
|
|
63
|
+
Version 0.3.0 requires exact document IDs for the operations above. Update the Pi package and deploy the matching add-in with Revit closed, then restart Revit and start a fresh Pi session so the new schemas and instructions are loaded. Obtain a fresh overview before writes. `ping` reports an installed/loaded version mismatch. Setup preserves existing workspace `AGENTS.md`; merge these targeting, result-reading, and folder rules into an older workspace's instructions when needed.
|
|
@@ -406,8 +406,9 @@ namespace RevitBridge
|
|
|
406
406
|
object? output = tool.RequiresDocument
|
|
407
407
|
? await _queue.RunAsync(uiApp =>
|
|
408
408
|
{
|
|
409
|
-
var document = uiApp.ActiveUIDocument?.Document ?? throw new NoActiveDocumentException();
|
|
410
|
-
|
|
409
|
+
var document = uiApp.ActiveUIDocument?.Document ?? throw new NoActiveDocumentException();
|
|
410
|
+
RevitBridge.Tools.DocumentGuard.CheckForTool(args, document, tool.Name);
|
|
411
|
+
return tool.Execute(args, new ToolContext(document, uiApp));
|
|
411
412
|
}, TimeSpan.FromMilliseconds(timeoutMs))
|
|
412
413
|
// RequiresDocument = false tools never touch the Revit API, so they
|
|
413
414
|
// run right here on the server task instead of the CommandQueue.
|
|
@@ -435,9 +436,9 @@ namespace RevitBridge
|
|
|
435
436
|
? Math.Clamp(value, 1_000, 600_000)
|
|
436
437
|
: 30_000;
|
|
437
438
|
|
|
438
|
-
/// <summary>
|
|
439
|
-
///
|
|
440
|
-
///
|
|
439
|
+
/// <summary>Complete bounded JSON for model context; the full payload always
|
|
440
|
+
/// remains in details for the extension's saved-result retrieval. ToolOutput
|
|
441
|
+
/// compact text is a display summary, not a substitute for requested data.</summary>
|
|
441
442
|
private static object BuildToolResponse(string toolName, object? output)
|
|
442
443
|
{
|
|
443
444
|
object? payload = output;
|
|
@@ -448,12 +449,11 @@ namespace RevitBridge
|
|
|
448
449
|
compact = toolOutput.CompactText;
|
|
449
450
|
}
|
|
450
451
|
|
|
451
|
-
string text =
|
|
452
|
+
string text = JsonSerializer.Serialize(payload);
|
|
452
453
|
bool truncated = text.Length > MaxContentChars;
|
|
453
454
|
if (truncated)
|
|
454
455
|
{
|
|
455
|
-
|
|
456
|
-
text = text[..Math.Max(0, MaxContentChars - suffix.Length)] + suffix;
|
|
456
|
+
text = $"Result exceeds the {MaxContentChars}-character inline limit. The complete value is in details.payload; the Pi extension saves it locally and provides read_revit_result for bounded retrieval.";
|
|
457
457
|
}
|
|
458
458
|
|
|
459
459
|
return new
|
|
@@ -461,7 +461,7 @@ namespace RevitBridge
|
|
|
461
461
|
success = true,
|
|
462
462
|
toolName,
|
|
463
463
|
content = new[] { new { type = "text", text } },
|
|
464
|
-
details = new { payload, contentTruncated = truncated },
|
|
464
|
+
details = new { payload, summary = compact, contentTruncated = truncated },
|
|
465
465
|
isError = false,
|
|
466
466
|
};
|
|
467
467
|
}
|