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 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.2.18] - 2026-08-21
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: calls set_parameters → "Done — Level 'L1' is now 'Ground Floor'."
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 ← reads run directly; writes run in one named transaction
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
- - Every write tool is flagged `write: true` — that flag is the machine-readable signal a client
43
- can gate on. Whether a write needs human confirmation is a **client-side decision**: the
44
- add-in cannot know your policy, so confirmation UX belongs in the Pi client/agent layer, not
45
- here.
46
- - All writes run in one named transaction: committed on success, rolled back on failure, always
47
- visible in Revit's undo history. Commit-time warnings are reported back (`commitWarnings`);
48
- error-severity failures roll back with Revit's failure text.
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
- - Blocking popups are auto-answered so Revit can never hang behind a dialog; unrecognized
52
- dialogs get the dismissive answer (Cancel/Close/No), never a blind OK.
53
- - Writes accept an optional `expected_document` check so a queued write cannot silently land in
54
- a different model than intended.
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
- ## Use it
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` — the add-in derives the folder from the
146
- document being exported, so even a session that touches many models files every output under
147
- the right one, with no naming decision from you or the AI. Each model folder carries a
148
- `model.txt` recording the model's GUID and file path, so two models that share a title stay
149
- distinguishable.
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 — auto-sorted into `Models\<model>\exports` |
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, undoable with Ctrl+Z in Revit (`execute_csharp` rolls back entirely on any
176
- error; `set_parameters` commits partial successes and reports each failure), but the model is
177
- yours to protect: test on copies, keep backups, read the result's `failed` lists.
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. Verified on Revit 2025 and 2027.
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 npm:pi-revit\n 2. Builds and deploys the 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.`);
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", "pi install npm:pi-revit");
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; full payload is in details]`;
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
- const content =
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.2.18",
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/",
@@ -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 tools call a headless bridge add-in inside Revit (2025, 2026, or 2027); Revit must be running with a project open (only `ping` and `search_api_docs` work without a document).
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` first when starting work on an unfamiliar model — one call returns project metadata, units, levels, grids, and category counts.
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` is the home for bulk parameter writes AND renames (the `Name` parameter covers levels, views, sheets, types). One transaction per batch; per-element failures are reported. Pass `expected_document` (the model title) when several models are open or the session is long — it makes the write fail cleanly instead of landing in a different active document.
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` files its output under `Documents\pi-revit\Models\<model title>\exports` automatically when `output_dir` is omitted — keyed to the exported document, so it lands right even across many models. Pass `output_dir` only when the user names a different target.
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 transaction is automatic: the whole script runs inside ONE backend-owned transaction — committed on success, rolled back on any exception. Do not open your own `Transaction` (sub-transactions are fine).
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 — the budget is 120s and Revit cannot be interrupted mid-script; modal dialogs are auto-dismissed and reported in `suppressedDialogs`.
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
- return tool.Execute(args, new ToolContext(document, uiApp));
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>Compact text for model context; full payload in details. Tools may
439
- /// return a ToolOutput (CompactText -> content, Payload -> details.payload) or
440
- /// any plain object (serialized to both).</summary>
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 = compact ?? JsonSerializer.Serialize(payload ?? new { });
452
+ string text = JsonSerializer.Serialize(payload);
452
453
  bool truncated = text.Length > MaxContentChars;
453
454
  if (truncated)
454
455
  {
455
- string suffix = $"... [truncated at {MaxContentChars} chars; full payload is in details]";
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
  }