pi-revit 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ahmad Altahlawi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,149 @@
1
+ # pi-revit
2
+
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
+
6
+ ```text
7
+ You: how many levels in the model?
8
+ Pi: calls get_model_overview → "There are 14 levels in the Revit model."
9
+
10
+ You: select all structural columns
11
+ Pi: get_elements → manage_selection → "Selected 222 structural columns."
12
+
13
+ You: rename level 'L1' to 'Ground Floor'
14
+ Pi: calls set_parameters → "Done — Level 'L1' is now 'Ground Floor'."
15
+ ```
16
+
17
+ ## How it works
18
+
19
+ ```text
20
+ Pi terminal session
21
+ │ native tools (registered by the pi-revit extension)
22
+ ▼
23
+ localhost HTTP bridge ← per-start token; connection info in %APPDATA%\RevitBridge\
24
+ │
25
+ ▼
26
+ headless Revit add-in ← no ribbon, no panels; just a bridge
27
+ │ ExternalEvent queue (Revit API thread)
28
+ ▼
29
+ Revit API ← reads run directly; writes run in one named transaction
30
+ ```
31
+
32
+ The extension discovers its tools from the bridge at startup (`/reload` re-discovers), so the
33
+ tool list always matches what the add-in serves. Everything between Pi and Revit is
34
+ local-machine only; note that Pi sends conversation context and tool results to your selected
35
+ LLM provider, like any Pi session.
36
+
37
+ ## Requirements
38
+
39
+ - Windows 10/11
40
+ - Autodesk Revit 2025, 2026, or 2027
41
+ - .NET SDK matching your Revit: [.NET 8](https://dotnet.microsoft.com/download/dotnet/8.0) for Revit 2025/2026, [.NET 10](https://dotnet.microsoft.com/download/dotnet/10.0) for Revit 2027
42
+ - [Node.js 20.3+](https://nodejs.org/)
43
+ - [Pi coding agent](https://pi.dev): `npm install -g --ignore-scripts @earendil-works/pi-coding-agent`
44
+
45
+ ## Install
46
+
47
+ Close Revit, then in PowerShell:
48
+
49
+ ```powershell
50
+ git clone https://github.com/Triavision-ai/pi-revit.git
51
+ cd pi-revit
52
+
53
+ # 1. Build + deploy the Revit add-in (RevitBridge.dll + the Roslyn DLLs for execute_csharp)
54
+ # Auto-detects every supported Revit (2025+) installed under Program Files and deploys to each.
55
+ # Only need the .NET SDK for the versions you have (e.g. just .NET 8 if you only run 2025/2026).
56
+ # For a non-default install location, pass -RevitVersion and -RevitApiPath, e.g.:
57
+ # ... scripts\deploy.ps1 -RevitVersion 2027 -RevitApiPath "D:\Autodesk\Revit 2027"
58
+ powershell -ExecutionPolicy Bypass -File scripts\deploy.ps1
59
+
60
+ # 2. Install the Pi package
61
+ pi install ./
62
+
63
+ # 3. Workspace + global pi-revit command
64
+ powershell -ExecutionPolicy Bypass -File scripts\setup-workspace.ps1
65
+ ```
66
+
67
+ Start Revit (click **Always Load** on the unsigned add-in prompt once) and open any
68
+ project. No panel or ribbon appears — the add-in is headless.
69
+
70
+ ## Use it
71
+
72
+ Open **any terminal** — PowerShell, CMD, or Windows Terminal — and type:
73
+
74
+ ```powershell
75
+ pi-revit
76
+ ```
77
+
78
+ That's all. Pi starts with the Revit tools ready:
79
+
80
+ ```text
81
+ > give me a model overview
82
+ > how many doors per level?
83
+ > select all structural columns
84
+ ```
85
+
86
+ **How this works:** install step 3 placed a small `pi-revit` command in the same folder as the
87
+ `pi` command itself. That folder is on your system PATH — which is exactly why *every* terminal
88
+ finds `pi-revit`, with no extra configuration. When you run it, it switches to your workspace at
89
+ `Documents\pi-revit` and starts Pi there, so your conventions file (`AGENTS.md`) loads
90
+ automatically and all Revit session history lives in one predictable place. The Revit tools
91
+ themselves are installed globally in Pi, and the extension discovers them live from the bridge
92
+ inside Revit each time a session starts.
93
+
94
+ **Per project:** create one subfolder per Revit project under `Documents\pi-revit\Projects\`:
95
+
96
+ ```powershell
97
+ pi-revit tower-b # Pi scoped to that project — its notes and its session history
98
+ pi-revit tower-b -c # continue that project's last session
99
+ ```
100
+
101
+ Plain `pi` from any folder also works; `pi-revit` just adds the right working folder on top.
102
+
103
+ ## Tools
104
+
105
+ | Tool | What it does |
106
+ |---|---|
107
+ | `ping` | Is the bridge reachable? Revit version |
108
+ | `get_model_overview` | Project info, units, levels, grids, category counts — call first |
109
+ | `get_elements` | Query/count elements of any category: parameter filters, pagination |
110
+ | `get_element_details` | Parameter values, location, bounding box, materials per element |
111
+ | `get_element_types` | Element types / family symbols, optional placed-instance counts |
112
+ | `manage_selection` | Get/set/clear the selection, zoom, temporary isolate |
113
+ | `set_parameters` | Bulk parameter writes + rename anything (one transaction per batch) |
114
+ | `search_api_docs` | Search the offline Revit API docs (works with no document open) |
115
+ | `execute_csharp` | Run a C# script in one auto-managed transaction — the escape hatch |
116
+ | `capture_view` | PNG snapshot of a view to a temp file (read the returned path to see it) |
117
+ | `export_documents` | PDF/DWG/PNG/IFC export of sheets and views |
118
+ | `get_model_health` | Warnings grouped + worksets, phases, design options audit |
119
+
120
+ ## Limitations — read before using on real projects
121
+
122
+ - **Write tools are unrestricted by design.** `set_parameters` and `execute_csharp` modify the
123
+ open model directly — there is no confirmation prompt and no sandbox. Writes run in named
124
+ transactions, undoable with Ctrl+Z in Revit (`execute_csharp` rolls back entirely on any
125
+ error; `set_parameters` commits partial successes and reports each failure), but the model is
126
+ yours to protect: test on copies, keep backups, read the result's `failed` lists.
127
+ - The add-in multi-targets .NET 8 (Revit 2025/2026) and .NET 10 (Revit 2027); `deploy.ps1`
128
+ auto-detects the Revit versions you have installed and builds only the matching framework(s),
129
+ so you only need the SDK for the Revit you run. Verified on Revit 2025 and 2027.
130
+ - **One Revit instance at a time** is discoverable (last started wins).
131
+ - A tool call that outlives its timeout is abandoned client-side but may still complete inside
132
+ Revit — verify model state before re-issuing a write.
133
+ - Long-running scripts cannot be interrupted mid-execution (Revit's API is single-threaded);
134
+ the `execute_csharp` budget is 120s.
135
+
136
+ ## Uninstall
137
+
138
+ Close Revit, then in PowerShell from the repo:
139
+
140
+ ```powershell
141
+ powershell -ExecutionPolicy Bypass -File scripts\uninstall.ps1
142
+ ```
143
+
144
+ This removes the Revit bridge add-in (every installed Revit version), the global `pi-revit`
145
+ command, the bridge runtime folder (`%APPDATA%\RevitBridge\`), and the Pi package registration.
146
+ Your workspace at `Documents\pi-revit` (notes + session history) is **preserved** — add
147
+ `-RemoveWorkspace` to delete it too, or `-RevitVersion 2026` to target a single Revit version.
148
+ Pi itself is left installed; remove it with `npm uninstall -g @earendil-works/pi-coding-agent` if
149
+ you want.
@@ -0,0 +1,239 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { Type, type TSchema } from "typebox";
3
+ import { readFile } from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+
7
+ interface BridgeInfo {
8
+ baseUrl: string;
9
+ token: string;
10
+ pid?: number;
11
+ revitVersion?: string;
12
+ }
13
+
14
+ interface ContentBlock {
15
+ type: string;
16
+ text: string;
17
+ }
18
+
19
+ interface BridgeToolResponse {
20
+ success?: boolean;
21
+ toolName?: string;
22
+ content?: ContentBlock[];
23
+ details?: unknown;
24
+ isError?: boolean;
25
+ error?: boolean;
26
+ message?: string;
27
+ hasActiveDocument?: boolean;
28
+ }
29
+
30
+ /** One entry of GET /tools, as served by ToolRegistry.Describe() on the bridge. */
31
+ interface BridgeToolDescriptor {
32
+ name: string;
33
+ label?: string;
34
+ description?: string;
35
+ category?: string;
36
+ tier?: string;
37
+ parameters?: unknown;
38
+ executionMode?: string;
39
+ write?: boolean;
40
+ requiresDocument?: boolean;
41
+ promptSnippet?: string | null;
42
+ promptGuidelines?: string[] | null;
43
+ }
44
+
45
+ const DEFAULT_TIMEOUT_MS = 30_000;
46
+ const LONG_TIMEOUT_MS = 120_000;
47
+ const DISCOVERY_TIMEOUT_MS = 10_000;
48
+ const MAX_MODEL_CONTENT_CHARS = 12_000;
49
+
50
+ /** Tools with a longer budget; everything else gets DEFAULT_TIMEOUT_MS. The same
51
+ * value is sent to the bridge as timeout_ms and used client-side via AbortSignal. */
52
+ const TOOL_TIMEOUTS_MS: Record<string, number> = {
53
+ execute_csharp: LONG_TIMEOUT_MS,
54
+ capture_view: LONG_TIMEOUT_MS,
55
+ export_documents: LONG_TIMEOUT_MS,
56
+ };
57
+
58
+ function toolTimeoutMs(name: string): number {
59
+ return TOOL_TIMEOUTS_MS[name] ?? DEFAULT_TIMEOUT_MS;
60
+ }
61
+
62
+ function bridgeInfoPath(): string {
63
+ const appData = process.env.APPDATA ?? path.join(os.homedir(), "AppData", "Roaming");
64
+ return path.join(appData, "RevitBridge", "bridge.json");
65
+ }
66
+
67
+ export async function readBridgeInfo(): Promise<BridgeInfo> {
68
+ const infoPath = bridgeInfoPath();
69
+ let raw: string;
70
+ try {
71
+ raw = await readFile(infoPath, "utf8");
72
+ } catch {
73
+ throw new Error(
74
+ `Revit bridge is not available. Start Revit with the bridge add-in loaded, then retry. Expected bridge info file: ${infoPath}`,
75
+ );
76
+ }
77
+
78
+ let info: BridgeInfo;
79
+ try {
80
+ info = JSON.parse(raw) as BridgeInfo;
81
+ } catch {
82
+ throw new Error(`Revit bridge info file is invalid JSON: ${infoPath}`);
83
+ }
84
+ if (!info.baseUrl || !info.token) {
85
+ throw new Error(`Revit bridge info file is invalid: ${infoPath}`);
86
+ }
87
+ return info;
88
+ }
89
+
90
+ /** Cancelling or timing out only abandons the HTTP request: the bridge has no
91
+ * cancellation path, so a queued or already-running work item may still execute
92
+ * in Revit (queued items expire only at the bridge-side timeout_ms deadline). */
93
+ function cancelledError(): Error {
94
+ return new Error(
95
+ "Revit bridge call was cancelled client-side; Revit may still run the queued or in-flight tool to completion. Verify model state (e.g. with get_elements/get_element_details) before retrying a write tool.",
96
+ );
97
+ }
98
+
99
+ function timeoutError(timeoutMs: number): Error {
100
+ return new Error(
101
+ `Revit did not answer within ${Math.ceil(timeoutMs / 1000)}s. Revit may be busy or showing a dialog; the request was abandoned client-side, but an already-started tool still runs to completion in Revit. Verify model state before retrying a write tool.`,
102
+ );
103
+ }
104
+
105
+ export async function bridgeRequest(
106
+ pathname: string,
107
+ init: { method: "GET" | "POST"; body?: string; query?: Record<string, string> },
108
+ signal?: AbortSignal,
109
+ timeoutMs = DEFAULT_TIMEOUT_MS,
110
+ ): Promise<unknown> {
111
+ const info = await readBridgeInfo();
112
+ const query = new URLSearchParams({ ...(init.query ?? {}), token: info.token });
113
+ const url = `${info.baseUrl}${pathname}?${query.toString()}`;
114
+
115
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
116
+ const signals = [timeoutSignal];
117
+ if (signal) signals.push(signal);
118
+
119
+ let response: Response;
120
+ try {
121
+ response = await fetch(url, {
122
+ method: init.method,
123
+ body: init.body,
124
+ headers: init.body ? { "content-type": "application/json; charset=utf-8" } : undefined,
125
+ signal: AbortSignal.any(signals),
126
+ });
127
+ } catch (error) {
128
+ if (signal?.aborted) throw cancelledError();
129
+ if (timeoutSignal.aborted) throw timeoutError(timeoutMs);
130
+ const reason = error instanceof Error ? error.message : String(error);
131
+ throw new Error(
132
+ `Could not reach the Revit bridge at ${info.baseUrl} (${reason}). Revit may have been closed; start Revit and retry.`,
133
+ );
134
+ }
135
+
136
+ let payload: unknown;
137
+ try {
138
+ payload = await response.json();
139
+ } catch {
140
+ // The abort signal also cancels body consumption: a timeout/cancel that
141
+ // fires after headers arrive must not be misreported as a bad response.
142
+ if (signal?.aborted) throw cancelledError();
143
+ if (timeoutSignal.aborted) throw timeoutError(timeoutMs);
144
+ throw new Error(`Revit bridge returned a non-JSON response (HTTP ${response.status}).`);
145
+ }
146
+
147
+ const body = payload as BridgeToolResponse;
148
+ if (!response.ok || body.error || body.isError || body.success === false) {
149
+ throw new Error(body.message ?? body.content?.[0]?.text ?? `Revit bridge request failed (HTTP ${response.status}).`);
150
+ }
151
+ return payload;
152
+ }
153
+
154
+ export function capText(text: string): string {
155
+ if (text.length <= MAX_MODEL_CONTENT_CHARS) return text;
156
+ const suffix = `... [truncated at ${MAX_MODEL_CONTENT_CHARS} chars; full payload is in details]`;
157
+ return text.slice(0, Math.max(0, MAX_MODEL_CONTENT_CHARS - suffix.length)) + suffix;
158
+ }
159
+
160
+ async function runBridgeTool(name: string, args: unknown, signal: AbortSignal | undefined, timeoutMs: number) {
161
+ const payload = (await bridgeRequest(
162
+ `/tools/${encodeURIComponent(name)}/execute`,
163
+ {
164
+ method: "POST",
165
+ body: JSON.stringify(args ?? {}),
166
+ query: { timeout_ms: String(timeoutMs) },
167
+ },
168
+ signal,
169
+ timeoutMs,
170
+ )) as BridgeToolResponse;
171
+
172
+ const content =
173
+ Array.isArray(payload.content) && payload.content.length > 0
174
+ ? payload.content.map((block) =>
175
+ typeof block.text === "string" ? { ...block, text: capText(block.text) } : block,
176
+ )
177
+ : [{ type: "text", text: capText(JSON.stringify(payload.details ?? {})) }];
178
+
179
+ return { content, details: payload.details };
180
+ }
181
+
182
+ function registerBridgeTool(pi: ExtensionAPI, descriptor: BridgeToolDescriptor) {
183
+ const timeoutMs = toolTimeoutMs(descriptor.name);
184
+ pi.registerTool({
185
+ name: descriptor.name,
186
+ label: descriptor.label ?? descriptor.name,
187
+ description: descriptor.description ?? `Revit bridge tool '${descriptor.name}'.`,
188
+ parameters: Type.Unsafe((descriptor.parameters ?? { type: "object", properties: {} }) as TSchema),
189
+ promptSnippet: descriptor.promptSnippet ?? undefined,
190
+ promptGuidelines: descriptor.promptGuidelines ?? undefined,
191
+ executionMode: descriptor.executionMode === "parallel" ? "parallel" : "sequential",
192
+ async execute(_toolCallId, params, signal) {
193
+ return runBridgeTool(descriptor.name, params, signal, timeoutMs);
194
+ },
195
+ });
196
+ }
197
+
198
+ function registerPing(pi: ExtensionAPI) {
199
+ pi.registerTool({
200
+ name: "ping",
201
+ label: "Ping Revit Bridge",
202
+ description: "Check that the Revit bridge is reachable and report the Revit version.",
203
+ parameters: Type.Object({}),
204
+ promptSnippet: "Check Revit bridge availability.",
205
+ promptGuidelines: ["Use ping when Revit tools fail or bridge availability is unclear."],
206
+ executionMode: "sequential",
207
+ async execute(_toolCallId, _params, signal) {
208
+ const payload = await bridgeRequest("/ping", { method: "GET" }, signal, 10_000);
209
+ return {
210
+ content: [{ type: "text", text: JSON.stringify(payload) }],
211
+ details: payload,
212
+ };
213
+ },
214
+ });
215
+ }
216
+
217
+ export default async function revitConnector(pi: ExtensionAPI) {
218
+ // ping is hard-coded: it must work (and report clearly) even when the
219
+ // bridge is down, so it is never part of /tools discovery.
220
+ registerPing(pi);
221
+
222
+ let descriptors: BridgeToolDescriptor[];
223
+ try {
224
+ const payload = (await bridgeRequest("/tools", { method: "GET" }, undefined, DISCOVERY_TIMEOUT_MS)) as {
225
+ tools?: BridgeToolDescriptor[];
226
+ };
227
+ descriptors = Array.isArray(payload?.tools) ? payload.tools : [];
228
+ } catch {
229
+ // Bridge down at startup (Revit closed, stale bridge.json, ...): keep
230
+ // only ping registered and never block pi startup. /reload re-discovers.
231
+ return;
232
+ }
233
+
234
+ for (const descriptor of descriptors) {
235
+ if (!descriptor || typeof descriptor.name !== "string" || !descriptor.name) continue;
236
+ if (descriptor.name === "ping") continue;
237
+ registerBridgeTool(pi, descriptor);
238
+ }
239
+ }
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "pi-revit",
3
+ "version": "0.1.0",
4
+ "description": "Native Pi connector for Autodesk Revit through a headless bridge add-in.",
5
+ "author": "Ahmad Altahlawi",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/Triavision-ai/pi-revit.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/Triavision-ai/pi-revit/issues"
13
+ },
14
+ "homepage": "https://github.com/Triavision-ai/pi-revit#readme",
15
+ "keywords": ["pi-package", "pi-extension", "revit", "bim", "autodesk", "revit-api"],
16
+ "files": ["extensions/", "skills/", "scripts/", "src/", "workspace/", "README.md"],
17
+ "pi": {
18
+ "extensions": ["./extensions/pi-revit/index.ts"],
19
+ "skills": ["./skills"]
20
+ },
21
+ "engines": {
22
+ "node": ">=20.3"
23
+ },
24
+ "peerDependencies": {
25
+ "@earendil-works/pi-coding-agent": "*",
26
+ "typebox": "*"
27
+ }
28
+ }
@@ -0,0 +1,37 @@
1
+ #Requires -Version 5.1
2
+ <#
3
+ Builds the Revit bridge add-in.
4
+
5
+ The project multi-targets net8.0-windows (Revit 2025/2026) and net10.0-windows (Revit 2027).
6
+ Omit -TargetFramework to build both; pass one to build only it (e.g. when only one Revit is installed).
7
+
8
+ Usage:
9
+ scripts\build.ps1
10
+ scripts\build.ps1 -Configuration Debug
11
+ scripts\build.ps1 -TargetFramework net10.0-windows -RevitApiPath "C:\Program Files\Autodesk\Revit 2027"
12
+ #>
13
+ param(
14
+ [string]$Configuration = 'Release',
15
+ [string]$RevitApiPath = '',
16
+ [string]$TargetFramework = ''
17
+ )
18
+
19
+ $ErrorActionPreference = 'Stop'
20
+ $project = Join-Path $PSScriptRoot '..\src\Revit\RevitBridge.csproj'
21
+
22
+ $buildArgs = @('build', $project, '-c', $Configuration)
23
+ if ($TargetFramework) {
24
+ # Override TargetFrameworks (plural) rather than TargetFramework: NuGet restore ignores a
25
+ # single -p:TargetFramework and still restores every framework the project declares, so a
26
+ # machine without the .NET 10 SDK fails the whole build with NETSDK1045 even when only
27
+ # net8.0 was asked for. Overriding the list restricts restore + build to this one framework.
28
+ $buildArgs += "-p:TargetFrameworks=$TargetFramework"
29
+ }
30
+ if ($RevitApiPath) {
31
+ $buildArgs += "-p:RevitApiPath=$RevitApiPath"
32
+ }
33
+
34
+ & dotnet @buildArgs
35
+ if ($LASTEXITCODE -ne 0) {
36
+ throw "dotnet build failed with exit code $LASTEXITCODE."
37
+ }
@@ -0,0 +1,99 @@
1
+ #Requires -Version 5.1
2
+ <#
3
+ Builds and deploys the Revit bridge add-in.
4
+
5
+ By default it auto-detects every supported Revit installed under %ProgramFiles%\Autodesk
6
+ (Revit 2025/2026 -> net8.0-windows, Revit 2027 -> net10.0-windows) and deploys to each one,
7
+ building only the framework(s) those installs need. Pass -RevitVersion to target a single
8
+ version explicitly (with -RevitApiPath when it is not in the default location).
9
+
10
+ Layout after deploy (per version):
11
+ %APPDATA%\Autodesk\Revit\Addins\<version>\RevitBridge.addin
12
+ %APPDATA%\Autodesk\Revit\Addins\<version>\RevitBridge\ (RevitBridge.dll + the full
13
+ build output: Roslyn/Microsoft.CodeAnalysis dependencies for execute_csharp)
14
+
15
+ Usage:
16
+ scripts\deploy.ps1 # auto-detect + deploy to all installed
17
+ scripts\deploy.ps1 -RevitVersion 2026
18
+ scripts\deploy.ps1 -RevitVersion 2027 -RevitApiPath "D:\Autodesk\Revit 2027"
19
+ scripts\deploy.ps1 -SkipBuild
20
+ #>
21
+ param(
22
+ [string]$RevitVersion = '',
23
+ [string]$Configuration = 'Release',
24
+ [string]$RevitApiPath = '',
25
+ [switch]$SkipBuild
26
+ )
27
+
28
+ $ErrorActionPreference = 'Stop'
29
+
30
+ # Revit 2025/2026 run on .NET 8; 2027 on .NET 10. Versions below 2025 run on .NET Framework
31
+ # and are not supported by this add-in.
32
+ $MinSupported = 2025
33
+ function Get-TargetFramework([int]$version) {
34
+ if ($version -ge 2027) { 'net10.0-windows' } else { 'net8.0-windows' }
35
+ }
36
+
37
+ # Resolve the targets to deploy as objects: { Version; Path; Tfm }.
38
+ $targets = @()
39
+ if ($RevitVersion) {
40
+ $v = [int]$RevitVersion
41
+ $path = if ($RevitApiPath) { $RevitApiPath } else { Join-Path $env:ProgramFiles "Autodesk\Revit $v" }
42
+ if (-not (Test-Path (Join-Path $path 'RevitAPI.dll'))) {
43
+ throw "RevitAPI.dll not found in '$path'. Pass -RevitApiPath with your Revit $v install folder."
44
+ }
45
+ $targets += [pscustomobject]@{ Version = $v; Path = $path; Tfm = (Get-TargetFramework $v) }
46
+ }
47
+ else {
48
+ # Auto-detect: scan %ProgramFiles%\Autodesk for "Revit <year>" folders that hold a RevitAPI.dll.
49
+ $autodesk = Join-Path $env:ProgramFiles 'Autodesk'
50
+ if (Test-Path $autodesk) {
51
+ Get-ChildItem -LiteralPath $autodesk -Directory -Filter 'Revit 20*' -ErrorAction SilentlyContinue |
52
+ ForEach-Object {
53
+ if (($_.Name -match '(\d{4})') -and (Test-Path (Join-Path $_.FullName 'RevitAPI.dll'))) {
54
+ $v = [int]$matches[1]
55
+ if ($v -ge $MinSupported) {
56
+ $targets += [pscustomobject]@{ Version = $v; Path = $_.FullName; Tfm = (Get-TargetFramework $v) }
57
+ }
58
+ else {
59
+ Write-Warning "Skipping Revit $v (only 2025+ is supported by this add-in)."
60
+ }
61
+ }
62
+ }
63
+ }
64
+ if (-not $targets) {
65
+ throw "No supported Revit (2025+) found under '$autodesk'. Install Revit, or pass -RevitVersion and -RevitApiPath for a non-default location."
66
+ }
67
+ $targets = @($targets | Sort-Object Version)
68
+ Write-Host ("Detected Revit: " + (($targets | ForEach-Object { $_.Version }) -join ', ')) -ForegroundColor Cyan
69
+ }
70
+
71
+ # Build once per distinct target framework, compiling against a matching RevitAPI.dll.
72
+ if (-not $SkipBuild) {
73
+ foreach ($group in ($targets | Group-Object Tfm)) {
74
+ $apiPath = ($group.Group | Select-Object -First 1).Path
75
+ & (Join-Path $PSScriptRoot 'build.ps1') -Configuration $Configuration -RevitApiPath $apiPath -TargetFramework $group.Name
76
+ }
77
+ }
78
+
79
+ # Deploy each detected version to its own Addins folder.
80
+ foreach ($t in $targets) {
81
+ $sourceDir = Join-Path $PSScriptRoot "..\src\Revit\bin\$Configuration\$($t.Tfm)"
82
+ $dll = Join-Path $sourceDir 'RevitBridge.dll'
83
+ if (-not (Test-Path $dll)) {
84
+ throw "Build output not found: $dll. Run scripts\build.ps1 first (or omit -SkipBuild)."
85
+ }
86
+
87
+ $addinsDir = Join-Path $env:APPDATA "Autodesk\Revit\Addins\$($t.Version)"
88
+ $targetDir = Join-Path $addinsDir 'RevitBridge'
89
+ New-Item -ItemType Directory -Force -Path $targetDir | Out-Null
90
+
91
+ # execute_csharp ships Roslyn: copy the ENTIRE build output (RevitBridge.dll,
92
+ # the Microsoft.CodeAnalysis dependency closure, pdb and deps.json) into the add-in folder.
93
+ Copy-Item -Path (Join-Path $sourceDir '*') -Destination $targetDir -Recurse -Force
94
+ Copy-Item (Join-Path $PSScriptRoot '..\src\Revit\RevitBridge.addin') $addinsDir -Force
95
+
96
+ Write-Host "Deployed Revit bridge add-in to $addinsDir (Revit $($t.Version), $($t.Tfm))" -ForegroundColor Green
97
+ }
98
+
99
+ Write-Host 'Restart Revit to load it. Verify with: scripts\revit.ps1 ping'
@@ -0,0 +1,112 @@
1
+ #Requires -Version 5.1
2
+ <#
3
+ Manual debug client for the Revit bridge. Not used by the Pi extension
4
+ (the extension calls the bridge directly with fetch).
5
+
6
+ Usage:
7
+ scripts\revit.ps1 ping
8
+ scripts\revit.ps1 tools
9
+ scripts\revit.ps1 describe-tool get_model_overview
10
+ scripts\revit.ps1 run get_model_overview '{}'
11
+ scripts\revit.ps1 run get_elements '{"category":"Walls","count_only":true}'
12
+ #>
13
+ param(
14
+ [Parameter(Position = 0)]
15
+ [ValidateSet('ping', 'tools', 'describe-tool', 'run')]
16
+ [string]$Command = 'ping',
17
+
18
+ [Parameter(Position = 1)]
19
+ [string]$ToolName,
20
+
21
+ [Parameter(Position = 2)]
22
+ [string]$JsonArgs = '{}',
23
+
24
+ [int]$TimeoutSec = 0
25
+ )
26
+
27
+ $ErrorActionPreference = 'Stop'
28
+ [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)
29
+ $OutputEncoding = [System.Text.UTF8Encoding]::new($false)
30
+ $bridgeInfoPath = Join-Path $env:APPDATA 'RevitBridge\bridge.json'
31
+
32
+ function Write-JsonLine($value) {
33
+ [Console]::Out.WriteLine(($value | ConvertTo-Json -Compress -Depth 20))
34
+ }
35
+
36
+ function Write-JsonError($message) {
37
+ Write-JsonLine @{ error = $true; message = $message }
38
+ exit 1
39
+ }
40
+
41
+ if ($TimeoutSec -le 0) {
42
+ $envTimeout = 0
43
+ if ([int]::TryParse([string]$env:REVIT_BRIDGE_TIMEOUT_SEC, [ref]$envTimeout) -and $envTimeout -gt 0) {
44
+ $TimeoutSec = $envTimeout
45
+ } else {
46
+ $TimeoutSec = 30
47
+ }
48
+ }
49
+
50
+ if (-not (Test-Path $bridgeInfoPath)) {
51
+ Write-JsonError "Revit bridge info not found. Start Revit with the bridge add-in loaded. Expected: $bridgeInfoPath"
52
+ }
53
+
54
+ try {
55
+ $info = Get-Content $bridgeInfoPath -Raw | ConvertFrom-Json
56
+ if (-not $info.baseUrl -or -not $info.token) {
57
+ Write-JsonError "Revit bridge info is invalid: $bridgeInfoPath"
58
+ }
59
+
60
+ switch ($Command) {
61
+ 'ping' {
62
+ $response = Invoke-RestMethod -Method Get -Uri "$($info.baseUrl)/ping`?token=$($info.token)" -TimeoutSec $TimeoutSec
63
+ Write-JsonLine $response
64
+ }
65
+ 'tools' {
66
+ $response = Invoke-RestMethod -Method Get -Uri "$($info.baseUrl)/tools`?token=$($info.token)" -TimeoutSec $TimeoutSec
67
+ Write-JsonLine $response
68
+ }
69
+ 'describe-tool' {
70
+ if ([string]::IsNullOrWhiteSpace($ToolName)) {
71
+ Write-JsonError 'describe-tool requires a tool name. Example: revit.ps1 describe-tool get_model_overview'
72
+ }
73
+ $encoded = [System.Uri]::EscapeDataString($ToolName)
74
+ $response = Invoke-RestMethod -Method Get -Uri "$($info.baseUrl)/tools/$encoded`?token=$($info.token)" -TimeoutSec $TimeoutSec
75
+ Write-JsonLine $response
76
+ }
77
+ 'run' {
78
+ if ([string]::IsNullOrWhiteSpace($ToolName)) {
79
+ Write-JsonError "run requires a tool name. Example: revit.ps1 run get_model_overview '{}'"
80
+ }
81
+ if ([string]::IsNullOrWhiteSpace($JsonArgs)) { $JsonArgs = '{}' }
82
+ # Validate locally so callers get a clear error before the bridge call.
83
+ $null = $JsonArgs | ConvertFrom-Json
84
+ $encoded = [System.Uri]::EscapeDataString($ToolName)
85
+ $uri = "$($info.baseUrl)/tools/$encoded/execute`?token=$($info.token)"
86
+ # Send bytes: PowerShell 5.1 would otherwise encode a string body as ISO-8859-1.
87
+ $bodyBytes = [System.Text.Encoding]::UTF8.GetBytes($JsonArgs)
88
+ $response = Invoke-RestMethod -Method Post -Uri $uri -Body $bodyBytes -ContentType 'application/json; charset=utf-8' -TimeoutSec $TimeoutSec
89
+ Write-JsonLine $response
90
+ }
91
+ }
92
+ }
93
+ catch {
94
+ # Surface the bridge's JSON error body (409 no-document, 403 stale token,
95
+ # 400 bad args, 404 unknown tool) instead of Invoke-RestMethod's generic
96
+ # status-line message. The body is already one-line JSON; print it verbatim.
97
+ $body = $null
98
+ if ($_.ErrorDetails -and -not [string]::IsNullOrWhiteSpace($_.ErrorDetails.Message)) {
99
+ $body = $_.ErrorDetails.Message
100
+ } elseif ($_.Exception -is [System.Net.WebException] -and $_.Exception.Response) {
101
+ # Windows PowerShell 5.1: read the WebException response stream directly.
102
+ try {
103
+ $reader = [System.IO.StreamReader]::new($_.Exception.Response.GetResponseStream(), [System.Text.Encoding]::UTF8)
104
+ $body = $reader.ReadToEnd()
105
+ } catch { $body = $null }
106
+ }
107
+ if (-not [string]::IsNullOrWhiteSpace($body)) {
108
+ [Console]::Out.WriteLine($body.Trim())
109
+ exit 1
110
+ }
111
+ Write-JsonError $_.Exception.Message
112
+ }