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 +6 -0
- package/README.md +12 -3
- package/bin/pi-revit.js +3 -5
- package/extensions/pi-revit/index.ts +95 -95
- package/package.json +4 -3
- package/scripts/build.ps1 +9 -3
- package/scripts/check-sdk.ps1 +66 -0
- package/scripts/deploy.ps1 +16 -4
- package/skills/pi-revit/SKILL.md +21 -21
- package/src/Revit/BridgeServer.cs +9 -9
- package/src/Revit/ToolRegistry.cs +32 -32
- package/src/Revit/Tools/DocumentGuard.cs +64 -64
- package/src/Revit/Tools/ExecuteCsharp.cs +22 -22
- package/src/Revit/Tools/ExportDocuments.cs +119 -119
- package/src/Revit/Tools/FailureGuard.cs +26 -26
- package/src/Revit/Tools/GetElementDetails.cs +10 -10
- package/src/Revit/Tools/GetElements.cs +17 -17
- package/src/Revit/Tools/GetModelOverview.cs +2 -2
- package/src/Revit/Tools/ManageSelection.cs +35 -35
- package/src/Revit/Tools/SetParameters.cs +14 -14
- package/src/Revit/Tools/ToolSupport.cs +1 -1
- package/workspace/AGENTS.md +45 -45
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|
package/scripts/deploy.ps1
CHANGED
|
@@ -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
|
-
#
|
|
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
|
|
package/skills/pi-revit/SKILL.md
CHANGED
|
@@ -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
|
}
|