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 +44 -0
- package/README.md +26 -4
- package/extensions/pi-revit/index.ts +64 -20
- package/package.json +1 -1
- package/scripts/setup-workspace.ps1 +80 -80
- package/scripts/uninstall.ps1 +146 -146
- package/skills/pi-revit/SKILL.md +2 -1
- package/src/Revit/Tools/ExecuteCsharp.cs +6 -0
- package/src/Revit/Tools/GetElementDetails.cs +26 -6
- package/src/Revit/Tools/GetElements.cs +12 -5
- package/src/Revit/Tools/SearchApiDocs.cs +13 -5
- package/src/Revit/Tools/SetParameters.cs +6 -0
- package/src/Revit/Tools/ToolSupport.cs +301 -261
package/scripts/uninstall.ps1
CHANGED
|
@@ -1,146 +1,146 @@
|
|
|
1
|
-
#Requires -Version 5.1
|
|
2
|
-
<#
|
|
3
|
-
Removes everything the pi-revit install scripts placed on this PC:
|
|
4
|
-
|
|
5
|
-
- Revit bridge add-in: %APPDATA%\Autodesk\Revit\Addins\<version>\RevitBridge.addin
|
|
6
|
-
%APPDATA%\Autodesk\Revit\Addins\<version>\RevitBridge\
|
|
7
|
-
- Global command: the pi-revit command next to the pi command
|
|
8
|
-
- Bridge runtime: %APPDATA%\RevitBridge\ (per-start connection token / bridge.json)
|
|
9
|
-
- Pi package: the pi-revit registration (pi remove), best-effort when pi is on PATH
|
|
10
|
-
|
|
11
|
-
The workspace at Documents\pi-revit holds YOUR data (AGENTS.md notes, per-project
|
|
12
|
-
session history) and is preserved by default. Pass -RemoveWorkspace to delete it too.
|
|
13
|
-
|
|
14
|
-
Pi itself (the global coding agent) is yours to manage and is never touched.
|
|
15
|
-
|
|
16
|
-
Close Revit before running. Idempotent — safe to re-run.
|
|
17
|
-
|
|
18
|
-
Usage:
|
|
19
|
-
scripts\uninstall.ps1
|
|
20
|
-
scripts\uninstall.ps1 -RevitVersion 2027
|
|
21
|
-
scripts\uninstall.ps1 -RemoveWorkspace
|
|
22
|
-
#>
|
|
23
|
-
param(
|
|
24
|
-
[string]$RevitVersion = '',
|
|
25
|
-
[string]$WorkspaceDir = (Join-Path ([Environment]::GetFolderPath('MyDocuments')) 'pi-revit'),
|
|
26
|
-
[switch]$RemoveWorkspace
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
$ErrorActionPreference = 'Stop'
|
|
30
|
-
|
|
31
|
-
function Remove-IfExists {
|
|
32
|
-
param([string]$Path, [string]$Label)
|
|
33
|
-
if (Test-Path -LiteralPath $Path) {
|
|
34
|
-
Remove-Item -LiteralPath $Path -Recurse -Force
|
|
35
|
-
Write-Host "Removed $Label : $Path" -ForegroundColor Green
|
|
36
|
-
} else {
|
|
37
|
-
Write-Host "Not present, skipped $Label : $Path"
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
# 1. Revit add-in. With no -RevitVersion, sweep every Addins\<version> folder so the
|
|
42
|
-
# user need not remember which Revit they deployed to.
|
|
43
|
-
$addinsRoot = Join-Path $env:APPDATA 'Autodesk\Revit\Addins'
|
|
44
|
-
$versions = if ($RevitVersion) {
|
|
45
|
-
@($RevitVersion)
|
|
46
|
-
} elseif (Test-Path -LiteralPath $addinsRoot) {
|
|
47
|
-
Get-ChildItem -LiteralPath $addinsRoot -Directory | ForEach-Object { $_.Name }
|
|
48
|
-
} else {
|
|
49
|
-
@()
|
|
50
|
-
}
|
|
51
|
-
foreach ($v in $versions) {
|
|
52
|
-
$dir = Join-Path $addinsRoot $v
|
|
53
|
-
Remove-IfExists (Join-Path $dir 'RevitBridge.addin') "Revit $v add-in manifest"
|
|
54
|
-
Remove-IfExists (Join-Path $dir 'RevitBridge') "Revit $v add-in folder"
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
# 2. Global pi-revit command next to the user's permanent pi. When this script runs
|
|
58
|
-
# from an npx cache or a node_modules copy, PATH may lead Get-Command to a pi shim
|
|
59
|
-
# inside that ephemeral tree first (see setup-workspace.ps1) — the launcher was
|
|
60
|
-
# never installed there, so resolve the permanent pi the same way setup does.
|
|
61
|
-
function Get-PermanentPiCommand {
|
|
62
|
-
# Long-form both sides of the prefix comparison: PATH entries may carry 8.3
|
|
63
|
-
# short names (
|
|
64
|
-
# mismatch would silently defeat the own-tree exclusion.
|
|
65
|
-
function Resolve-LongPath([string]$p) {
|
|
66
|
-
try { (Get-Item -LiteralPath $p -ErrorAction Stop).FullName } catch { $p }
|
|
67
|
-
}
|
|
68
|
-
$ownTree = $null
|
|
69
|
-
$repoRoot = Resolve-LongPath (Split-Path $PSScriptRoot -Parent)
|
|
70
|
-
$idx = $repoRoot.LastIndexOf('\node_modules\', [StringComparison]::OrdinalIgnoreCase)
|
|
71
|
-
if ($idx -ge 0) { $ownTree = $repoRoot.Substring(0, $idx + '\node_modules\'.Length) }
|
|
72
|
-
Get-Command pi -All -ErrorAction SilentlyContinue | Where-Object {
|
|
73
|
-
if (-not $_.Source) { return $false }
|
|
74
|
-
$src = Resolve-LongPath $_.Source
|
|
75
|
-
($src -notmatch '\\_npx\\') -and
|
|
76
|
-
(-not $ownTree -or -not $src.StartsWith($ownTree, [StringComparison]::OrdinalIgnoreCase))
|
|
77
|
-
} | Select-Object -First 1
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
$pi = Get-Command pi -ErrorAction SilentlyContinue
|
|
81
|
-
$piPermanent = Get-PermanentPiCommand
|
|
82
|
-
if ($piPermanent) {
|
|
83
|
-
$binDir = Split-Path $piPermanent.Source -Parent
|
|
84
|
-
Remove-IfExists (Join-Path $binDir 'pi-revit.cmd') 'global pi-revit command'
|
|
85
|
-
} else {
|
|
86
|
-
Write-Host 'No permanent pi found on PATH; skipping the global pi-revit command.'
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
# 3. Bridge runtime folder (connection token; recreated on each Revit start).
|
|
90
|
-
Remove-IfExists (Join-Path $env:APPDATA 'RevitBridge') 'bridge runtime folder'
|
|
91
|
-
|
|
92
|
-
# 4. Pi package registration. Pi keys a package by its install source: the npm
|
|
93
|
-
# installer (npx.cmd -y pi-revit) registers `npm:pi-revit`, while an install
|
|
94
|
-
# from a clone of this repo (`pi install ./`) registers the repo path resolved
|
|
95
|
-
# from the repo root. Try both forms; each succeeds only for the source that
|
|
96
|
-
# is actually registered, so a machine with both gets both removed.
|
|
97
|
-
if ($pi) {
|
|
98
|
-
$repoRoot = Split-Path $PSScriptRoot -Parent
|
|
99
|
-
# When this script runs from the npm-installed copy, `pi remove npm:pi-revit`
|
|
100
|
-
# deletes the very folder the script lives in — and Windows fails that delete
|
|
101
|
-
# (EBUSY) while any working directory sits inside it. Step our own process out
|
|
102
|
-
# if needed; the calling shell's location we can only warn about.
|
|
103
|
-
if ($repoRoot -match '\\node_modules\\' -and
|
|
104
|
-
(Get-Location).Path.StartsWith($repoRoot, [StringComparison]::OrdinalIgnoreCase)) {
|
|
105
|
-
Set-Location $env:SystemRoot
|
|
106
|
-
Write-Warning "This PowerShell was started inside $repoRoot. If the package removal below fails, run 'cd \' in your shell and re-run the uninstall."
|
|
107
|
-
}
|
|
108
|
-
try {
|
|
109
|
-
$removed = @()
|
|
110
|
-
# './' resolves against the repo root, so it runs from there — and first,
|
|
111
|
-
# while this folder is guaranteed to still exist. 'npm:pi-revit' then runs
|
|
112
|
-
# from a neutral directory so no working directory blocks the folder delete.
|
|
113
|
-
foreach ($source in @('./', 'npm:pi-revit')) {
|
|
114
|
-
Push-Location $(if ($source -eq './') { $repoRoot } else { $env:SystemRoot })
|
|
115
|
-
try {
|
|
116
|
-
$global:LASTEXITCODE = 0
|
|
117
|
-
& pi remove $source
|
|
118
|
-
if ($LASTEXITCODE -eq 0) {
|
|
119
|
-
$removed += $source
|
|
120
|
-
}
|
|
121
|
-
} finally {
|
|
122
|
-
Pop-Location
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
if ($removed.Count -gt 0) {
|
|
126
|
-
Write-Host "Removed the pi-revit Pi package ($($removed -join ', '))." -ForegroundColor Green
|
|
127
|
-
} else {
|
|
128
|
-
Write-Warning "No pi-revit Pi package registration was found. If pi still lists it, run 'pi remove npm:pi-revit' (npm install) or 'pi remove ./' from this repo (install from source)."
|
|
129
|
-
}
|
|
130
|
-
} catch {
|
|
131
|
-
Write-Warning "Could not auto-remove the Pi package; run 'pi remove npm:pi-revit' (npm install) or 'pi remove ./' from this repo (install from source). ($_)"
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
# 5. Workspace (user data) — opt-in only.
|
|
136
|
-
if ($RemoveWorkspace) {
|
|
137
|
-
Remove-IfExists $WorkspaceDir 'workspace'
|
|
138
|
-
} else {
|
|
139
|
-
Write-Host ''
|
|
140
|
-
Write-Host "Workspace preserved (your notes + session history): $WorkspaceDir" -ForegroundColor Yellow
|
|
141
|
-
Write-Host 'Pass -RemoveWorkspace to delete it too.'
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
Write-Host ''
|
|
145
|
-
Write-Host 'pi-revit uninstalled. Pi itself is untouched; remove it yourself if you want:' -ForegroundColor Green
|
|
146
|
-
Write-Host ' npm uninstall -g @earendil-works/pi-coding-agent'
|
|
1
|
+
#Requires -Version 5.1
|
|
2
|
+
<#
|
|
3
|
+
Removes everything the pi-revit install scripts placed on this PC:
|
|
4
|
+
|
|
5
|
+
- Revit bridge add-in: %APPDATA%\Autodesk\Revit\Addins\<version>\RevitBridge.addin
|
|
6
|
+
%APPDATA%\Autodesk\Revit\Addins\<version>\RevitBridge\
|
|
7
|
+
- Global command: the pi-revit command next to the pi command
|
|
8
|
+
- Bridge runtime: %APPDATA%\RevitBridge\ (per-start connection token / bridge.json)
|
|
9
|
+
- Pi package: the pi-revit registration (pi remove), best-effort when pi is on PATH
|
|
10
|
+
|
|
11
|
+
The workspace at Documents\pi-revit holds YOUR data (AGENTS.md notes, per-project
|
|
12
|
+
session history) and is preserved by default. Pass -RemoveWorkspace to delete it too.
|
|
13
|
+
|
|
14
|
+
Pi itself (the global coding agent) is yours to manage and is never touched.
|
|
15
|
+
|
|
16
|
+
Close Revit before running. Idempotent — safe to re-run.
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
scripts\uninstall.ps1
|
|
20
|
+
scripts\uninstall.ps1 -RevitVersion 2027
|
|
21
|
+
scripts\uninstall.ps1 -RemoveWorkspace
|
|
22
|
+
#>
|
|
23
|
+
param(
|
|
24
|
+
[string]$RevitVersion = '',
|
|
25
|
+
[string]$WorkspaceDir = (Join-Path ([Environment]::GetFolderPath('MyDocuments')) 'pi-revit'),
|
|
26
|
+
[switch]$RemoveWorkspace
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
$ErrorActionPreference = 'Stop'
|
|
30
|
+
|
|
31
|
+
function Remove-IfExists {
|
|
32
|
+
param([string]$Path, [string]$Label)
|
|
33
|
+
if (Test-Path -LiteralPath $Path) {
|
|
34
|
+
Remove-Item -LiteralPath $Path -Recurse -Force
|
|
35
|
+
Write-Host "Removed $Label : $Path" -ForegroundColor Green
|
|
36
|
+
} else {
|
|
37
|
+
Write-Host "Not present, skipped $Label : $Path"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# 1. Revit add-in. With no -RevitVersion, sweep every Addins\<version> folder so the
|
|
42
|
+
# user need not remember which Revit they deployed to.
|
|
43
|
+
$addinsRoot = Join-Path $env:APPDATA 'Autodesk\Revit\Addins'
|
|
44
|
+
$versions = if ($RevitVersion) {
|
|
45
|
+
@($RevitVersion)
|
|
46
|
+
} elseif (Test-Path -LiteralPath $addinsRoot) {
|
|
47
|
+
Get-ChildItem -LiteralPath $addinsRoot -Directory | ForEach-Object { $_.Name }
|
|
48
|
+
} else {
|
|
49
|
+
@()
|
|
50
|
+
}
|
|
51
|
+
foreach ($v in $versions) {
|
|
52
|
+
$dir = Join-Path $addinsRoot $v
|
|
53
|
+
Remove-IfExists (Join-Path $dir 'RevitBridge.addin') "Revit $v add-in manifest"
|
|
54
|
+
Remove-IfExists (Join-Path $dir 'RevitBridge') "Revit $v add-in folder"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# 2. Global pi-revit command next to the user's permanent pi. When this script runs
|
|
58
|
+
# from an npx cache or a node_modules copy, PATH may lead Get-Command to a pi shim
|
|
59
|
+
# inside that ephemeral tree first (see setup-workspace.ps1) — the launcher was
|
|
60
|
+
# never installed there, so resolve the permanent pi the same way setup does.
|
|
61
|
+
function Get-PermanentPiCommand {
|
|
62
|
+
# Long-form both sides of the prefix comparison: PATH entries may carry 8.3
|
|
63
|
+
# short names (SOMEUS~1.NAM) while $PSScriptRoot is normalized, and a form
|
|
64
|
+
# mismatch would silently defeat the own-tree exclusion.
|
|
65
|
+
function Resolve-LongPath([string]$p) {
|
|
66
|
+
try { (Get-Item -LiteralPath $p -ErrorAction Stop).FullName } catch { $p }
|
|
67
|
+
}
|
|
68
|
+
$ownTree = $null
|
|
69
|
+
$repoRoot = Resolve-LongPath (Split-Path $PSScriptRoot -Parent)
|
|
70
|
+
$idx = $repoRoot.LastIndexOf('\node_modules\', [StringComparison]::OrdinalIgnoreCase)
|
|
71
|
+
if ($idx -ge 0) { $ownTree = $repoRoot.Substring(0, $idx + '\node_modules\'.Length) }
|
|
72
|
+
Get-Command pi -All -ErrorAction SilentlyContinue | Where-Object {
|
|
73
|
+
if (-not $_.Source) { return $false }
|
|
74
|
+
$src = Resolve-LongPath $_.Source
|
|
75
|
+
($src -notmatch '\\_npx\\') -and
|
|
76
|
+
(-not $ownTree -or -not $src.StartsWith($ownTree, [StringComparison]::OrdinalIgnoreCase))
|
|
77
|
+
} | Select-Object -First 1
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
$pi = Get-Command pi -ErrorAction SilentlyContinue
|
|
81
|
+
$piPermanent = Get-PermanentPiCommand
|
|
82
|
+
if ($piPermanent) {
|
|
83
|
+
$binDir = Split-Path $piPermanent.Source -Parent
|
|
84
|
+
Remove-IfExists (Join-Path $binDir 'pi-revit.cmd') 'global pi-revit command'
|
|
85
|
+
} else {
|
|
86
|
+
Write-Host 'No permanent pi found on PATH; skipping the global pi-revit command.'
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# 3. Bridge runtime folder (connection token; recreated on each Revit start).
|
|
90
|
+
Remove-IfExists (Join-Path $env:APPDATA 'RevitBridge') 'bridge runtime folder'
|
|
91
|
+
|
|
92
|
+
# 4. Pi package registration. Pi keys a package by its install source: the npm
|
|
93
|
+
# installer (npx.cmd -y pi-revit) registers `npm:pi-revit`, while an install
|
|
94
|
+
# from a clone of this repo (`pi install ./`) registers the repo path resolved
|
|
95
|
+
# from the repo root. Try both forms; each succeeds only for the source that
|
|
96
|
+
# is actually registered, so a machine with both gets both removed.
|
|
97
|
+
if ($pi) {
|
|
98
|
+
$repoRoot = Split-Path $PSScriptRoot -Parent
|
|
99
|
+
# When this script runs from the npm-installed copy, `pi remove npm:pi-revit`
|
|
100
|
+
# deletes the very folder the script lives in — and Windows fails that delete
|
|
101
|
+
# (EBUSY) while any working directory sits inside it. Step our own process out
|
|
102
|
+
# if needed; the calling shell's location we can only warn about.
|
|
103
|
+
if ($repoRoot -match '\\node_modules\\' -and
|
|
104
|
+
(Get-Location).Path.StartsWith($repoRoot, [StringComparison]::OrdinalIgnoreCase)) {
|
|
105
|
+
Set-Location $env:SystemRoot
|
|
106
|
+
Write-Warning "This PowerShell was started inside $repoRoot. If the package removal below fails, run 'cd \' in your shell and re-run the uninstall."
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
$removed = @()
|
|
110
|
+
# './' resolves against the repo root, so it runs from there — and first,
|
|
111
|
+
# while this folder is guaranteed to still exist. 'npm:pi-revit' then runs
|
|
112
|
+
# from a neutral directory so no working directory blocks the folder delete.
|
|
113
|
+
foreach ($source in @('./', 'npm:pi-revit')) {
|
|
114
|
+
Push-Location $(if ($source -eq './') { $repoRoot } else { $env:SystemRoot })
|
|
115
|
+
try {
|
|
116
|
+
$global:LASTEXITCODE = 0
|
|
117
|
+
& pi remove $source
|
|
118
|
+
if ($LASTEXITCODE -eq 0) {
|
|
119
|
+
$removed += $source
|
|
120
|
+
}
|
|
121
|
+
} finally {
|
|
122
|
+
Pop-Location
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if ($removed.Count -gt 0) {
|
|
126
|
+
Write-Host "Removed the pi-revit Pi package ($($removed -join ', '))." -ForegroundColor Green
|
|
127
|
+
} else {
|
|
128
|
+
Write-Warning "No pi-revit Pi package registration was found. If pi still lists it, run 'pi remove npm:pi-revit' (npm install) or 'pi remove ./' from this repo (install from source)."
|
|
129
|
+
}
|
|
130
|
+
} catch {
|
|
131
|
+
Write-Warning "Could not auto-remove the Pi package; run 'pi remove npm:pi-revit' (npm install) or 'pi remove ./' from this repo (install from source). ($_)"
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
# 5. Workspace (user data) — opt-in only.
|
|
136
|
+
if ($RemoveWorkspace) {
|
|
137
|
+
Remove-IfExists $WorkspaceDir 'workspace'
|
|
138
|
+
} else {
|
|
139
|
+
Write-Host ''
|
|
140
|
+
Write-Host "Workspace preserved (your notes + session history): $WorkspaceDir" -ForegroundColor Yellow
|
|
141
|
+
Write-Host 'Pass -RemoveWorkspace to delete it too.'
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
Write-Host ''
|
|
145
|
+
Write-Host 'pi-revit uninstalled. Pi itself is untouched; remove it yourself if you want:' -ForegroundColor Green
|
|
146
|
+
Write-Host ' npm uninstall -g @earendil-works/pi-coding-agent'
|
package/skills/pi-revit/SKILL.md
CHANGED
|
@@ -29,7 +29,8 @@ Workflow guidance:
|
|
|
29
29
|
- Call `get_model_overview` first when starting work on an unfamiliar model — one call returns project metadata, units, levels, grids, and category counts.
|
|
30
30
|
- `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.
|
|
31
31
|
- The selection pipeline is `get_elements` -> ids -> `manage_selection` (action `set`); there is no inline filter on selection.
|
|
32
|
-
- `set_parameters` is the home for bulk parameter writes AND renames (the `Name` parameter covers levels, views, sheets, types). One transaction per batch; per-element failures are reported.
|
|
32
|
+
- `set_parameters` is the home for bulk parameter writes AND renames (the `Name` parameter covers levels, views, sheets, types). One transaction per batch; per-element failures are reported. Pass `expected_document` (the model title) when several models are open or the session is long — it makes the write fail cleanly instead of landing in a different active document.
|
|
33
|
+
- Parameter display names are LOCALIZED: in a non-English Revit UI, `Mark` is `Kennzeichen` (German), `マーク` (Japanese), etc. 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.
|
|
33
34
|
- 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.
|
|
34
35
|
- `export_documents` files its output under `Documents\pi-revit\Models\<model title>\exports` automatically when `output_dir` is omitted — keyed to the exported document, so it lands right even across many models. Pass `output_dir` only when the user names a different target.
|
|
35
36
|
|
|
@@ -72,6 +72,11 @@ namespace RevitBridge.Tools
|
|
|
72
72
|
type = "string",
|
|
73
73
|
description = "C# script body (top-level statements; using directives allowed at the top). Globals doc/uidoc/uiapp and Dump(value) are in scope. The final expression or a return statement is the result.",
|
|
74
74
|
},
|
|
75
|
+
expected_document = new
|
|
76
|
+
{
|
|
77
|
+
type = "string",
|
|
78
|
+
description = "Optional safety check: title of the document this script is meant for (as reported by get_model_overview). If the active document differs (user switched models), the call fails before running anything.",
|
|
79
|
+
},
|
|
75
80
|
},
|
|
76
81
|
required = new[] { "code" },
|
|
77
82
|
};
|
|
@@ -88,6 +93,7 @@ namespace RevitBridge.Tools
|
|
|
88
93
|
var doc = context.Document ?? throw new NoActiveDocumentException();
|
|
89
94
|
var uiapp = context.UIApplication ?? throw new NoActiveDocumentException();
|
|
90
95
|
var uidoc = uiapp.ActiveUIDocument ?? throw new NoActiveDocumentException();
|
|
96
|
+
DocumentGuard.CheckExpectedDocument(args, doc);
|
|
91
97
|
|
|
92
98
|
string code = JsonArgs.GetString(args, "code") ?? string.Empty;
|
|
93
99
|
if (string.IsNullOrWhiteSpace(code))
|
|
@@ -27,7 +27,7 @@ namespace RevitBridge.Tools
|
|
|
27
27
|
{
|
|
28
28
|
type = "array",
|
|
29
29
|
items = new { type = "string" },
|
|
30
|
-
description = "Only return these parameters (case-insensitive exact names). Default: all.",
|
|
30
|
+
description = "Only return these parameters. Matches the display name OR the BuiltInParameter enum name (case-insensitive exact). Display names are localized (e.g. 'Mark' is 'Kennzeichen' in a German UI) — prefer enum names like ALL_MODEL_MARK for language-independent filtering. Default: all.",
|
|
31
31
|
},
|
|
32
32
|
include = new
|
|
33
33
|
{
|
|
@@ -101,12 +101,13 @@ namespace RevitBridge.Tools
|
|
|
101
101
|
};
|
|
102
102
|
|
|
103
103
|
int parameterCount = 0;
|
|
104
|
+
int parameterTotal = 0;
|
|
104
105
|
if (withParameters)
|
|
105
106
|
{
|
|
106
107
|
var parameters = new List<Dictionary<string, object?>>();
|
|
107
|
-
AppendParameters(doc, element, nameFilter, isType: false, parameters);
|
|
108
|
+
parameterTotal += AppendParameters(doc, element, nameFilter, isType: false, parameters);
|
|
108
109
|
if (withTypeParameters && elementType != null)
|
|
109
|
-
AppendParameters(doc, elementType, nameFilter, isType: true, parameters);
|
|
110
|
+
parameterTotal += AppendParameters(doc, elementType, nameFilter, isType: true, parameters);
|
|
110
111
|
dto["parameters"] = parameters;
|
|
111
112
|
parameterCount = parameters.Count;
|
|
112
113
|
}
|
|
@@ -119,7 +120,12 @@ namespace RevitBridge.Tools
|
|
|
119
120
|
dto["materials"] = DescribeMaterials(doc, element);
|
|
120
121
|
|
|
121
122
|
elements.Add(dto);
|
|
122
|
-
|
|
123
|
+
// With a name filter active, "0 params" is ambiguous (none matched vs none
|
|
124
|
+
// exist): report matched-of-total so a localization miss is visible.
|
|
125
|
+
string paramSummary = !withParameters ? string.Empty
|
|
126
|
+
: nameFilter != null ? $", {parameterCount} of {parameterTotal} params matched parameter_names"
|
|
127
|
+
: $", {parameterCount} params";
|
|
128
|
+
compactParts.Add($"'{element.Name}' (id {id}, {element.Category?.Name ?? "no category"}{paramSummary})");
|
|
123
129
|
}
|
|
124
130
|
|
|
125
131
|
string compact = $"{elements.Count} element(s)"
|
|
@@ -131,18 +137,32 @@ namespace RevitBridge.Tools
|
|
|
131
137
|
return new ToolOutput(new { count = elements.Count, elements, not_found = notFound }, compact);
|
|
132
138
|
}
|
|
133
139
|
|
|
134
|
-
|
|
140
|
+
/// <summary>Appends matching parameter rows and returns how many parameters the
|
|
141
|
+
/// element exposes in total (before the name filter).</summary>
|
|
142
|
+
private static int AppendParameters(Document doc, Element element, HashSet<string>? nameFilter, bool isType, List<Dictionary<string, object?>> target)
|
|
135
143
|
{
|
|
136
144
|
var rows = new List<Dictionary<string, object?>>();
|
|
145
|
+
int total = 0;
|
|
137
146
|
foreach (Parameter parameter in element.Parameters)
|
|
138
147
|
{
|
|
148
|
+
total++;
|
|
139
149
|
string name = parameter.Definition?.Name ?? string.Empty;
|
|
140
150
|
if (nameFilter != null && !nameFilter.Contains(name))
|
|
141
|
-
|
|
151
|
+
{
|
|
152
|
+
// Display names are localized; accept the language-independent
|
|
153
|
+
// BuiltInParameter enum name (e.g. ALL_MODEL_MARK) as an alias.
|
|
154
|
+
string? builtIn = parameter.Definition is InternalDefinition internalDefinition
|
|
155
|
+
&& internalDefinition.BuiltInParameter != BuiltInParameter.INVALID
|
|
156
|
+
? internalDefinition.BuiltInParameter.ToString()
|
|
157
|
+
: null;
|
|
158
|
+
if (builtIn is null || !nameFilter.Contains(builtIn))
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
142
161
|
rows.Add(DescribeParameter(doc, parameter, name, isType));
|
|
143
162
|
}
|
|
144
163
|
rows.Sort((a, b) => string.Compare(a["name"] as string, b["name"] as string, StringComparison.OrdinalIgnoreCase));
|
|
145
164
|
target.AddRange(rows);
|
|
165
|
+
return total;
|
|
146
166
|
}
|
|
147
167
|
|
|
148
168
|
private static Dictionary<string, object?> DescribeParameter(Document doc, Parameter parameter, string name, bool isType)
|
|
@@ -287,6 +287,14 @@ namespace RevitBridge.Tools
|
|
|
287
287
|
break;
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
+
// Display-name promotion is only trustworthy inside one category/class: the
|
|
291
|
+
// probe sees just the first ProbeSize elements in collector order, so in an
|
|
292
|
+
// unscoped query a unanimous sample can still hide other categories further
|
|
293
|
+
// on whose same-named parameter has a different id — and a pinned quick rule
|
|
294
|
+
// would silently drop their matches.
|
|
295
|
+
bool scoped = !string.IsNullOrWhiteSpace(JsonArgs.GetString(args, "category"))
|
|
296
|
+
|| !string.IsNullOrWhiteSpace(JsonArgs.GetString(args, "of_class"));
|
|
297
|
+
|
|
290
298
|
foreach (var rule in rules)
|
|
291
299
|
{
|
|
292
300
|
if (rule.Op is RuleOp.Regex or RuleOp.IsEmpty or RuleOp.IsNotEmpty)
|
|
@@ -296,12 +304,11 @@ namespace RevitBridge.Tools
|
|
|
296
304
|
continue;
|
|
297
305
|
// BuiltInParameter/guid rules address one global parameter id. A plain
|
|
298
306
|
// display name can resolve to DIFFERENT ids per category or family
|
|
299
|
-
// (e.g. 'Width' -> DOOR_WIDTH vs WINDOW_WIDTH)
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
// otherwise the rule stays on the (per-element, correct) post-scan path.
|
|
307
|
+
// (e.g. 'Width' -> DOOR_WIDTH vs WINDOW_WIDTH). Promote a display-name
|
|
308
|
+
// rule only when the query is scoped AND all probed elements agree on
|
|
309
|
+
// the id; otherwise it stays on the (per-element, correct) post-scan path.
|
|
303
310
|
bool oneGlobalId = rule.BuiltIn != null || rule.SharedGuid != null
|
|
304
|
-
|| found.All(parameter => parameter!.Id == found[0]!.Id);
|
|
311
|
+
|| (scoped && found.All(parameter => parameter!.Id == found[0]!.Id));
|
|
305
312
|
if (oneGlobalId)
|
|
306
313
|
rule.QuickRule = TryBuildQuickRule(doc, rule, found[0]!);
|
|
307
314
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
using System.IO;
|
|
2
2
|
using System.Text;
|
|
3
|
+
using System.Text.RegularExpressions;
|
|
3
4
|
using System.Text.Json;
|
|
4
5
|
using System.Xml;
|
|
5
6
|
using System.Xml.Linq;
|
|
@@ -124,7 +125,13 @@ namespace RevitBridge.Tools
|
|
|
124
125
|
|
|
125
126
|
private static (List<ApiMember> Top, int Total) Search(DocIndex index, string query, char? kindFilter, int maxResults)
|
|
126
127
|
{
|
|
128
|
+
// Signatures are rendered exactly one way ("Name(Type, Type)"): normalize the
|
|
129
|
+
// query's spacing around commas and parentheses so 'Wall.Create(Document,Curve'
|
|
130
|
+
// and 'Wall.Create( Document, Curve' hit the same rendered text instead of
|
|
131
|
+
// failing on typography.
|
|
127
132
|
string q = query.ToLowerInvariant();
|
|
133
|
+
q = Regex.Replace(q, @"\s*,\s*", ", ");
|
|
134
|
+
q = Regex.Replace(q, @"\(\s+", "(");
|
|
128
135
|
string[] words = q.Split(WordSeparators, StringSplitOptions.RemoveEmptyEntries);
|
|
129
136
|
|
|
130
137
|
var scored = new List<(ApiMember Member, int Score)>();
|
|
@@ -215,11 +222,12 @@ namespace RevitBridge.Tools
|
|
|
215
222
|
return markdown.ToString();
|
|
216
223
|
}
|
|
217
224
|
|
|
218
|
-
/// <summary>The model
|
|
219
|
-
///
|
|
220
|
-
///
|
|
221
|
-
///
|
|
222
|
-
///
|
|
225
|
+
/// <summary>The model can read details.payload (the cap note above even points it
|
|
226
|
+
/// there), but this markdown is what lands in the strongest-attention position of
|
|
227
|
+
/// the tool result — so the top match carries its remarks, parameter, return, and
|
|
228
|
+
/// exception docs inline (capped for display; the payload keeps full text). Lines
|
|
229
|
+
/// that would blow the markdown budget are dropped individually. When the top match
|
|
230
|
+
/// is one of several same-named overloads, a note says how to target another one.</summary>
|
|
223
231
|
private static void AppendTopMatchDocs(StringBuilder markdown, ApiMember member, int overloadCount)
|
|
224
232
|
{
|
|
225
233
|
var lines = new List<string>(5);
|
|
@@ -44,6 +44,11 @@ namespace RevitBridge.Tools
|
|
|
44
44
|
required = new[] { "element_id", "parameter", "value" },
|
|
45
45
|
},
|
|
46
46
|
},
|
|
47
|
+
expected_document = new
|
|
48
|
+
{
|
|
49
|
+
type = "string",
|
|
50
|
+
description = "Optional safety check: title of the document these writes are meant for (as reported by get_model_overview). If the active document differs (user switched models), the call fails without changing anything.",
|
|
51
|
+
},
|
|
47
52
|
},
|
|
48
53
|
required = new[] { "updates" },
|
|
49
54
|
};
|
|
@@ -58,6 +63,7 @@ namespace RevitBridge.Tools
|
|
|
58
63
|
public object? Execute(JsonElement args, ToolContext context)
|
|
59
64
|
{
|
|
60
65
|
var doc = context.Document ?? throw new NoActiveDocumentException();
|
|
66
|
+
DocumentGuard.CheckExpectedDocument(args, doc);
|
|
61
67
|
var updates = ParseUpdates(args);
|
|
62
68
|
|
|
63
69
|
var succeeded = new List<Dictionary<string, object?>>();
|