pi-revit 0.2.7 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,50 @@ 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.2.9] - 2026-07-21
11
+
12
+ ### Added
13
+ - Self-healing tool discovery: when pi starts before Revit, the extension now keeps
14
+ retrying tool discovery in the background (every 15s) and also re-discovers on a
15
+ successful `ping` — no more sessions stuck with only `ping` registered until a fresh
16
+ pi start. When tools arrive mid-session, `ping`'s result says so.
17
+ - `set_parameters` and `execute_csharp` accept an optional `expected_document` (the model
18
+ title): if the active document differs — e.g. the user switched models mid-session —
19
+ the write fails cleanly instead of landing in the wrong model.
20
+ - `get_element_details.parameter_names` now also matches language-independent
21
+ BuiltInParameter enum names (e.g. `ALL_MODEL_MARK`), so filtering works in non-English
22
+ Revit UIs where display names are localized.
23
+
24
+ ### Fixed
25
+ - `get_element_details` no longer reports a misleading "0 params" when a
26
+ `parameter_names` filter simply matched nothing — it now reports "N of M params
27
+ matched parameter_names" so localization misses are visible. (This explains the
28
+ earlier "0 params vs 38 params" reports: different filter arguments, not flaky reads.)
29
+ - `get_elements`: a display-name filter rule in an **unscoped** query (no category /
30
+ of_class) is no longer promoted to a pinned collector filter based on a 50-element
31
+ probe — it stays on the per-element post-scan path, so categories beyond the probe
32
+ window can't be silently dropped when the same parameter name maps to different ids.
33
+
34
+ ### Changed
35
+ - SKILL.md: guidance on localized parameter names (prefer BuiltInParameter enum names)
36
+ and on using `expected_document` for long sessions / multiple open models.
37
+ - README: new "Safety model" section stating explicitly what the add-in enforces and
38
+ that write-confirmation UX is a client-side decision.
39
+
40
+ Requires redeploying the Revit add-in (`scripts\deploy.ps1` with Revit closed, then
41
+ restart Revit).
42
+
43
+ ## [0.2.8] - 2026-07-21
44
+
45
+ ### Fixed
46
+ - `search_api_docs`: overload-targeted queries no longer fail on comma spacing.
47
+ `Wall.Create(Document,Curve` (no space) and `Wall.Create( Document, Curve` now match the
48
+ same overloads as `Wall.Create(Document, Curve` — the query's spacing around commas and
49
+ parentheses is normalized to the rendered signature style before matching.
50
+
51
+ Requires redeploying the Revit add-in (`scripts\deploy.ps1` with Revit closed, then restart
52
+ Revit).
53
+
10
54
  ## [0.2.7] - 2026-07-21
11
55
 
12
56
  ### Added
package/README.md CHANGED
@@ -29,10 +29,32 @@ headless Revit add-in ← no ribbon, no panels; just a bridge
29
29
  Revit API ← reads run directly; writes run in one named transaction
30
30
  ```
31
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.
32
+ The extension discovers its tools from the bridge at startup (retrying in the background until
33
+ Revit is up), so the tool list always matches what the add-in serves. Everything between Pi and
34
+ Revit is local-machine only; note that Pi sends conversation context and tool results to your
35
+ selected LLM provider, like any Pi session.
36
+
37
+ ## Safety model
38
+
39
+ Be deliberate about pointing an LLM at a real project model. The add-in enforces what it can
40
+ enforce mechanically, and is honest about what it cannot:
41
+
42
+ - Every write tool is flagged `write: true` — that flag is the machine-readable signal a client
43
+ can gate on. Whether a write needs human confirmation is a **client-side decision**: the
44
+ add-in cannot know your policy, so confirmation UX belongs in the Pi client/agent layer, not
45
+ here.
46
+ - All writes run in one named transaction: committed on success, rolled back on failure, always
47
+ visible in Revit's undo history. Commit-time warnings are reported back (`commitWarnings`);
48
+ error-severity failures roll back with Revit's failure text.
49
+ - `execute_csharp` is an unrestricted escape hatch by design — scripts have full CLR access.
50
+ Treat it like giving the agent a macro editor, on a model you have saved or can restore.
51
+ - Blocking popups are auto-answered so Revit can never hang behind a dialog; unrecognized
52
+ dialogs get the dismissive answer (Cancel/Close/No), never a blind OK.
53
+ - Writes accept an optional `expected_document` check so a queued write cannot silently land in
54
+ a different model than intended.
55
+
56
+ Practical advice: work on saved models, keep worksharing backups/central protection as usual,
57
+ and review the agent's summary of what changed after any write session.
36
58
 
37
59
  ## Requirements
38
60
 
@@ -275,7 +275,7 @@ async function announceUpdateOnce(notify: (message: string, level: "info") => vo
275
275
  }
276
276
  }
277
277
 
278
- function registerPing(pi: ExtensionAPI) {
278
+ function registerPing(pi: ExtensionAPI, onBridgeAlive?: () => Promise<"ready" | "registered" | "failed">) {
279
279
  pi.registerTool({
280
280
  name: "ping",
281
281
  label: "Ping Revit Bridge",
@@ -287,18 +287,71 @@ function registerPing(pi: ExtensionAPI) {
287
287
  async execute(_toolCallId, _params, signal) {
288
288
  const payload = await bridgeRequest("/ping", { method: "GET" }, signal, 10_000);
289
289
  const warning = versionMismatch((payload as { addinVersion?: string }).addinVersion);
290
+ // The bridge is alive: if this session started before Revit and only has
291
+ // ping, register the bridge tools now and tell the model they arrived.
292
+ let registrationNote = "";
293
+ if (onBridgeAlive) {
294
+ const state = await onBridgeAlive();
295
+ if (state === "registered")
296
+ registrationNote = "\nNOTE: The Revit bridge tools (get_elements, set_parameters, execute_csharp, ...) were just registered in this session and are available from now on.";
297
+ else if (state === "failed")
298
+ registrationNote = "\nNOTE: Bridge tool discovery failed even though ping succeeded; retry ping or restart pi.";
299
+ }
290
300
  return {
291
- content: [{ type: "text", text: JSON.stringify(payload) + (warning ? `\nWARNING: ${warning}` : "") }],
301
+ content: [{ type: "text", text: JSON.stringify(payload) + (warning ? `\nWARNING: ${warning}` : "") + registrationNote }],
292
302
  details: payload,
293
303
  };
294
304
  },
295
305
  });
296
306
  }
297
307
 
308
+ const REDISCOVERY_INTERVAL_MS = 15_000;
309
+
298
310
  export default async function revitConnector(pi: ExtensionAPI) {
311
+ // Self-healing discovery: when pi starts before Revit is ready, the initial
312
+ // GET /tools fails and only ping is registered. Rather than requiring a
313
+ // fresh pi start (/reload does not reliably re-run async registration), a
314
+ // background retry keeps probing until the bridge appears, and a successful
315
+ // ping also triggers an immediate attempt.
316
+ let bridgeToolsRegistered = false;
317
+ let discoveryInFlight: Promise<boolean> | null = null;
318
+
319
+ async function discoverAndRegister(): Promise<boolean> {
320
+ if (bridgeToolsRegistered) return true;
321
+ if (discoveryInFlight) return discoveryInFlight;
322
+ discoveryInFlight = (async () => {
323
+ try {
324
+ const payload = (await bridgeRequest("/tools", { method: "GET" }, undefined, DISCOVERY_TIMEOUT_MS)) as {
325
+ tools?: BridgeToolDescriptor[];
326
+ };
327
+ const descriptors = Array.isArray(payload?.tools) ? payload.tools : [];
328
+ if (descriptors.length === 0) return false;
329
+ for (const descriptor of descriptors) {
330
+ if (!descriptor || typeof descriptor.name !== "string" || !descriptor.name) continue;
331
+ if (descriptor.name === "ping") continue;
332
+ registerBridgeTool(pi, descriptor);
333
+ }
334
+ bridgeToolsRegistered = true;
335
+ return true;
336
+ } catch {
337
+ // Bridge down (Revit closed, still starting, stale bridge.json):
338
+ // stay on ping only and try again later.
339
+ return false;
340
+ } finally {
341
+ discoveryInFlight = null;
342
+ }
343
+ })();
344
+ return discoveryInFlight;
345
+ }
346
+
299
347
  // ping is hard-coded: it must work (and report clearly) even when the
300
- // bridge is down, so it is never part of /tools discovery.
301
- registerPing(pi);
348
+ // bridge is down, so it is never part of /tools discovery. A successful
349
+ // ping doubles as a re-discovery trigger — the natural first call in a
350
+ // session that finds itself without bridge tools.
351
+ registerPing(pi, async () => {
352
+ if (bridgeToolsRegistered) return "ready";
353
+ return (await discoverAndRegister()) ? "registered" : "failed";
354
+ });
302
355
 
303
356
  // Surface an incomplete update (see versionMismatch) once per session, right
304
357
  // where the user lands after running `pi update --extensions`. Bridge down at
@@ -314,21 +367,12 @@ export default async function revitConnector(pi: ExtensionAPI) {
314
367
  }
315
368
  });
316
369
 
317
- let descriptors: BridgeToolDescriptor[];
318
- try {
319
- const payload = (await bridgeRequest("/tools", { method: "GET" }, undefined, DISCOVERY_TIMEOUT_MS)) as {
320
- tools?: BridgeToolDescriptor[];
321
- };
322
- descriptors = Array.isArray(payload?.tools) ? payload.tools : [];
323
- } catch {
324
- // Bridge down at startup (Revit closed, stale bridge.json, ...): keep
325
- // only ping registered and never block pi startup. /reload re-discovers.
326
- return;
327
- }
370
+ if (await discoverAndRegister()) return;
328
371
 
329
- for (const descriptor of descriptors) {
330
- if (!descriptor || typeof descriptor.name !== "string" || !descriptor.name) continue;
331
- if (descriptor.name === "ping") continue;
332
- registerBridgeTool(pi, descriptor);
333
- }
372
+ // Never block pi startup on Revit: keep retrying quietly in the background
373
+ // and stop the moment discovery succeeds.
374
+ const timer = setInterval(async () => {
375
+ if (await discoverAndRegister()) clearInterval(timer);
376
+ }, REDISCOVERY_INTERVAL_MS);
377
+ timer.unref?.();
334
378
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-revit",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
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",
@@ -1,80 +1,80 @@
1
- #Requires -Version 5.1
2
- <#
3
- Sets up the pi-revit user environment on this PC:
4
-
5
- 1. Workspace at Documents\pi-revit (AGENTS.md conventions, Models\ output tree,
6
- double-click launcher). An existing AGENTS.md is never overwritten.
7
- 2. Global `pi-revit` command installed next to the `pi` command, so any terminal can run:
8
- pi-revit -> Pi in the workspace
9
- pi-revit -c -> continue the last session
10
- Model output sorts itself: the Revit tools file each export under
11
- Models\<model title>\ automatically, keyed by the document it came from.
12
-
13
- Idempotent — safe to re-run. Usage:
14
- scripts\setup-workspace.ps1
15
- scripts\setup-workspace.ps1 -WorkspaceDir "D:\work\pi-revit"
16
- #>
17
- param(
18
- [string]$WorkspaceDir = (Join-Path ([Environment]::GetFolderPath('MyDocuments')) 'pi-revit')
19
- )
20
-
21
- $ErrorActionPreference = 'Stop'
22
- $templates = Join-Path (Split-Path $PSScriptRoot -Parent) 'workspace'
23
-
24
- # 1. Workspace folders. Models\ is where the Revit tools file per-model output.
25
- New-Item -ItemType Directory -Force -Path (Join-Path $WorkspaceDir 'Models') | Out-Null
26
-
27
- # Conventions / notes: copy only when missing so user edits survive re-runs.
28
- $workspaceAgents = Join-Path $WorkspaceDir 'AGENTS.md'
29
- if (-not (Test-Path $workspaceAgents)) {
30
- Copy-Item (Join-Path $templates 'AGENTS.md') $workspaceAgents
31
- }
32
-
33
- # Launcher is code, not user data: always refresh.
34
- Copy-Item (Join-Path $templates 'pi-revit-here.cmd') (Join-Path $WorkspaceDir 'pi-revit.cmd') -Force
35
-
36
- Write-Host "Workspace ready: $WorkspaceDir" -ForegroundColor Green
37
-
38
- # 2. Global pi-revit command next to pi (that directory is on PATH by definition).
39
- # The template's workspace path is rewritten to honor -WorkspaceDir.
40
- # Under `npx pi-revit`, npx prepends its ephemeral cache's node_modules\.bin to
41
- # PATH, and that folder holds a pi shim pulled in via peerDependencies. A launcher
42
- # written next to that shim lands in a folder that is neither on the user's own
43
- # PATH nor long-lived, so skip pi entries inside this package's node_modules tree
44
- # or any npx cache and use the user's permanent pi instead.
45
- function Get-PermanentPiCommand {
46
- # Long-form both sides of the prefix comparison: PATH entries may carry 8.3
47
- # short names (AHMAD~1.TAH) while $PSScriptRoot is normalized, and a form
48
- # mismatch would silently defeat the own-tree exclusion.
49
- function Resolve-LongPath([string]$p) {
50
- try { (Get-Item -LiteralPath $p -ErrorAction Stop).FullName } catch { $p }
51
- }
52
- $ownTree = $null
53
- $repoRoot = Resolve-LongPath (Split-Path $PSScriptRoot -Parent)
54
- $idx = $repoRoot.LastIndexOf('\node_modules\', [StringComparison]::OrdinalIgnoreCase)
55
- if ($idx -ge 0) { $ownTree = $repoRoot.Substring(0, $idx + '\node_modules\'.Length) }
56
- Get-Command pi -All -ErrorAction SilentlyContinue | Where-Object {
57
- if (-not $_.Source) { return $false }
58
- $src = Resolve-LongPath $_.Source
59
- ($src -notmatch '\\_npx\\') -and
60
- (-not $ownTree -or -not $src.StartsWith($ownTree, [StringComparison]::OrdinalIgnoreCase))
61
- } | Select-Object -First 1
62
- }
63
-
64
- $pi = Get-PermanentPiCommand
65
- if ($pi) {
66
- $binDir = Split-Path $pi.Source -Parent
67
- # Literal line swap (no regex): the workspace path may contain characters
68
- # like $ that a -replace replacement string would misinterpret.
69
- $globalCmd = Get-Content (Join-Path $templates 'pi-revit-global.cmd') | ForEach-Object {
70
- if ($_ -like 'set "WORKSPACE=*') { 'set "WORKSPACE=' + $WorkspaceDir + '"' } else { $_ }
71
- }
72
- # cmd.exe parses .cmd files in the console's OEM code page: write the launcher
73
- # in that encoding so workspace paths with non-ASCII characters (user names,
74
- # localized folder names) stay intact.
75
- Set-Content -Path (Join-Path $binDir 'pi-revit.cmd') -Value $globalCmd -Encoding Oem
76
- Write-Host "Global command installed: $(Join-Path $binDir 'pi-revit.cmd')" -ForegroundColor Green
77
- Write-Host 'Run pi-revit from any terminal (pi-revit -c continues the last session).'
78
- } else {
79
- Write-Warning 'No permanent pi command found on PATH. Install Pi first (npm install -g --ignore-scripts @earendil-works/pi-coding-agent), then run npx.cmd -y pi-revit again (or re-run this script) to get the global pi-revit command.'
80
- }
1
+ #Requires -Version 5.1
2
+ <#
3
+ Sets up the pi-revit user environment on this PC:
4
+
5
+ 1. Workspace at Documents\pi-revit (AGENTS.md conventions, Models\ output tree,
6
+ double-click launcher). An existing AGENTS.md is never overwritten.
7
+ 2. Global `pi-revit` command installed next to the `pi` command, so any terminal can run:
8
+ pi-revit -> Pi in the workspace
9
+ pi-revit -c -> continue the last session
10
+ Model output sorts itself: the Revit tools file each export under
11
+ Models\<model title>\ automatically, keyed by the document it came from.
12
+
13
+ Idempotent — safe to re-run. Usage:
14
+ scripts\setup-workspace.ps1
15
+ scripts\setup-workspace.ps1 -WorkspaceDir "D:\work\pi-revit"
16
+ #>
17
+ param(
18
+ [string]$WorkspaceDir = (Join-Path ([Environment]::GetFolderPath('MyDocuments')) 'pi-revit')
19
+ )
20
+
21
+ $ErrorActionPreference = 'Stop'
22
+ $templates = Join-Path (Split-Path $PSScriptRoot -Parent) 'workspace'
23
+
24
+ # 1. Workspace folders. Models\ is where the Revit tools file per-model output.
25
+ New-Item -ItemType Directory -Force -Path (Join-Path $WorkspaceDir 'Models') | Out-Null
26
+
27
+ # Conventions / notes: copy only when missing so user edits survive re-runs.
28
+ $workspaceAgents = Join-Path $WorkspaceDir 'AGENTS.md'
29
+ if (-not (Test-Path $workspaceAgents)) {
30
+ Copy-Item (Join-Path $templates 'AGENTS.md') $workspaceAgents
31
+ }
32
+
33
+ # Launcher is code, not user data: always refresh.
34
+ Copy-Item (Join-Path $templates 'pi-revit-here.cmd') (Join-Path $WorkspaceDir 'pi-revit.cmd') -Force
35
+
36
+ Write-Host "Workspace ready: $WorkspaceDir" -ForegroundColor Green
37
+
38
+ # 2. Global pi-revit command next to pi (that directory is on PATH by definition).
39
+ # The template's workspace path is rewritten to honor -WorkspaceDir.
40
+ # Under `npx pi-revit`, npx prepends its ephemeral cache's node_modules\.bin to
41
+ # PATH, and that folder holds a pi shim pulled in via peerDependencies. A launcher
42
+ # written next to that shim lands in a folder that is neither on the user's own
43
+ # PATH nor long-lived, so skip pi entries inside this package's node_modules tree
44
+ # or any npx cache and use the user's permanent pi instead.
45
+ function Get-PermanentPiCommand {
46
+ # Long-form both sides of the prefix comparison: PATH entries may carry 8.3
47
+ # short names (SOMEUS~1.NAM) while $PSScriptRoot is normalized, and a form
48
+ # mismatch would silently defeat the own-tree exclusion.
49
+ function Resolve-LongPath([string]$p) {
50
+ try { (Get-Item -LiteralPath $p -ErrorAction Stop).FullName } catch { $p }
51
+ }
52
+ $ownTree = $null
53
+ $repoRoot = Resolve-LongPath (Split-Path $PSScriptRoot -Parent)
54
+ $idx = $repoRoot.LastIndexOf('\node_modules\', [StringComparison]::OrdinalIgnoreCase)
55
+ if ($idx -ge 0) { $ownTree = $repoRoot.Substring(0, $idx + '\node_modules\'.Length) }
56
+ Get-Command pi -All -ErrorAction SilentlyContinue | Where-Object {
57
+ if (-not $_.Source) { return $false }
58
+ $src = Resolve-LongPath $_.Source
59
+ ($src -notmatch '\\_npx\\') -and
60
+ (-not $ownTree -or -not $src.StartsWith($ownTree, [StringComparison]::OrdinalIgnoreCase))
61
+ } | Select-Object -First 1
62
+ }
63
+
64
+ $pi = Get-PermanentPiCommand
65
+ if ($pi) {
66
+ $binDir = Split-Path $pi.Source -Parent
67
+ # Literal line swap (no regex): the workspace path may contain characters
68
+ # like $ that a -replace replacement string would misinterpret.
69
+ $globalCmd = Get-Content (Join-Path $templates 'pi-revit-global.cmd') | ForEach-Object {
70
+ if ($_ -like 'set "WORKSPACE=*') { 'set "WORKSPACE=' + $WorkspaceDir + '"' } else { $_ }
71
+ }
72
+ # cmd.exe parses .cmd files in the console's OEM code page: write the launcher
73
+ # in that encoding so workspace paths with non-ASCII characters (user names,
74
+ # localized folder names) stay intact.
75
+ Set-Content -Path (Join-Path $binDir 'pi-revit.cmd') -Value $globalCmd -Encoding Oem
76
+ Write-Host "Global command installed: $(Join-Path $binDir 'pi-revit.cmd')" -ForegroundColor Green
77
+ Write-Host 'Run pi-revit from any terminal (pi-revit -c continues the last session).'
78
+ } else {
79
+ Write-Warning 'No permanent pi command found on PATH. Install Pi first (npm install -g --ignore-scripts @earendil-works/pi-coding-agent), then run npx.cmd -y pi-revit again (or re-run this script) to get the global pi-revit command.'
80
+ }