pi-revit 0.3.0 → 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
@@ -7,6 +7,12 @@ Format follows [Keep a Changelog](https://keepachangelog.com/); version headers
7
7
  Every published version gets an entry with **Added** / **Changed** / **Fixed** sections
8
8
  describing what the user will notice — not internal refactors.
9
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
+
10
16
  ## [0.3.0] - 2026-09-09
11
17
 
12
18
  ### Added
package/README.md CHANGED
@@ -107,9 +107,18 @@ npx.cmd -y pi-revit
107
107
  ```
108
108
 
109
109
  This installs the Pi package, builds and deploys the Revit bridge add-in, creates the
110
- `Documents\pi-revit` workspace, and installs the global `pi-revit` command.
111
-
112
- 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
113
122
  project. No panel or ribbon appears — the add-in is headless.
114
123
 
115
124
  ### Manual npm install
package/bin/pi-revit.js CHANGED
@@ -37,10 +37,10 @@ function runCmd(title, commandLine) {
37
37
  if (result.status !== 0) process.exit(result.status ?? 1);
38
38
  }
39
39
 
40
- function runPowerShellScript(scriptName) {
40
+ function runPowerShellScript(scriptName, args = []) {
41
41
  const scriptPath = path.join(scriptsDir, scriptName);
42
42
  if (!fs.existsSync(scriptPath)) fail(`missing script: ${scriptPath}`);
43
- run(scriptName, "powershell.exe", ["-ExecutionPolicy", "Bypass", "-File", scriptPath]);
43
+ run(scriptName, "powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", scriptPath, ...args]);
44
44
  }
45
45
 
46
46
  function revitIsRunning() {
@@ -74,9 +74,7 @@ if (!commandExists("pi")) {
74
74
  fail("the 'pi' command was not found on PATH. Install Pi first: npm install -g --ignore-scripts @earendil-works/pi-coding-agent");
75
75
  }
76
76
 
77
- if (!commandExists("dotnet")) {
78
- fail("the 'dotnet' command was not found on PATH. Install the .NET SDK required by your Revit version.");
79
- }
77
+ runPowerShellScript("deploy.ps1", ["-CheckOnly", "-OfferDownload"]);
80
78
 
81
79
  if (revitIsRunning()) {
82
80
  waitForEnter();
@@ -1,7 +1,7 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { Type, type TSchema } from "typebox";
3
- import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
4
- import { randomUUID } from "node:crypto";
3
+ import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
4
+ import { randomUUID } from "node:crypto";
5
5
  import os from "node:os";
6
6
  import path from "node:path";
7
7
  import { fileURLToPath } from "node:url";
@@ -48,13 +48,13 @@ interface BridgeToolDescriptor {
48
48
  const DEFAULT_TIMEOUT_MS = 30_000;
49
49
  const LONG_TIMEOUT_MS = 120_000;
50
50
  const DISCOVERY_TIMEOUT_MS = 10_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
+ 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;
58
58
 
59
59
  /** Tools with a longer budget; everything else gets DEFAULT_TIMEOUT_MS. The same
60
60
  * value is sent to the bridge as timeout_ms and used client-side via AbortSignal. */
@@ -160,88 +160,88 @@ export async function bridgeRequest(
160
160
  return payload;
161
161
  }
162
162
 
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
- }
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
+ }
245
245
 
246
246
  async function runBridgeTool(name: string, args: unknown, signal: AbortSignal | undefined, timeoutMs: number) {
247
247
  const payload = (await bridgeRequest(
@@ -255,7 +255,7 @@ async function runBridgeTool(name: string, args: unknown, signal: AbortSignal |
255
255
  timeoutMs,
256
256
  )) as BridgeToolResponse;
257
257
 
258
- return { content: await modelContent(name, payload), details: payload.details };
258
+ return { content: await modelContent(name, payload), details: payload.details };
259
259
  }
260
260
 
261
261
  function registerBridgeTool(pi: ExtensionAPI, descriptor: BridgeToolDescriptor) {
@@ -384,8 +384,8 @@ function registerPing(pi: ExtensionAPI, onBridgeAlive?: () => Promise<"ready" |
384
384
 
385
385
  const REDISCOVERY_INTERVAL_MS = 15_000;
386
386
 
387
- export default async function revitConnector(pi: ExtensionAPI) {
388
- registerResultReader(pi);
387
+ export default async function revitConnector(pi: ExtensionAPI) {
388
+ registerResultReader(pi);
389
389
  // Self-healing discovery: when pi starts before Revit is ready, the initial
390
390
  // GET /tools fails and only ping is registered. Rather than requiring a
391
391
  // fresh pi start (/reload does not reliably re-run async registration), a
@@ -406,7 +406,7 @@ export default async function revitConnector(pi: ExtensionAPI) {
406
406
  if (descriptors.length === 0) return false;
407
407
  for (const descriptor of descriptors) {
408
408
  if (!descriptor || typeof descriptor.name !== "string" || !descriptor.name) continue;
409
- if (descriptor.name === "ping" || descriptor.name === "read_revit_result") continue;
409
+ if (descriptor.name === "ping" || descriptor.name === "read_revit_result") continue;
410
410
  registerBridgeTool(pi, descriptor);
411
411
  }
412
412
  bridgeToolsRegistered = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-revit",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Native Pi connector for Autodesk Revit. Run npx.cmd -y pi-revit for the full Windows install.",
5
5
  "author": "Ahmad Altahlawi",
6
6
  "license": "MIT",
@@ -27,8 +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",
31
- "test:installer": "node tests/installer/installer.test.cjs"
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"
32
33
  },
33
34
  "files": [
34
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
 
@@ -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) 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.
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 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.
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,41 +23,41 @@ Work with the live Revit model. The bridge targets Revit 2025, 2026, and 2027; t
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) |
27
- | Continue a saved large tool result | `read_revit_result` (local, no Revit call) |
26
+ | Warnings / model quality audit | `get_model_health` (advanced) |
27
+ | Continue a saved large tool result | `read_revit_result` (local, no Revit call) |
28
28
 
29
29
  Workflow guidance:
30
30
 
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
+ - 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.
33
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.
34
34
  - The selection pipeline is `get_elements` -> ids -> `manage_selection` (action `set`); there is no inline filter on selection.
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.
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.
36
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.
37
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.
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.
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.
40
40
 
41
41
  ## execute_csharp playbook
42
42
 
43
43
  - Globals: `doc` (Document), `uidoc` (UIDocument), `uiapp` (UIApplication), and `Dump(value)` to record intermediates into the result's `dumps[]`.
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.
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.
45
45
  - Scripts must be fully synchronous: `await`/`async` is rejected at compile time; never block on `Task.Result`/`.Wait()`.
46
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}`).
47
47
  - Lengths are internal units (decimal feet) — convert with `UnitUtils.ConvertToInternalUnits`/`ConvertFromInternalUnits`.
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.
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.
49
49
  - `capture_view` returns a `filePath` to a temp PNG, never image data — open it with the read tool to actually see it.
50
50
 
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.
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.
56
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`.
57
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.
58
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.
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.
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,9 +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
- RevitBridge.Tools.DocumentGuard.CheckForTool(args, document, tool.Name);
411
- 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));
412
412
  }, TimeSpan.FromMilliseconds(timeoutMs))
413
413
  // RequiresDocument = false tools never touch the Revit API, so they
414
414
  // run right here on the server task instead of the CommandQueue.
@@ -436,9 +436,9 @@ namespace RevitBridge
436
436
  ? Math.Clamp(value, 1_000, 600_000)
437
437
  : 30_000;
438
438
 
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>
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>
442
442
  private static object BuildToolResponse(string toolName, object? output)
443
443
  {
444
444
  object? payload = output;
@@ -449,11 +449,11 @@ namespace RevitBridge
449
449
  compact = toolOutput.CompactText;
450
450
  }
451
451
 
452
- string text = JsonSerializer.Serialize(payload);
452
+ string text = JsonSerializer.Serialize(payload);
453
453
  bool truncated = text.Length > MaxContentChars;
454
454
  if (truncated)
455
455
  {
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.";
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, summary = compact, contentTruncated = truncated },
464
+ details = new { payload, summary = compact, contentTruncated = truncated },
465
465
  isError = false,
466
466
  };
467
467
  }