pi-revit 0.4.0 → 0.5.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.
Files changed (119) hide show
  1. package/AGENTS.md +167 -0
  2. package/CHANGELOG.md +44 -0
  3. package/README.md +644 -553
  4. package/bin/pi-revit.js +9 -9
  5. package/docs/architecture.md +271 -0
  6. package/docs/evaluation.md +434 -0
  7. package/docs/invariants.json +147 -0
  8. package/extensions/pi-revit/completion-monitor.ts +55 -0
  9. package/extensions/pi-revit/contracts.ts +146 -0
  10. package/extensions/pi-revit/discovery.ts +93 -0
  11. package/extensions/pi-revit/index.ts +342 -255
  12. package/extensions/pi-revit/instance-router.ts +86 -86
  13. package/extensions/pi-revit/platform-prompt.ts +40 -0
  14. package/extensions/pi-revit/scope-monitor.ts +114 -0
  15. package/extensions/pi-revit/script-library.ts +144 -144
  16. package/extensions/pi-revit/tool-catalog.ts +113 -14
  17. package/extensions/pi-revit/tool-documentation.ts +72 -0
  18. package/extensions/pi-revit/tool-schema.ts +8 -0
  19. package/package.json +8 -2
  20. package/scripts/build.ps1 +9 -9
  21. package/scripts/check-sdk.ps1 +66 -66
  22. package/scripts/check-tool-documentation.mjs +287 -0
  23. package/scripts/deploy.ps1 +16 -16
  24. package/scripts/generate-contracts.mjs +80 -0
  25. package/scripts/lib/platform.mjs +226 -0
  26. package/scripts/test-extension.mjs +15 -0
  27. package/skills/pi-revit/SKILL.md +30 -218
  28. package/skills/pi-revit/contracts.generated.json +3524 -0
  29. package/skills/pi-revit/references/execution-rules.md +41 -0
  30. package/skills/pi-revit/references/model-audit-export.md +38 -27
  31. package/skills/pi-revit/references/operation-recovery.md +33 -0
  32. package/skills/pi-revit/references/room-documentation.md +37 -26
  33. package/skills/pi-revit/references/tool-index.md +89 -0
  34. package/skills/pi-revit/references/tools/capture_view.md +62 -0
  35. package/skills/pi-revit/references/tools/change_element_types.md +65 -0
  36. package/skills/pi-revit/references/tools/create_tags.md +85 -0
  37. package/skills/pi-revit/references/tools/delete_elements.md +66 -0
  38. package/skills/pi-revit/references/tools/execute_csharp.md +81 -0
  39. package/skills/pi-revit/references/tools/export_documents.md +75 -0
  40. package/skills/pi-revit/references/tools/find_revit_tools.md +96 -0
  41. package/skills/pi-revit/references/tools/get_element_details.md +66 -0
  42. package/skills/pi-revit/references/tools/get_element_relationships.md +61 -0
  43. package/skills/pi-revit/references/tools/get_element_types.md +67 -0
  44. package/skills/pi-revit/references/tools/get_elements.md +87 -0
  45. package/skills/pi-revit/references/tools/get_linked_elements.md +79 -0
  46. package/skills/pi-revit/references/tools/get_linked_models.md +57 -0
  47. package/skills/pi-revit/references/tools/get_model_coordinates.md +64 -0
  48. package/skills/pi-revit/references/tools/get_model_health.md +53 -0
  49. package/skills/pi-revit/references/tools/get_model_overview.md +57 -0
  50. package/skills/pi-revit/references/tools/get_revit_operation.md +54 -0
  51. package/skills/pi-revit/references/tools/get_schedule_fields.md +62 -0
  52. package/skills/pi-revit/references/tools/get_schedules.md +71 -0
  53. package/skills/pi-revit/references/tools/manage_element_sets.md +92 -0
  54. package/skills/pi-revit/references/tools/manage_revit_instances.md +63 -0
  55. package/skills/pi-revit/references/tools/manage_revit_scripts.md +109 -0
  56. package/skills/pi-revit/references/tools/manage_schedules.md +90 -0
  57. package/skills/pi-revit/references/tools/manage_selection.md +66 -0
  58. package/skills/pi-revit/references/tools/manage_sheet_placements.md +82 -0
  59. package/skills/pi-revit/references/tools/manage_sheets.md +71 -0
  60. package/skills/pi-revit/references/tools/manage_views.md +95 -0
  61. package/skills/pi-revit/references/tools/measure_geometry.md +71 -0
  62. package/skills/pi-revit/references/tools/open_view.md +59 -0
  63. package/skills/pi-revit/references/tools/ping.md +41 -0
  64. package/skills/pi-revit/references/tools/query_spatial_elements.md +74 -0
  65. package/skills/pi-revit/references/tools/read_revit_result.md +53 -0
  66. package/skills/pi-revit/references/tools/search_api_docs.md +65 -0
  67. package/skills/pi-revit/references/tools/set_parameters.md +75 -0
  68. package/skills/pi-revit/references/tools/summarize_elements.md +64 -0
  69. package/skills/pi-revit/references/tools/transform_elements.md +79 -0
  70. package/skills/pi-revit/references/visual-verification.md +36 -0
  71. package/skills/pi-revit/tool-manifest.json +338 -0
  72. package/src/Revit/BridgeServer.cs +93 -87
  73. package/src/Revit/OperationStore.cs +178 -178
  74. package/src/Revit/ToolRegistry.cs +88 -57
  75. package/src/Revit/Tools/CaptureView.cs +10 -2
  76. package/src/Revit/Tools/ChangeElementTypes.cs +74 -60
  77. package/src/Revit/Tools/ChangeSet.cs +39 -0
  78. package/src/Revit/Tools/CreateTags.cs +107 -95
  79. package/src/Revit/Tools/DeleteElements.cs +53 -44
  80. package/src/Revit/Tools/DocumentGuard.cs +74 -64
  81. package/src/Revit/Tools/ElementNames.cs +103 -0
  82. package/src/Revit/Tools/ElementQueryScope.cs +27 -27
  83. package/src/Revit/Tools/ElementTraits.cs +53 -0
  84. package/src/Revit/Tools/ExecuteCsharp.cs +54 -45
  85. package/src/Revit/Tools/ExportDocuments.cs +129 -121
  86. package/src/Revit/Tools/GetElementDetails.cs +37 -41
  87. package/src/Revit/Tools/GetElementRelationships.cs +82 -76
  88. package/src/Revit/Tools/GetElementTypes.cs +8 -0
  89. package/src/Revit/Tools/GetElements.cs +75 -86
  90. package/src/Revit/Tools/GetLinkedElements.cs +89 -82
  91. package/src/Revit/Tools/GetLinkedModels.cs +73 -66
  92. package/src/Revit/Tools/GetModelCoordinates.cs +56 -49
  93. package/src/Revit/Tools/GetModelHealth.cs +7 -0
  94. package/src/Revit/Tools/GetModelOverview.cs +185 -158
  95. package/src/Revit/Tools/GetScheduleFields.cs +44 -37
  96. package/src/Revit/Tools/GetSchedules.cs +96 -89
  97. package/src/Revit/Tools/InheritedState.Summary.cs +57 -0
  98. package/src/Revit/Tools/InheritedState.cs +144 -0
  99. package/src/Revit/Tools/ManageElementSets.cs +114 -106
  100. package/src/Revit/Tools/ManageSchedules.cs +174 -164
  101. package/src/Revit/Tools/ManageSelection.cs +45 -37
  102. package/src/Revit/Tools/ManageSheetPlacements.cs +113 -97
  103. package/src/Revit/Tools/ManageSheets.cs +72 -63
  104. package/src/Revit/Tools/ManageViews.cs +115 -100
  105. package/src/Revit/Tools/MeasureGeometry.cs +60 -54
  106. package/src/Revit/Tools/ModelChanges.cs +154 -0
  107. package/src/Revit/Tools/ModelEditBatch.cs +105 -102
  108. package/src/Revit/Tools/ModelEditInputs.cs +49 -49
  109. package/src/Revit/Tools/OpenView.cs +9 -2
  110. package/src/Revit/Tools/ParameterResolver.cs +94 -0
  111. package/src/Revit/Tools/QuerySpatialElements.cs +70 -63
  112. package/src/Revit/Tools/SearchApiDocs.cs +72 -4
  113. package/src/Revit/Tools/SetParameters.cs +60 -79
  114. package/src/Revit/Tools/SpatialBounds.cs +30 -30
  115. package/src/Revit/Tools/SummarizeElements.cs +94 -87
  116. package/src/Revit/Tools/ToolContract.cs +48 -0
  117. package/src/Revit/Tools/ToolSupport.cs +5 -1
  118. package/src/Revit/Tools/TransformElements.cs +72 -57
  119. package/workspace/AGENTS.md +26 -20
@@ -1,146 +1,146 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import { Type } from "typebox";
3
- import { mkdir, readdir, readFile, writeFile, rename } from "node:fs/promises";
4
- import { createHash, randomUUID } from "node:crypto";
5
- import path from "node:path";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { Type } from "typebox";
3
+ import { mkdir, readdir, readFile, writeFile, rename } from "node:fs/promises";
4
+ import { createHash, randomUUID } from "node:crypto";
5
+ import path from "node:path";
6
6
  import os from "node:os";
7
-
8
- type InputKind = "string" | "number" | "integer" | "boolean" | "object" | "array";
9
- interface Definition { name: string; description: string; code: string; input_types: Record<string, InputKind>; }
10
- interface Version extends Definition { version: string; created_at: string; }
11
- interface Receipt { operation_id: string; bridge_id: string; }
12
- type Result = { content: { type: "text"; text: string }[]; details?: unknown };
13
- type Runner = (args: Record<string, unknown>, signal: AbortSignal | undefined, prepared: (receipt: Receipt) => Promise<void>) => Promise<Result>;
14
- const kinds = ["string", "number", "integer", "boolean", "object", "array"] as const;
15
- const object = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === "object" && !Array.isArray(value);
16
- const hash = (value: unknown) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
17
- const scriptName = (name: unknown) => {
18
- if (typeof name !== "string" || !/^[a-z][a-z0-9_-]{0,63}$/.test(name)) throw new Error("name must be 1–64 lowercase letters, digits, underscores or hyphens, starting with a letter.");
19
- return name;
20
- };
21
- const versionId = (version: unknown) => {
22
- if (typeof version !== "string" || !/^[a-f0-9]{64}$/.test(version)) throw new Error("version must be the exact saved 64-character hash.");
23
- return version;
24
- };
25
-
26
- export function createScriptLibrary(root: string) {
27
- const versions = path.join(root, "versions"), history = path.join(root, "history");
28
- const definition = (value: Record<string, unknown>): Definition => {
29
- const name = scriptName(value.name);
30
- if (typeof value.code !== "string" || !value.code.trim() || value.code.length > 100000) throw new Error("code must contain 1–100,000 characters.");
31
- if (typeof value.description !== "string" || value.description.length > 2000) throw new Error("description must be a string of at most 2,000 characters.");
32
- if (!object(value.input_types) || Object.keys(value.input_types).length > 40) throw new Error("input_types must declare at most 40 named inputs.");
33
- const input_types: Record<string, InputKind> = Object.create(null);
34
- for (const key of Object.keys(value.input_types).sort()) {
35
- if (!/^[A-Za-z][A-Za-z0-9_]{0,63}$/.test(key) || !kinds.includes(value.input_types[key] as InputKind)) throw new Error(`Invalid input declaration: ${key}.`);
36
- input_types[key] = value.input_types[key] as InputKind;
37
- }
38
- return { name, description: value.description, code: value.code, input_types };
39
- };
40
- async function files(directory: string) {
41
- try { return (await readdir(directory)).filter(file => file.endsWith(".json")); }
42
- catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return []; throw error; }
43
- }
44
- async function read(name: unknown, version: unknown): Promise<Version> {
45
- const value = JSON.parse(await readFile(path.join(versions, `${scriptName(name)}--${versionId(version)}.json`), "utf8"));
46
- const content = definition(value);
47
- if (content.name !== name || hash(content) !== version || value.version !== version) throw new Error("Saved script integrity check failed; no code was executed.");
48
- return { ...content, version: value.version, created_at: value.created_at };
49
- }
50
- async function save(args: Record<string, unknown>) {
51
- const content = definition(args), version = hash(content);
52
- await mkdir(versions, { recursive: true });
53
- const file = path.join(versions, `${content.name}--${version}.json`);
54
- const value = { ...content, version, created_at: new Date().toISOString() };
55
- try { await writeFile(file, JSON.stringify(value, null, 2), { encoding: "utf8", flag: "wx" }); }
56
- catch (error) { if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error; }
57
- const saved = await read(content.name, version);
58
- return { name: saved.name, version, created_at: saved.created_at, file_path: file, executed: false };
59
- }
60
- function validateInputs(saved: Version, input: unknown) {
61
- if (!object(input)) throw new Error("inputs must be a JSON object.");
62
- if (JSON.stringify(input).length > 100000) throw new Error("inputs exceed 100,000 JSON characters.");
63
- for (const name of Object.keys(input)) if (!Object.hasOwn(saved.input_types, name)) throw new Error(`Undeclared input: ${name}.`);
64
- for (const [name, kind] of Object.entries(saved.input_types)) {
65
- const value = input[name];
66
- const valid = Object.hasOwn(input, name) && (kind === "array" ? Array.isArray(value) : kind === "object" ? object(value)
67
- : kind === "integer" ? Number.isSafeInteger(value) : kind === "number" ? typeof value === "number" && Number.isFinite(value) : typeof value === kind);
68
- if (!valid) throw new Error(`Input '${name}' is required and must be ${kind}.`);
69
- }
70
- return input;
71
- }
72
- async function list(name: unknown, offset: number, limit: number) {
73
- const filter = name === undefined ? undefined : scriptName(name);
74
- const names = (await files(versions)).filter(file => /^[a-z][a-z0-9_-]{0,63}--[a-f0-9]{64}\.json$/.test(file) && (!filter || file.slice(0, file.lastIndexOf("--")) === filter)).sort();
75
- const entries = [];
76
- for (const file of names.slice(offset, offset + limit)) {
77
- const split = file.lastIndexOf("--");
78
- const value = await read(file.slice(0, split), file.slice(split + 2, -5));
79
- entries.push({ name: value.name, version: value.version, description: value.description, input_types: value.input_types, created_at: value.created_at });
80
- }
81
- return { total_count: names.length, offset, entries, next_offset: offset + entries.length < names.length ? offset + entries.length : null };
82
- }
83
- async function writeHistory(record: Record<string, unknown>) {
84
- await mkdir(history, { recursive: true });
85
- const file = path.join(history, `${record.run_id}.json`), temp = `${file}.${randomUUID()}.tmp`;
86
- await writeFile(temp, JSON.stringify(record, null, 2), { encoding: "utf8", flag: "wx" });
87
- await rename(temp, file);
88
- }
89
- async function readHistory(name: unknown, offset: number, limit: number) {
90
- const filter = name === undefined ? undefined : scriptName(name);
91
- const entries = [];
92
- for (const file of await files(history)) {
93
- if (!/^[a-f0-9-]{36}\.json$/.test(file)) continue;
94
- const value = JSON.parse(await readFile(path.join(history, file), "utf8"));
95
- if (!filter || value.name === filter) entries.push(value);
96
- }
97
- entries.sort((a, b) => b.started_at.localeCompare(a.started_at) || a.run_id.localeCompare(b.run_id));
98
- return { total_count: entries.length, offset, entries: entries.slice(offset, offset + limit), next_offset: offset + limit < entries.length ? offset + limit : null };
99
- }
100
- return { root, read, save, list, validateInputs, writeHistory, readHistory };
101
- }
102
-
103
- export function registerScriptLibrary(pi: ExtensionAPI, run: Runner, render: (value: unknown) => Promise<Result>) {
104
- const library = createScriptLibrary(path.join(process.env.APPDATA ?? path.join(os.homedir(), "AppData", "Roaming"), "pi-revit", "scripts"));
105
- pi.registerTool({
7
+
8
+ type InputKind = "string" | "number" | "integer" | "boolean" | "object" | "array";
9
+ interface Definition { name: string; description: string; code: string; input_types: Record<string, InputKind>; }
10
+ interface Version extends Definition { version: string; created_at: string; }
11
+ interface Receipt { operation_id: string; bridge_id: string; }
12
+ type Result = { content: { type: "text"; text: string }[]; details?: unknown };
13
+ type Runner = (args: Record<string, unknown>, signal: AbortSignal | undefined, prepared: (receipt: Receipt) => Promise<void>) => Promise<Result>;
14
+ const kinds = ["string", "number", "integer", "boolean", "object", "array"] as const;
15
+ const object = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === "object" && !Array.isArray(value);
16
+ const hash = (value: unknown) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
17
+ const scriptName = (name: unknown) => {
18
+ if (typeof name !== "string" || !/^[a-z][a-z0-9_-]{0,63}$/.test(name)) throw new Error("name must be 1–64 lowercase letters, digits, underscores or hyphens, starting with a letter.");
19
+ return name;
20
+ };
21
+ const versionId = (version: unknown) => {
22
+ if (typeof version !== "string" || !/^[a-f0-9]{64}$/.test(version)) throw new Error("version must be the exact saved 64-character hash.");
23
+ return version;
24
+ };
25
+
26
+ export function createScriptLibrary(root: string) {
27
+ const versions = path.join(root, "versions"), history = path.join(root, "history");
28
+ const definition = (value: Record<string, unknown>): Definition => {
29
+ const name = scriptName(value.name);
30
+ if (typeof value.code !== "string" || !value.code.trim() || value.code.length > 100000) throw new Error("code must contain 1–100,000 characters.");
31
+ if (typeof value.description !== "string" || value.description.length > 2000) throw new Error("description must be a string of at most 2,000 characters.");
32
+ if (!object(value.input_types) || Object.keys(value.input_types).length > 40) throw new Error("input_types must declare at most 40 named inputs.");
33
+ const input_types: Record<string, InputKind> = Object.create(null);
34
+ for (const key of Object.keys(value.input_types).sort()) {
35
+ if (!/^[A-Za-z][A-Za-z0-9_]{0,63}$/.test(key) || !kinds.includes(value.input_types[key] as InputKind)) throw new Error(`Invalid input declaration: ${key}.`);
36
+ input_types[key] = value.input_types[key] as InputKind;
37
+ }
38
+ return { name, description: value.description, code: value.code, input_types };
39
+ };
40
+ async function files(directory: string) {
41
+ try { return (await readdir(directory)).filter(file => file.endsWith(".json")); }
42
+ catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return []; throw error; }
43
+ }
44
+ async function read(name: unknown, version: unknown): Promise<Version> {
45
+ const value = JSON.parse(await readFile(path.join(versions, `${scriptName(name)}--${versionId(version)}.json`), "utf8"));
46
+ const content = definition(value);
47
+ if (content.name !== name || hash(content) !== version || value.version !== version) throw new Error("Saved script integrity check failed; no code was executed.");
48
+ return { ...content, version: value.version, created_at: value.created_at };
49
+ }
50
+ async function save(args: Record<string, unknown>) {
51
+ const content = definition(args), version = hash(content);
52
+ await mkdir(versions, { recursive: true });
53
+ const file = path.join(versions, `${content.name}--${version}.json`);
54
+ const value = { ...content, version, created_at: new Date().toISOString() };
55
+ try { await writeFile(file, JSON.stringify(value, null, 2), { encoding: "utf8", flag: "wx" }); }
56
+ catch (error) { if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error; }
57
+ const saved = await read(content.name, version);
58
+ return { name: saved.name, version, created_at: saved.created_at, file_path: file, executed: false };
59
+ }
60
+ function validateInputs(saved: Version, input: unknown) {
61
+ if (!object(input)) throw new Error("inputs must be a JSON object.");
62
+ if (JSON.stringify(input).length > 100000) throw new Error("inputs exceed 100,000 JSON characters.");
63
+ for (const name of Object.keys(input)) if (!Object.hasOwn(saved.input_types, name)) throw new Error(`Undeclared input: ${name}.`);
64
+ for (const [name, kind] of Object.entries(saved.input_types)) {
65
+ const value = input[name];
66
+ const valid = Object.hasOwn(input, name) && (kind === "array" ? Array.isArray(value) : kind === "object" ? object(value)
67
+ : kind === "integer" ? Number.isSafeInteger(value) : kind === "number" ? typeof value === "number" && Number.isFinite(value) : typeof value === kind);
68
+ if (!valid) throw new Error(`Input '${name}' is required and must be ${kind}.`);
69
+ }
70
+ return input;
71
+ }
72
+ async function list(name: unknown, offset: number, limit: number) {
73
+ const filter = name === undefined ? undefined : scriptName(name);
74
+ const names = (await files(versions)).filter(file => /^[a-z][a-z0-9_-]{0,63}--[a-f0-9]{64}\.json$/.test(file) && (!filter || file.slice(0, file.lastIndexOf("--")) === filter)).sort();
75
+ const entries = [];
76
+ for (const file of names.slice(offset, offset + limit)) {
77
+ const split = file.lastIndexOf("--");
78
+ const value = await read(file.slice(0, split), file.slice(split + 2, -5));
79
+ entries.push({ name: value.name, version: value.version, description: value.description, input_types: value.input_types, created_at: value.created_at });
80
+ }
81
+ return { total_count: names.length, offset, entries, next_offset: offset + entries.length < names.length ? offset + entries.length : null };
82
+ }
83
+ async function writeHistory(record: Record<string, unknown>) {
84
+ await mkdir(history, { recursive: true });
85
+ const file = path.join(history, `${record.run_id}.json`), temp = `${file}.${randomUUID()}.tmp`;
86
+ await writeFile(temp, JSON.stringify(record, null, 2), { encoding: "utf8", flag: "wx" });
87
+ await rename(temp, file);
88
+ }
89
+ async function readHistory(name: unknown, offset: number, limit: number) {
90
+ const filter = name === undefined ? undefined : scriptName(name);
91
+ const entries = [];
92
+ for (const file of await files(history)) {
93
+ if (!/^[a-f0-9-]{36}\.json$/.test(file)) continue;
94
+ const value = JSON.parse(await readFile(path.join(history, file), "utf8"));
95
+ if (!filter || value.name === filter) entries.push(value);
96
+ }
97
+ entries.sort((a, b) => b.started_at.localeCompare(a.started_at) || a.run_id.localeCompare(b.run_id));
98
+ return { total_count: entries.length, offset, entries: entries.slice(offset, offset + limit), next_offset: offset + limit < entries.length ? offset + limit : null };
99
+ }
100
+ return { root, read, save, list, validateInputs, writeHistory, readHistory };
101
+ }
102
+
103
+ export function registerScriptLibrary(pi: ExtensionAPI, run: Runner, render: (value: unknown) => Promise<Result>) {
104
+ const library = createScriptLibrary(path.join(process.env.APPDATA ?? path.join(os.homedir(), "AppData", "Roaming"), "pi-revit", "scripts"));
105
+ pi.registerTool({
106
106
  name: "manage_revit_scripts", label: "Manage Revit Scripts",
107
- description: "Save, list, read, run and inspect local reusable C# scripts. Saving never executes code. Versions are immutable content hashes; read a version before explicitly running that exact hash. Every declared input is required; extra inputs are rejected. input_types validates top-level JSON kinds only; scripts validate nested contents and domain rules. Runs use execute_csharp with separate inputs JsonElement, exact expected_document_id and operation receipts. Scripts have the same unrestricted model/UI/file/external effects and transaction behavior as execute_csharp. Local history records version, document, input hash and receipt, not input values or results. After interruption, inspect get_revit_operation before retrying with the same _operation_id and identical inputs. No automatic runs or model saving.",
108
- parameters: Type.Object({
109
- action: Type.Union(["list", "save", "read", "run", "history"].map(value => Type.Literal(value))),
110
- name: Type.Optional(Type.String()), version: Type.Optional(Type.String()), description: Type.Optional(Type.String({ maxLength: 2000 })),
111
- code: Type.Optional(Type.String({ minLength: 1, maxLength: 100000 })),
112
- input_types: Type.Optional(Type.Record(Type.String(), Type.Union(kinds.map(value => Type.Literal(value))))),
113
- inputs: Type.Optional(Type.Record(Type.String(), Type.Unknown())), expected_document_id: Type.Optional(Type.String()), _operation_id: Type.Optional(Type.String()),
114
- offset: Type.Optional(Type.Integer({ minimum: 0 })), limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 100 })),
115
- }),
116
- executionMode: "sequential",
117
- async execute(_id, args, signal) {
118
- const offset = Math.max(0, args.offset ?? 0), limit = Math.max(1, Math.min(100, args.limit ?? 20));
119
- if (args.action === "save") return render(await library.save(args));
120
- if (args.action === "list") return render(await library.list(args.name, offset, limit));
121
- if (args.action === "history") return render(await library.readHistory(args.name, offset, limit));
122
- if (args.action !== "read" && args.action !== "run") throw new Error("Unknown script library action.");
123
- const saved = await library.read(args.name, args.version);
124
- if (args.action === "read") return render(saved);
125
- if (typeof args.expected_document_id !== "string" || !args.expected_document_id) throw new Error("Run requires expected_document_id from get_model_overview.");
126
- const inputs = library.validateInputs(saved, args.inputs ?? {});
127
- const record: Record<string, unknown> = { run_id: randomUUID(), name: saved.name, version: saved.version, started_at: new Date().toISOString(), expected_document_id: args.expected_document_id, inputs_hash: hash(inputs), state: "prepared" };
128
- let dispatched = false;
129
- try {
130
- const result = await run({ code: saved.code, inputs, expected_document_id: args.expected_document_id, ...(args._operation_id !== undefined ? { _operation_id: args._operation_id } : {}) }, signal, async receipt => {
131
- Object.assign(record, receipt); await library.writeHistory(record); dispatched = true;
132
- });
133
- Object.assign(record, { state: "response_received", completed_at: new Date().toISOString() });
134
- try { await library.writeHistory(record); }
135
- catch { result.content.push({ type: "text", text: "The script response was received, but the final local history update failed. Use the operation receipt for its outcome." }); }
136
- return result;
137
- } catch (error) {
138
- if (dispatched) {
139
- Object.assign(record, { state: "outcome_unconfirmed", completed_at: new Date().toISOString() });
140
- try { await library.writeHistory(record); } catch { /* Preserve the original error and operation ID. */ }
141
- }
142
- throw error;
143
- }
144
- },
145
- });
146
- }
107
+ description: "Save, list, read, run and inspect local reusable C# scripts. Saving never executes code. Versions are immutable content hashes; read a version before explicitly running that exact hash. Every declared input is required; extra inputs are rejected. input_types validates top-level JSON kinds only; scripts validate nested contents and domain rules. Runs use execute_csharp with separate inputs JsonElement, exact expected_document_id and operation receipts. Scripts have the same unrestricted model/UI/file/external effects and transaction behavior as execute_csharp. Local history records version, document, input hash and receipt, not input values or results. After interruption, inspect get_revit_operation before retrying with the same _operation_id and identical inputs. No automatic runs or model saving.",
108
+ parameters: Type.Object({
109
+ action: Type.Union(["list", "save", "read", "run", "history"].map(value => Type.Literal(value))),
110
+ name: Type.Optional(Type.String()), version: Type.Optional(Type.String()), description: Type.Optional(Type.String({ maxLength: 2000 })),
111
+ code: Type.Optional(Type.String({ minLength: 1, maxLength: 100000 })),
112
+ input_types: Type.Optional(Type.Record(Type.String(), Type.Union(kinds.map(value => Type.Literal(value))))),
113
+ inputs: Type.Optional(Type.Record(Type.String(), Type.Unknown())), expected_document_id: Type.Optional(Type.String()), _operation_id: Type.Optional(Type.String()),
114
+ offset: Type.Optional(Type.Integer({ minimum: 0 })), limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 100 })),
115
+ }),
116
+ executionMode: "sequential",
117
+ async execute(_id, args, signal) {
118
+ const offset = Math.max(0, args.offset ?? 0), limit = Math.max(1, Math.min(100, args.limit ?? 20));
119
+ if (args.action === "save") return render(await library.save(args));
120
+ if (args.action === "list") return render(await library.list(args.name, offset, limit));
121
+ if (args.action === "history") return render(await library.readHistory(args.name, offset, limit));
122
+ if (args.action !== "read" && args.action !== "run") throw new Error("Unknown script library action.");
123
+ const saved = await library.read(args.name, args.version);
124
+ if (args.action === "read") return render(saved);
125
+ if (typeof args.expected_document_id !== "string" || !args.expected_document_id) throw new Error("Run requires expected_document_id from get_model_overview.");
126
+ const inputs = library.validateInputs(saved, args.inputs ?? {});
127
+ const record: Record<string, unknown> = { run_id: randomUUID(), name: saved.name, version: saved.version, started_at: new Date().toISOString(), expected_document_id: args.expected_document_id, inputs_hash: hash(inputs), state: "prepared" };
128
+ let dispatched = false;
129
+ try {
130
+ const result = await run({ code: saved.code, inputs, expected_document_id: args.expected_document_id, ...(args._operation_id !== undefined ? { _operation_id: args._operation_id } : {}) }, signal, async receipt => {
131
+ Object.assign(record, receipt); await library.writeHistory(record); dispatched = true;
132
+ });
133
+ Object.assign(record, { state: "response_received", completed_at: new Date().toISOString() });
134
+ try { await library.writeHistory(record); }
135
+ catch { result.content.push({ type: "text", text: "The script response was received, but the final local history update failed. Use the operation receipt for its outcome." }); }
136
+ return result;
137
+ } catch (error) {
138
+ if (dispatched) {
139
+ Object.assign(record, { state: "outcome_unconfirmed", completed_at: new Date().toISOString() });
140
+ try { await library.writeHistory(record); } catch { /* Preserve the original error and operation ID. */ }
141
+ }
142
+ throw error;
143
+ }
144
+ },
145
+ });
146
+ }
@@ -1,5 +1,10 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { Type } from "typebox";
3
+ import { readdirSync, readFileSync } from "node:fs";
4
+ import path from "node:path";
5
+ import { contractHash, descriptorLimits, packagedContracts, withApiLookups, type ToolLimit } from "./contracts.js";
6
+ import { createDiscoveryIndex } from "./discovery.js";
7
+ import { documentedTools, packagedGuidance, readablePathWithin, resolveToolDocumentation, skillRoot } from "./tool-documentation.js";
3
8
 
4
9
  export interface BridgeToolDescriptor {
5
10
  name: string;
@@ -12,26 +17,89 @@ export interface BridgeToolDescriptor {
12
17
  write?: boolean;
13
18
  effects?: string[];
14
19
  requiresDocument?: boolean;
20
+ documentKinds?: string[] | null;
15
21
  promptSnippet?: string | null;
16
22
  promptGuidelines?: string[] | null;
23
+ keywords?: string[] | null;
24
+ limits?: unknown;
25
+ verification?: string | null;
17
26
  }
18
27
 
28
+ interface Candidate {
29
+ name: string; source: "native" | "bridge"; description: string | null; summary: string | null; keywords: string[];
30
+ tier: string | null; effects: string[] | null; verification: string | null; limits: ToolLimit[] | null; inputs: string[]; documentKinds: string[] | null;
31
+ }
32
+ interface Guide { name: string; kind: string; path: string | null; summary: string; keywords: string[] }
33
+
34
+ /** Packaged guidance plus every skill found beside this package's skill (future subject skills included). */
35
+ function loadGuidance(): Guide[] {
36
+ const guides: Guide[] = packagedGuidance.map(g => ({ name: g.name, kind: g.kind, path: readablePathWithin(skillRoot, g.path), summary: g.summary, keywords: g.keywords ?? [] }));
37
+ const skillsRoot = path.resolve(skillRoot, "..");
38
+ let directories: string[] = [];
39
+ try { directories = readdirSync(skillsRoot, { withFileTypes: true }).filter(d => d.isDirectory()).map(d => d.name).sort(); } catch { /* no sibling skills */ }
40
+ for (const directory of directories) {
41
+ const file = readablePathWithin(skillsRoot, path.join(directory, "SKILL.md"));
42
+ if (!file) continue;
43
+ try {
44
+ const front = readFileSync(file, "utf8").match(/^---\r?\n([\s\S]*?)\r?\n---/)?.[1] ?? "";
45
+ const field = (key: string) => front.match(new RegExp(`^${key}:\\s*(.+)$`, "m"))?.[1]?.trim().replace(/^["']|["']$/g, "") ?? null;
46
+ const keywords = (field("keywords") ?? "").split(",").map(k => k.trim()).filter(Boolean);
47
+ guides.push({ name: field("name") ?? directory, kind: "skill", path: file, summary: field("description") ?? "", keywords });
48
+ } catch { /* unreadable skill metadata is skipped, never fatal */ }
49
+ }
50
+ return guides;
51
+ }
52
+
53
+ const FALLBACK_ROUTE = [
54
+ "A missing dedicated tool does not mean the operation is impossible.",
55
+ "If a tool is still expected, retry with fewer task words, a different wording, exact names, or scope=documentation.",
56
+ "Otherwise verify the needed Revit API members with search_api_docs, then use execute_csharp within the requested scope.",
57
+ "Report that something is not possible only after that check, naming what was checked.",
58
+ ];
59
+
19
60
  /** The bridge registry is the source of truth; discovery never duplicates its schemas. */
20
61
  export function createToolCatalog(pi: ExtensionAPI, discover: () => Promise<boolean>) {
21
62
  const entries = new Map<string, BridgeToolDescriptor>();
63
+ const liveHashes = new Map<string, string>();
64
+ // Pi retains registrations when a bridge switch merely deactivates them.
65
+ // Keep this separate from the selected bridge's current discovery snapshot.
66
+ const registeredBridgeNames = new Set<string>();
67
+ const manifestByName = new Map(documentedTools.map(entry => [entry.name, entry]));
68
+ const natives = documentedTools.filter(entry => entry.source === "native");
69
+ const guides = loadGuidance();
70
+ const index = createDiscoveryIndex();
71
+ let bridgeVersion: string | null = null;
72
+ let observedAt: string | null = null;
22
73
  function hideAdvanced(names: string[] = [...entries.keys()]) {
23
74
  const advanced = new Set(names.filter(name => entries.get(name)?.tier === "advanced"));
24
75
  pi.setActiveTools(pi.getActiveTools().filter(name => !advanced.has(name)));
25
76
  }
77
+ function candidate(name: string, source: "native" | "bridge", live?: BridgeToolDescriptor): Candidate {
78
+ const packaged = packagedContracts.get(name);
79
+ return {
80
+ name, source,
81
+ description: live?.description ?? null,
82
+ summary: manifestByName.get(name)?.summary ?? null,
83
+ // Live v2 metadata wins; packaged metadata fills gaps from older bridges; unknown stays unknown.
84
+ keywords: live?.keywords ?? packaged?.keywords ?? [],
85
+ tier: live?.tier ?? packaged?.tier ?? (source === "native" ? "core" : null),
86
+ effects: live?.effects ?? (live?.write !== undefined ? (live.write ? ["model"] : []) : packaged?.effects ?? null),
87
+ verification: live?.verification ?? packaged?.verification ?? null,
88
+ limits: descriptorLimits(live?.limits) ?? packaged?.limits ?? null,
89
+ inputs: Object.keys(((live?.parameters ?? packaged?.parameters) as { properties?: object } | undefined)?.properties ?? {}),
90
+ documentKinds: live?.documentKinds ?? packaged?.document_kinds ?? null,
91
+ };
92
+ }
26
93
 
27
94
  pi.registerTool({
28
95
  name: "find_revit_tools",
29
96
  label: "Find Revit Tools",
30
- description: "Search the installed Revit tool catalogue and activate matching tools for this Pi session. Search words match tool names and descriptions. Supply exact names to activate known tools. With no query or names, list tools without activating them. Activation is additive and does not run any Revit operation. Read the returned schemas on the next model turn before calling a newly activated tool.",
31
- promptSnippet: "Find and activate specialist Revit tools for links, schedules, relationships, inspection and editing.",
32
- promptGuidelines: ["Use find_revit_tools to discover and activate specialist Revit tools before using execute_csharp for a task that may already have a dedicated tool."],
97
+ description: "Search PI-Revit tools, workflows and guidance by English task words, and activate matching tools. Tool vocabulary is English: translate a request written in another language into English search words first. The default available scope covers native utilities and the selected bridge's last-discovered tools; a query or names activates matches, while browsing only lists. scope=documentation searches packaged manuals without contacting Revit or activating tools, including when Revit is closed. Each tool result gives its declared limits with alternatives, its verification method, its manual path and whether that manual matches the selected bridge's contract. When nothing matches every word, the result states the remaining route: API search, then custom execution. Activation is additive and runs no Revit operation.",
98
+ promptSnippet: "Find PI-Revit capabilities, their limits and alternatives, and activate specialist tools.",
99
+ promptGuidelines: ["find_revit_tools: search with short English task words (translate the user's language first); use names for a known tool and scope=documentation while Revit is closed. Read only the returned manuals you need."],
33
100
  parameters: Type.Object({
34
- query: Type.Optional(Type.String({ description: "Search words, such as linked elements or schedules." })),
101
+ scope: Type.Optional(Type.Union([Type.Literal("available"), Type.Literal("documentation")], { description: "available (default): search registered native and last-discovered bridge tools. documentation: read-only packaged manual lookup; no bridge request or activation." })),
102
+ query: Type.Optional(Type.String({ description: "Short English task words, such as 'tag rooms' or 'count walls per level'; translate from the user's language first. Tools matching every word rank first, followed by strong partial matches; when nothing matches every word the remaining route is explained. Use separate queries for different capabilities." })),
35
103
  names: Type.Optional(Type.Array(Type.String(), { maxItems: 20, description: "Exact tool names; takes precedence over query." })),
36
104
  activate: Type.Optional(Type.Boolean({ description: "Activate returned matches. Default true for a query or exact names; false for browsing." })),
37
105
  offset: Type.Optional(Type.Integer({ minimum: 0 })),
@@ -39,22 +107,45 @@ export function createToolCatalog(pi: ExtensionAPI, discover: () => Promise<bool
39
107
  }),
40
108
  executionMode: "sequential",
41
109
  async execute(_id, args) {
42
- if (!entries.size && !(await discover())) throw new Error("The Revit tool catalogue is unavailable. Start Revit with the bridge loaded and retry.");
110
+ const scope = args.scope ?? "available";
111
+ if (scope !== "available" && scope !== "documentation") throw new Error("scope must be available or documentation.");
112
+ if (scope === "documentation" && args.activate === true) throw new Error("documentation scope does not activate tools. Use available scope to activate registered tools.");
113
+ if (scope === "available" && observedAt === null) await discover();
114
+ const candidates = new Map<string, Candidate>();
115
+ if (scope === "documentation") for (const tool of documentedTools) candidates.set(tool.name, candidate(tool.name, tool.source));
116
+ for (const tool of natives) candidates.set(tool.name, candidate(tool.name, "native"));
117
+ for (const tool of entries.values()) candidates.set(tool.name, candidate(tool.name, "bridge", tool));
43
118
  const names = args.names?.length ? [...new Set(args.names)] : undefined;
44
- if (names?.some(name => !entries.has(name))) throw new Error(`Unknown Revit tool names: ${names.filter(name => !entries.has(name)).join(", ")}`);
45
- const words = (args.query ?? "").trim().toLowerCase().split(/[\s_]+/).filter(Boolean);
46
- const matches = [...entries.values()].filter(entry => names ? names.includes(entry.name)
47
- : words.every(word => `${entry.name.replaceAll("_", " ")} ${entry.description ?? ""}`.toLowerCase().includes(word)))
48
- .sort((a, b) => a.name.localeCompare(b.name));
119
+ if (names?.some(name => !candidates.has(name))) throw new Error(`Unknown Revit tool names or tools unavailable in this scope: ${names.filter(name => !candidates.has(name)).join(", ")}. Use scope=documentation to look up packaged manuals independently of bridge availability.`);
120
+ const query = (args.query ?? "").trim();
121
+ const toDocument = (c: Candidate) => ({ name: c.name, keywords: c.keywords, summary: c.summary, description: c.description, limits: c.limits?.map(limit => limit.what), inputs: c.inputs });
122
+ let mode: "names" | "browse" | "all" | "partial" | "none";
123
+ let matches: Candidate[];
124
+ if (names) { mode = "names"; matches = [...candidates.values()].filter(c => names.includes(c.name)).sort((a, b) => a.name.localeCompare(b.name)); }
125
+ else if (!query) { mode = "browse"; matches = [...candidates.values()].sort((a, b) => a.name.localeCompare(b.name)); }
126
+ else { const result = index.search([...candidates.values()], query, toDocument); mode = result.mode; matches = result.matches.map(m => m.item); }
49
127
  const offset = Math.max(0, args.offset ?? 0);
50
- const page = matches.slice(offset, offset + Math.max(1, Math.min(20, args.limit ?? 10)));
51
- const activate = args.activate ?? Boolean(names || words.length);
128
+ const pageSize = Math.max(1, Math.min(20, args.limit ?? (mode === "partial" ? 5 : 10)));
129
+ const page = matches.slice(offset, offset + pageSize);
130
+ const activate = scope === "available" && (args.activate ?? (mode === "names" || mode === "all" || mode === "partial"));
52
131
  if (activate && page.length) pi.setActiveTools([...new Set([...pi.getActiveTools(), ...page.map(entry => entry.name)])]);
53
132
  const active = new Set(pi.getActiveTools());
133
+ const guidance = query && !names ? index.search(guides, query, g => ({ name: g.name, keywords: g.keywords, summary: g.summary })).matches.slice(0, 5)
134
+ .map(({ item }) => ({ name: item.name, kind: item.kind, path: item.path, summary: item.summary })) : [];
54
135
  const result = {
136
+ scope, bridge_catalog_known: observedAt !== null, bridge_catalog_observed_at: observedAt, match: mode,
137
+ instructions: "Bridge availability is a discovery snapshot, not a fresh health check; registration, advertisement and activation are separate. Follow a tool's limits alternatives when it does not cover the request; an API alternative's lookup verifies all its members in one search_api_docs call. contract_match means the manual was generated from the selected bridge's exact input contract; otherwise trust the active schema. Read only relevant manual paths.",
55
138
  total_count: matches.length, offset, returned_count: page.length,
56
139
  next_offset: offset + page.length < matches.length ? offset + page.length : null,
57
- tools: page.map(entry => ({ name: entry.name, description: entry.description, tier: entry.tier ?? "core", effects: entry.effects ?? (entry.write ? ["model"] : []), active: active.has(entry.name) })),
140
+ tools: page.map(entry => ({ name: entry.name, description: entry.description ?? entry.summary, source: entry.source,
141
+ tier: entry.tier, effects: entry.effects, verification: entry.verification, document_kinds: entry.documentKinds,
142
+ limits: entry.limits ? withApiLookups(entry.limits) : "unknown: no declared limits; if the tool does not cover the request, use the capability route",
143
+ registered: entry.source === "native" || registeredBridgeNames.has(entry.name),
144
+ advertised_by_selected_bridge: entry.source === "native" ? null : observedAt !== null ? entries.has(entry.name) : null,
145
+ active: active.has(entry.name),
146
+ documentation: resolveToolDocumentation(entry.name, { contractHash: entries.has(entry.name) ? liveHashes.get(entry.name) ?? null : null, bridgeVersion }) })),
147
+ guidance,
148
+ ...(query && (mode === "none" || mode === "partial") ? { fallback: { reason: mode === "none" ? "No tool matched this query." : "No tool matched every word; these are partial matches.", route: FALLBACK_ROUTE } } : {}),
58
149
  };
59
150
  return { content: [{ type: "text" as const, text: JSON.stringify(result) }], details: result };
60
151
  },
@@ -62,6 +153,14 @@ export function createToolCatalog(pi: ExtensionAPI, discover: () => Promise<bool
62
153
  function reset() {
63
154
  pi.setActiveTools(pi.getActiveTools().filter(name => !entries.has(name)));
64
155
  entries.clear();
156
+ liveHashes.clear();
157
+ bridgeVersion = null;
158
+ observedAt = null;
65
159
  }
66
- return { add: (entry: BridgeToolDescriptor) => entries.set(entry.name, entry), hideAdvanced, reset };
160
+ return { add: (entry: BridgeToolDescriptor) => {
161
+ entries.set(entry.name, entry); liveHashes.set(entry.name, contractHash(entry)); registeredBridgeNames.add(entry.name); observedAt = new Date().toISOString();
162
+ },
163
+ // Called after successful /tools discovery, including a valid empty array.
164
+ setBridgeVersion: (value: string | null) => { bridgeVersion = value; observedAt = new Date().toISOString(); },
165
+ liveContracts: () => new Map(liveHashes), hideAdvanced, reset };
67
166
  }
@@ -0,0 +1,72 @@
1
+ import { accessSync, constants, realpathSync, statSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import manifest from "../../skills/pi-revit/tool-manifest.json";
5
+ import { version as packageVersion } from "../../package.json";
6
+ import { packagedContracts } from "./contracts.js";
7
+
8
+ export interface DocumentedTool { name: string; source: "native" | "bridge"; group: string; summary: string; }
9
+ export interface GuidanceEntry { name: string; kind: string; path: string; summary: string; keywords?: string[] }
10
+ // This index is documentation metadata only. Live registration remains authoritative
11
+ // for input schemas, effects, tool availability and execution.
12
+ export const documentedTools = manifest.tools as DocumentedTool[];
13
+ export const packagedGuidance = manifest.guidance as GuidanceEntry[];
14
+ export const documentationRevision = manifest.documentation_revision as string;
15
+ const byName = new Map(documentedTools.map(tool => [tool.name, tool]));
16
+ const defaultRoot = fileURLToPath(new URL("../../skills/pi-revit/references/tools/", import.meta.url));
17
+ export const skillRoot = fileURLToPath(new URL("../../skills/pi-revit/", import.meta.url));
18
+ export const manualDirectory = defaultRoot;
19
+
20
+ /**
21
+ * Compatibility compares the executable-contract hash of the selected bridge's live
22
+ * descriptor with the hash the packaged manual was generated from:
23
+ * package_local native tool documented and shipped with this extension
24
+ * contract_match identical input schema and effects; the manual describes this contract
25
+ * contract_changed the live contract differs; trust the active schema over the manual
26
+ * undocumented the live tool has no packaged contract (newer bridge)
27
+ * unknown no live contract observed (bridge not discovered)
28
+ */
29
+ export interface LiveEvidence { contractHash?: string | null; bridgeVersion?: string | null }
30
+
31
+ /** Real-path containment keeps documentation lookup inside `root`; failures are nonfatal. */
32
+ export function readablePathWithin(root: string, relative: string): string | null {
33
+ try {
34
+ const base = realpathSync(root), real = realpathSync(path.resolve(root, relative));
35
+ const inside = path.relative(base, real);
36
+ if (inside && inside !== ".." && !inside.startsWith(`..${path.sep}`) && !path.isAbsolute(inside) && statSync(real).isFile()) {
37
+ accessSync(real, constants.R_OK);
38
+ return real;
39
+ }
40
+ } catch {
41
+ // Missing, unreadable or concurrently replaced documentation must not
42
+ // prevent tool registration or discovery. Do not read manual bodies here.
43
+ }
44
+ return null;
45
+ }
46
+
47
+ export function createDocumentationResolver(root = defaultRoot) {
48
+ return (name: string, live: LiveEvidence = {}) => {
49
+ const entry = byName.get(name);
50
+ const contract = packagedContracts.get(name);
51
+ const liveHash = live.contractHash ?? null;
52
+ if (!entry) return {
53
+ key: name, status: "missing", path: null, packaged_contract_hash: null, live_contract_hash: liveHash,
54
+ compatibility: liveHash ? "undocumented" : "unknown",
55
+ };
56
+ const readablePath = readablePathWithin(root, `${entry.name}.md`);
57
+ const packagedHash = contract?.contract_hash ?? null;
58
+ return {
59
+ key: entry.name, status: readablePath ? "available" : "missing", path: readablePath,
60
+ revision: documentationRevision, package_version: packageVersion,
61
+ packaged_contract_hash: packagedHash,
62
+ live_contract_hash: entry.source === "bridge" ? liveHash : null,
63
+ observed_bridge_version: entry.source === "bridge" ? live.bridgeVersion ?? null : null,
64
+ compatibility: entry.source === "native" ? "package_local"
65
+ : !liveHash ? "unknown"
66
+ : !packagedHash ? "undocumented"
67
+ : liveHash === packagedHash ? "contract_match" : "contract_changed",
68
+ };
69
+ };
70
+ }
71
+
72
+ export const resolveToolDocumentation = createDocumentationResolver();
@@ -0,0 +1,8 @@
1
+ /** Compose the extension-only retry input after the bridge has added document targeting. */
2
+ export function publicBridgeSchema(parameters: unknown) {
3
+ const schema = structuredClone(parameters ?? { type: "object", properties: {} }) as { properties?: Record<string, unknown> };
4
+ schema.properties = { ...schema.properties, _operation_id: {
5
+ type: "string", description: "Optional exact operation ID for retrying an identical earlier request. Reuses its result without repeating the action. Omit for a new operation.",
6
+ } };
7
+ return schema;
8
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-revit",
3
- "version": "0.4.0",
3
+ "version": "0.5.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",
@@ -28,8 +28,12 @@
28
28
  "setup": "powershell -ExecutionPolicy Bypass -File scripts/setup-workspace.ps1",
29
29
  "uninstall:revit": "powershell -ExecutionPolicy Bypass -File scripts/uninstall.ps1",
30
30
  "test:search": "dotnet run --project tests/search-engine",
31
+ "test:extension": "node scripts/test-extension.mjs",
32
+ "test:docs": "node scripts/check-tool-documentation.mjs",
31
33
  "test:installer": "node --test tests/installer/*.test.cjs",
32
- "test:sdk": "powershell -NoProfile -ExecutionPolicy Bypass -File tests/installer/sdk.test.ps1"
34
+ "test:sdk": "powershell -NoProfile -ExecutionPolicy Bypass -File tests/installer/sdk.test.ps1",
35
+ "generate:contracts": "node scripts/generate-contracts.mjs",
36
+ "test:corpus": "node tests/discovery/run-corpus.mjs"
33
37
  },
34
38
  "files": [
35
39
  "bin/",
@@ -38,6 +42,8 @@
38
42
  "scripts/",
39
43
  "src/",
40
44
  "workspace/",
45
+ "docs/",
46
+ "AGENTS.md",
41
47
  "README.md",
42
48
  "CHANGELOG.md"
43
49
  ],
package/scripts/build.ps1 CHANGED
@@ -17,15 +17,15 @@ param(
17
17
  )
18
18
 
19
19
  $ErrorActionPreference = 'Stop'
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)
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)
29
29
 
30
30
  # Stamp the package version into the assembly so the bridge can report which release
31
31
  # the deployed add-in came from; the pi extension compares it against its own package