pi-revit 0.2.0 → 0.2.2

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.
@@ -3,6 +3,7 @@ import { Type, type TSchema } from "typebox";
3
3
  import { readFile } from "node:fs/promises";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
+ import { version as packageVersion } from "../../package.json";
6
7
 
7
8
  interface BridgeInfo {
8
9
  baseUrl: string;
@@ -195,6 +196,17 @@ function registerBridgeTool(pi: ExtensionAPI, descriptor: BridgeToolDescriptor)
195
196
  });
196
197
  }
197
198
 
199
+ /** `pi update --extensions` refreshes this package but not the deployed Revit add-in,
200
+ * so a newer extension can silently talk to an older bridge. The add-in reports the
201
+ * package version it was built from (stamped by scripts/build.ps1); any difference
202
+ * means the update is incomplete. */
203
+ function versionMismatch(addinVersion: unknown): string | null {
204
+ const addin = typeof addinVersion === "string" && addinVersion.length > 0 ? addinVersion : null;
205
+ if (addin === packageVersion) return null;
206
+ const state = addin ? `still runs version ${addin}` : "predates version reporting";
207
+ return `pi-revit ${packageVersion} is installed, but the Revit add-in ${state} — the update is incomplete. Close Revit and run: npx.cmd -y pi-revit (or ask the agent to run scripts\\deploy.ps1 from the installed package, then restart Revit).`;
208
+ }
209
+
198
210
  function registerPing(pi: ExtensionAPI) {
199
211
  pi.registerTool({
200
212
  name: "ping",
@@ -206,8 +218,9 @@ function registerPing(pi: ExtensionAPI) {
206
218
  executionMode: "sequential",
207
219
  async execute(_toolCallId, _params, signal) {
208
220
  const payload = await bridgeRequest("/ping", { method: "GET" }, signal, 10_000);
221
+ const warning = versionMismatch((payload as { addinVersion?: string }).addinVersion);
209
222
  return {
210
- content: [{ type: "text", text: JSON.stringify(payload) }],
223
+ content: [{ type: "text", text: JSON.stringify(payload) + (warning ? `\nWARNING: ${warning}` : "") }],
211
224
  details: payload,
212
225
  };
213
226
  },
@@ -219,6 +232,19 @@ export default async function revitConnector(pi: ExtensionAPI) {
219
232
  // bridge is down, so it is never part of /tools discovery.
220
233
  registerPing(pi);
221
234
 
235
+ // Surface an incomplete update (see versionMismatch) once per session, right
236
+ // where the user lands after running `pi update --extensions`. Bridge down at
237
+ // session start is the normal Revit-closed case: stay quiet.
238
+ pi.on("session_start", async (_event, ctx) => {
239
+ try {
240
+ const payload = (await bridgeRequest("/ping", { method: "GET" }, undefined, 3_000)) as { addinVersion?: string };
241
+ const warning = versionMismatch(payload.addinVersion);
242
+ if (warning) ctx.ui.notify(warning, "warning");
243
+ } catch {
244
+ // No bridge, no verdict.
245
+ }
246
+ });
247
+
222
248
  let descriptors: BridgeToolDescriptor[];
223
249
  try {
224
250
  const payload = (await bridgeRequest("/tools", { method: "GET" }, undefined, DISCOVERY_TIMEOUT_MS)) as {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-revit",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
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",
package/scripts/build.ps1 CHANGED
@@ -20,6 +20,17 @@ $ErrorActionPreference = 'Stop'
20
20
  $project = Join-Path $PSScriptRoot '..\src\Revit\RevitBridge.csproj'
21
21
 
22
22
  $buildArgs = @('build', $project, '-c', $Configuration)
23
+
24
+ # Stamp the package version into the assembly so the bridge can report which release
25
+ # the deployed add-in came from; the pi extension compares it against its own package
26
+ # version at session start to detect partial updates.
27
+ $packageJson = Join-Path $PSScriptRoot '..\package.json'
28
+ if (Test-Path $packageJson) {
29
+ $packageVersion = (Get-Content $packageJson -Raw | ConvertFrom-Json).version
30
+ if ($packageVersion) {
31
+ $buildArgs += "-p:Version=$packageVersion"
32
+ }
33
+ }
23
34
  if ($TargetFramework) {
24
35
  # Override TargetFrameworks (plural) rather than TargetFramework: NuGet restore ignores a
25
36
  # single -p:TargetFramework and still restores every framework the project declares, so a
@@ -1,6 +1,7 @@
1
1
  using System.IO;
2
2
  using System.Net;
3
3
  using System.Net.Sockets;
4
+ using System.Reflection;
4
5
  using System.Text;
5
6
  using System.Text.Json;
6
7
 
@@ -22,6 +23,15 @@ namespace RevitBridge
22
23
  {
23
24
  private const int DefaultPort = 47777;
24
25
  private const int MaxPort = 47797;
26
+
27
+ /// <summary>Package version stamped into the assembly by scripts/build.ps1. The pi
28
+ /// extension compares this against its own package version to detect partial
29
+ /// updates (npm package updated, add-in not redeployed); a build made without
30
+ /// the stamp reports its default assembly version instead.</summary>
31
+ private static readonly string AddinVersion =
32
+ typeof(BridgeServer).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion?.Split('+')[0]
33
+ ?? typeof(BridgeServer).Assembly.GetName().Version?.ToString(3)
34
+ ?? "unknown";
25
35
  private const int MaxRequestBytes = 4_000_000;
26
36
  private const int MaxContentChars = 12_000;
27
37
 
@@ -260,7 +270,7 @@ namespace RevitBridge
260
270
  private async Task<(int Status, object Response)> RouteAsync(string method, string path, Dictionary<string, string> query, string body, CancellationToken token)
261
271
  {
262
272
  if (method == "GET" && path == "/ping")
263
- return (200, new { ok = true, service = "revit-bridge", revitVersion = _revitVersion, pid = Environment.ProcessId });
273
+ return (200, new { ok = true, service = "revit-bridge", revitVersion = _revitVersion, pid = Environment.ProcessId, addinVersion = AddinVersion });
264
274
 
265
275
  if (method == "GET" && path == "/tools")
266
276
  {
@@ -21,16 +21,19 @@ namespace RevitBridge.Tools
21
21
  {
22
22
  private const int DefaultMaxResults = 10;
23
23
  private const int MaxResultsCap = 50;
24
+
25
+ // The Max*Chars caps apply to the model-visible markdown only; the index and
26
+ // the details payload keep the full documentation text.
24
27
  private const int MaxMarkdownChars = 6_000;
25
- private const int MaxSummaryChars = 600;
26
28
  private const int MaxRemarksChars = 600;
27
29
  private const int MaxReturnsChars = 300;
28
30
  private const int MaxParamDocChars = 240;
29
31
  private const int MaxParamsPerMember = 10;
32
+ private const int MaxExceptionsPerMember = 8;
30
33
 
31
34
  public string Name => "search_api_docs";
32
35
  public string Label => "Search API Docs";
33
- public string Description => "Search the offline Revit API documentation (the RevitAPI.xml and RevitAPIUI.xml files shipped with Revit) for types, methods, constructors, properties, fields, and events; every public API enum is fully searchable by value name (values the XML leaves undocumented are synthesized from the API assemblies). query is a single name or substring — e.g. 'FilteredElementCollector', 'Wall.Create', 'WALL_BASE_OFFSET' — ranked: exact name first, then prefix, then substring; dotted Type.Member queries match composites. Returns signatures with summary, remarks, parameter docs, return docs, and the Revit version a member was introduced in ('since'). Works with no document open. Use it to verify exact classes, members, and signatures before writing execute_csharp code. The first query builds the index (a few seconds); later queries are instant.";
36
+ public string Description => "Search the offline Revit API documentation (the RevitAPI.xml and RevitAPIUI.xml files shipped with Revit) for types, methods, constructors, properties, fields, and events; every public API enum is fully searchable by value name (values the XML leaves undocumented are synthesized from the API assemblies). query is a single name or substring — e.g. 'FilteredElementCollector', 'Wall.Create', 'WALL_BASE_OFFSET' — ranked: exact name first, then prefix, then substring; dotted Type.Member queries match composites, and same-named overloads rank simplest-first. To target one overload, continue the query past a parenthesis with parameter types, e.g. 'Wall.Create(Document, Curve'. Returns signatures with summary, remarks, parameter docs, return docs, exception docs, and the Revit version a member was introduced in ('since'); the top match shows its full docs inline. Works with no document open. Use it to verify exact classes, members, and signatures before writing execute_csharp code. The first query builds the index (a few seconds); later queries are instant.";
34
37
 
35
38
  public bool RequiresDocument => false;
36
39
 
@@ -97,6 +100,9 @@ namespace RevitBridge.Tools
97
100
  .Select(pair => new Dictionary<string, object?> { ["name"] = pair.Key, ["description"] = pair.Value })
98
101
  .ToList(),
99
102
  ["returns"] = member.Returns,
103
+ ["exceptions"] = member.Exceptions?
104
+ .Select(pair => new Dictionary<string, object?> { ["type"] = pair.Key, ["description"] = pair.Value })
105
+ .ToList(),
100
106
  });
101
107
  }
102
108
 
@@ -134,6 +140,7 @@ namespace RevitBridge.Tools
134
140
  var top = scored
135
141
  .OrderByDescending(entry => entry.Score)
136
142
  .ThenBy(entry => entry.Member.Composite.Length)
143
+ .ThenBy(entry => entry.Member.ParameterCount)
137
144
  .ThenBy(entry => entry.Member.FullName, StringComparer.Ordinal)
138
145
  .Take(maxResults)
139
146
  .Select(entry => entry.Member)
@@ -143,8 +150,15 @@ namespace RevitBridge.Tools
143
150
 
144
151
  private static int Score(ApiMember member, string q, string[] words)
145
152
  {
153
+ // A query with a parenthesis targets a specific overload by signature,
154
+ // e.g. 'wall.create(document, curve' — matched against the shortened
155
+ // signature text before any name-based ranking.
156
+ bool signatureQuery = q.Contains('(');
157
+
146
158
  int score;
147
159
  if (member.CompositeLower == q) score = 1000;
160
+ else if (signatureQuery && member.SignatureLower == q) score = 980;
161
+ else if (signatureQuery && member.SignatureLower.StartsWith(q, StringComparison.Ordinal)) score = 950;
148
162
  else if (member.ShortNameLower == q) score = 900;
149
163
  else if (member.CompositeLower.StartsWith(q, StringComparison.Ordinal)) score = 700;
150
164
  else if (member.ShortNameLower.StartsWith(q, StringComparison.Ordinal)) score = 650;
@@ -181,7 +195,7 @@ namespace RevitBridge.Tools
181
195
  }
182
196
  markdown.Append(line);
183
197
  if (i == 0)
184
- AppendTopMatchDocs(markdown, member);
198
+ AppendTopMatchDocs(markdown, member, top.Count(other => other.CompositeLower == member.CompositeLower));
185
199
  }
186
200
  }
187
201
  foreach (string warning in index.Warnings)
@@ -190,18 +204,23 @@ namespace RevitBridge.Tools
190
204
  }
191
205
 
192
206
  /// <summary>The model sees only this markdown — details.payload never reaches it —
193
- /// so the top match carries its remarks, parameter, and return docs inline. Lines
194
- /// that would blow the markdown budget are dropped individually; narrowing the
195
- /// query promotes any other match to the top slot with its docs.</summary>
196
- private static void AppendTopMatchDocs(StringBuilder markdown, ApiMember member)
207
+ /// so the top match carries its remarks, parameter, return, and exception docs
208
+ /// inline (capped for display; the payload keeps full text). Lines that would blow
209
+ /// the markdown budget are dropped individually. When the top match is one of
210
+ /// several same-named overloads, a note says how to target another one.</summary>
211
+ private static void AppendTopMatchDocs(StringBuilder markdown, ApiMember member, int overloadCount)
197
212
  {
198
- var lines = new List<string>(3);
213
+ var lines = new List<string>(5);
199
214
  if (member.Remarks is { } remarks)
200
- lines.Add($"\n remarks: {remarks}");
215
+ lines.Add($"\n remarks: {Cap(remarks, MaxRemarksChars)}");
201
216
  if (member.Parameters is { Count: > 0 } parameters)
202
- lines.Add($"\n params: {string.Join("; ", parameters.Select(pair => $"{pair.Key} — {pair.Value}"))}");
217
+ lines.Add($"\n params: {string.Join("; ", parameters.Select(pair => $"{pair.Key} — {Cap(pair.Value, MaxParamDocChars)}"))}");
203
218
  if (member.Returns is { } returns)
204
- lines.Add($"\n returns: {returns}");
219
+ lines.Add($"\n returns: {Cap(returns, MaxReturnsChars)}");
220
+ if (member.Exceptions is { Count: > 0 } exceptions)
221
+ lines.Add($"\n throws: {string.Join("; ", exceptions.Select(pair => $"{pair.Key} — {Cap(pair.Value, MaxParamDocChars)}"))}");
222
+ if (overloadCount > 1)
223
+ lines.Add($"\n note: {member.Composite} has {overloadCount} overload(s) listed; full docs shown for the simplest top-ranked one. To target another, continue the query past a parenthesis with parameter types, e.g. '{member.Composite}(Document, '.");
205
224
  foreach (string line in lines)
206
225
  {
207
226
  if (markdown.Length + line.Length > MaxMarkdownChars)
@@ -262,11 +281,18 @@ namespace RevitBridge.Tools
262
281
  public required string FullNameLower { get; init; }
263
282
  public required string CompositeLower { get; init; }
264
283
  public required string ShortNameLower { get; init; }
284
+ public required string SignatureLower { get; init; }
285
+
286
+ /// <summary>Top-level parameter count from the doc id; overload tie-breaks
287
+ /// rank the simplest overload first.</summary>
288
+ public required int ParameterCount { get; init; }
289
+
265
290
  public string? Summary { get; init; }
266
291
  public string? Remarks { get; init; }
267
292
  public string? Returns { get; init; }
268
293
  public string? Since { get; init; }
269
294
  public IReadOnlyList<KeyValuePair<string, string>>? Parameters { get; init; }
295
+ public IReadOnlyList<KeyValuePair<string, string>>? Exceptions { get; init; }
270
296
  }
271
297
 
272
298
  private sealed class DocIndex
@@ -364,6 +390,8 @@ namespace RevitBridge.Tools
364
390
  FullNameLower = fullName.ToLowerInvariant(),
365
391
  CompositeLower = composite.ToLowerInvariant(),
366
392
  ShortNameLower = name.ToLowerInvariant(),
393
+ SignatureLower = composite.ToLowerInvariant(),
394
+ ParameterCount = 0,
367
395
  Summary = enumType == typeof(BuiltInCategory)
368
396
  ? "BuiltInCategory enum value — usable as the 'category' argument of get_elements/get_element_types and with FilteredElementCollector.OfCategory in execute_csharp."
369
397
  : $"{typeName} enum value (not documented in the XML; synthesized from {assemblyName} metadata).",
@@ -481,12 +509,24 @@ namespace RevitBridge.Tools
481
509
  if (parameters is { Count: >= MaxParamsPerMember })
482
510
  break;
483
511
  string? name = param.Attribute("name")?.Value;
484
- string? text = Cap(CleanDocText(param), MaxParamDocChars);
512
+ string? text = CleanDocText(param);
485
513
  if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(text))
486
514
  continue;
487
515
  (parameters ??= new List<KeyValuePair<string, string>>()).Add(new KeyValuePair<string, string>(name, text));
488
516
  }
489
517
 
518
+ List<KeyValuePair<string, string>>? exceptions = null;
519
+ foreach (var exception in element.Elements("exception"))
520
+ {
521
+ if (exceptions is { Count: >= MaxExceptionsPerMember })
522
+ break;
523
+ string? cref = exception.Attribute("cref")?.Value;
524
+ string? text = CleanDocText(exception);
525
+ if (string.IsNullOrEmpty(cref) || string.IsNullOrEmpty(text))
526
+ continue;
527
+ (exceptions ??= new List<KeyValuePair<string, string>>()).Add(new KeyValuePair<string, string>(ShortCref(cref), text));
528
+ }
529
+
490
530
  return new ApiMember
491
531
  {
492
532
  Kind = kind,
@@ -498,14 +538,34 @@ namespace RevitBridge.Tools
498
538
  FullNameLower = path.ToLowerInvariant(),
499
539
  CompositeLower = composite.ToLowerInvariant(),
500
540
  ShortNameLower = shortName.ToLowerInvariant(),
501
- Summary = Cap(CleanDocText(element.Element("summary")), MaxSummaryChars),
502
- Remarks = Cap(CleanDocText(element.Element("remarks")), MaxRemarksChars),
503
- Returns = Cap(CleanDocText(element.Element("returns")), MaxReturnsChars),
541
+ SignatureLower = signature.ToLowerInvariant(),
542
+ ParameterCount = CountTopLevelParameters(paramText),
543
+ Summary = CleanDocText(element.Element("summary")),
544
+ Remarks = CleanDocText(element.Element("remarks")),
545
+ Returns = CleanDocText(element.Element("returns")),
504
546
  Since = CleanDocText(element.Element("since")),
505
547
  Parameters = parameters,
548
+ Exceptions = exceptions,
506
549
  };
507
550
  }
508
551
 
552
+ /// <summary>Parameter count of a doc-id parameter list; commas inside generic
553
+ /// braces (Dictionary{K,V}) do not separate parameters.</summary>
554
+ private static int CountTopLevelParameters(string? paramText)
555
+ {
556
+ if (string.IsNullOrEmpty(paramText))
557
+ return 0;
558
+ int count = 1;
559
+ int depth = 0;
560
+ foreach (char c in paramText)
561
+ {
562
+ if (c == '{') depth++;
563
+ else if (c == '}') depth--;
564
+ else if (c == ',' && depth == 0) count++;
565
+ }
566
+ return count;
567
+ }
568
+
509
569
  // ------------------------------------------------------------- text utils
510
570
 
511
571
  /// <summary>Flattens doc XML to plain text: see/seealso cref -> short type name,