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 +21 -0
- package/README.md +149 -0
- package/extensions/pi-revit/index.ts +239 -0
- package/package.json +28 -0
- package/scripts/build.ps1 +37 -0
- package/scripts/deploy.ps1 +99 -0
- package/scripts/revit.ps1 +112 -0
- package/scripts/setup-workspace.ps1 +53 -0
- package/scripts/uninstall.ps1 +103 -0
- package/skills/pi-revit/SKILL.md +50 -0
- package/src/Revit/Application.cs +94 -0
- package/src/Revit/BridgeServer.cs +419 -0
- package/src/Revit/CommandQueue.cs +93 -0
- package/src/Revit/RevitBridge.addin +11 -0
- package/src/Revit/RevitBridge.csproj +48 -0
- package/src/Revit/ToolRegistry.cs +109 -0
- package/src/Revit/Tools/CaptureView.cs +168 -0
- package/src/Revit/Tools/ExecuteCsharp.cs +418 -0
- package/src/Revit/Tools/ExportDocuments.cs +256 -0
- package/src/Revit/Tools/GetElementDetails.cs +318 -0
- package/src/Revit/Tools/GetElementTypes.cs +151 -0
- package/src/Revit/Tools/GetElements.cs +725 -0
- package/src/Revit/Tools/GetModelHealth.cs +191 -0
- package/src/Revit/Tools/GetModelOverview.cs +159 -0
- package/src/Revit/Tools/ManageSelection.cs +220 -0
- package/src/Revit/Tools/SearchApiDocs.cs +610 -0
- package/src/Revit/Tools/SetParameters.cs +382 -0
- package/src/Revit/Tools/ToolSupport.cs +261 -0
- package/workspace/AGENTS.md +23 -0
- package/workspace/pi-revit-global.cmd +31 -0
- package/workspace/pi-revit-here.cmd +5 -0
- package/workspace/project-AGENTS.md +8 -0
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
|
+
}
|