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 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.2.18] - 2026-08-21
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: 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.
@@ -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
- Start Revit (click **Always Load** on the unsigned add-in prompt once) and open any
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
- ## Use it
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` — 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.
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 — auto-sorted into `Models\<model>\exports` |
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, 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.
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. Verified on Revit 2025 and 2027.
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 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) {
@@ -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
- if (!commandExists("dotnet")) {
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", "pi install npm:pi-revit");
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; full payload is in details]`;
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
- 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) {
@@ -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.2.18",
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
- $buildArgs = @('build', $project, '-c', $Configuration)
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
+ }
@@ -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
- # Build once per distinct target framework, compiling against a matching RevitAPI.dll.
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