pi-revit 0.3.1 → 0.5.0
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/AGENTS.md +167 -0
- package/CHANGELOG.md +114 -42
- package/README.md +598 -138
- package/bin/pi-revit.js +9 -9
- package/docs/architecture.md +271 -0
- package/docs/evaluation.md +434 -0
- package/docs/invariants.json +147 -0
- package/extensions/pi-revit/completion-monitor.ts +55 -0
- package/extensions/pi-revit/contracts.ts +146 -0
- package/extensions/pi-revit/discovery.ts +93 -0
- package/extensions/pi-revit/index.ts +231 -85
- package/extensions/pi-revit/instance-router.ts +86 -0
- package/extensions/pi-revit/platform-prompt.ts +40 -0
- package/extensions/pi-revit/scope-monitor.ts +114 -0
- package/extensions/pi-revit/script-library.ts +146 -0
- package/extensions/pi-revit/tool-catalog.ts +166 -0
- package/extensions/pi-revit/tool-documentation.ts +72 -0
- package/extensions/pi-revit/tool-schema.ts +8 -0
- package/package.json +65 -59
- package/scripts/build.ps1 +9 -9
- package/scripts/check-sdk.ps1 +66 -66
- package/scripts/check-tool-documentation.mjs +287 -0
- package/scripts/deploy.ps1 +16 -16
- package/scripts/generate-contracts.mjs +80 -0
- package/scripts/lib/platform.mjs +226 -0
- package/scripts/test-extension.mjs +15 -0
- package/skills/pi-revit/SKILL.md +39 -63
- package/skills/pi-revit/contracts.generated.json +3524 -0
- package/skills/pi-revit/references/execution-rules.md +41 -0
- package/skills/pi-revit/references/model-audit-export.md +40 -0
- package/skills/pi-revit/references/operation-recovery.md +33 -0
- package/skills/pi-revit/references/room-documentation.md +39 -0
- package/skills/pi-revit/references/tool-index.md +89 -0
- package/skills/pi-revit/references/tools/capture_view.md +62 -0
- package/skills/pi-revit/references/tools/change_element_types.md +65 -0
- package/skills/pi-revit/references/tools/create_tags.md +85 -0
- package/skills/pi-revit/references/tools/delete_elements.md +66 -0
- package/skills/pi-revit/references/tools/execute_csharp.md +81 -0
- package/skills/pi-revit/references/tools/export_documents.md +75 -0
- package/skills/pi-revit/references/tools/find_revit_tools.md +96 -0
- package/skills/pi-revit/references/tools/get_element_details.md +66 -0
- package/skills/pi-revit/references/tools/get_element_relationships.md +61 -0
- package/skills/pi-revit/references/tools/get_element_types.md +67 -0
- package/skills/pi-revit/references/tools/get_elements.md +87 -0
- package/skills/pi-revit/references/tools/get_linked_elements.md +79 -0
- package/skills/pi-revit/references/tools/get_linked_models.md +57 -0
- package/skills/pi-revit/references/tools/get_model_coordinates.md +64 -0
- package/skills/pi-revit/references/tools/get_model_health.md +53 -0
- package/skills/pi-revit/references/tools/get_model_overview.md +57 -0
- package/skills/pi-revit/references/tools/get_revit_operation.md +54 -0
- package/skills/pi-revit/references/tools/get_schedule_fields.md +62 -0
- package/skills/pi-revit/references/tools/get_schedules.md +71 -0
- package/skills/pi-revit/references/tools/manage_element_sets.md +92 -0
- package/skills/pi-revit/references/tools/manage_revit_instances.md +63 -0
- package/skills/pi-revit/references/tools/manage_revit_scripts.md +109 -0
- package/skills/pi-revit/references/tools/manage_schedules.md +90 -0
- package/skills/pi-revit/references/tools/manage_selection.md +66 -0
- package/skills/pi-revit/references/tools/manage_sheet_placements.md +82 -0
- package/skills/pi-revit/references/tools/manage_sheets.md +71 -0
- package/skills/pi-revit/references/tools/manage_views.md +95 -0
- package/skills/pi-revit/references/tools/measure_geometry.md +71 -0
- package/skills/pi-revit/references/tools/open_view.md +59 -0
- package/skills/pi-revit/references/tools/ping.md +41 -0
- package/skills/pi-revit/references/tools/query_spatial_elements.md +74 -0
- package/skills/pi-revit/references/tools/read_revit_result.md +53 -0
- package/skills/pi-revit/references/tools/search_api_docs.md +65 -0
- package/skills/pi-revit/references/tools/set_parameters.md +75 -0
- package/skills/pi-revit/references/tools/summarize_elements.md +64 -0
- package/skills/pi-revit/references/tools/transform_elements.md +79 -0
- package/skills/pi-revit/references/visual-verification.md +36 -0
- package/skills/pi-revit/tool-manifest.json +338 -0
- package/src/Revit/BridgeServer.cs +75 -19
- package/src/Revit/OperationStore.cs +178 -0
- package/src/Revit/ToolRegistry.cs +61 -8
- package/src/Revit/Tools/CaptureView.cs +9 -0
- package/src/Revit/Tools/ChangeElementTypes.cs +74 -0
- package/src/Revit/Tools/ChangeSet.cs +39 -0
- package/src/Revit/Tools/CreateTags.cs +107 -0
- package/src/Revit/Tools/DeleteElements.cs +53 -0
- package/src/Revit/Tools/DocumentGuard.cs +12 -2
- package/src/Revit/Tools/ElementNames.cs +103 -0
- package/src/Revit/Tools/ElementQueryScope.cs +27 -0
- package/src/Revit/Tools/ElementTraits.cs +53 -0
- package/src/Revit/Tools/ExecuteCsharp.cs +26 -7
- package/src/Revit/Tools/ExportDocuments.cs +9 -0
- package/src/Revit/Tools/FailureGuard.cs +26 -26
- package/src/Revit/Tools/GetElementDetails.cs +28 -2
- package/src/Revit/Tools/GetElementRelationships.cs +82 -0
- package/src/Revit/Tools/GetElementTypes.cs +8 -0
- package/src/Revit/Tools/GetElements.cs +58 -55
- package/src/Revit/Tools/GetLinkedElements.cs +89 -0
- package/src/Revit/Tools/GetLinkedModels.cs +73 -0
- package/src/Revit/Tools/GetModelCoordinates.cs +56 -0
- package/src/Revit/Tools/GetModelHealth.cs +7 -0
- package/src/Revit/Tools/GetModelOverview.cs +187 -160
- package/src/Revit/Tools/GetScheduleFields.cs +44 -0
- package/src/Revit/Tools/GetSchedules.cs +96 -0
- package/src/Revit/Tools/InheritedState.Summary.cs +57 -0
- package/src/Revit/Tools/InheritedState.cs +144 -0
- package/src/Revit/Tools/ManageElementSets.cs +114 -0
- package/src/Revit/Tools/ManageSchedules.cs +174 -0
- package/src/Revit/Tools/ManageSelection.cs +9 -0
- package/src/Revit/Tools/ManageSheetPlacements.cs +113 -0
- package/src/Revit/Tools/ManageSheets.cs +72 -0
- package/src/Revit/Tools/ManageViews.cs +115 -0
- package/src/Revit/Tools/MeasureGeometry.cs +60 -0
- package/src/Revit/Tools/ModelChanges.cs +154 -0
- package/src/Revit/Tools/ModelEditBatch.cs +105 -0
- package/src/Revit/Tools/ModelEditInputs.cs +49 -0
- package/src/Revit/Tools/OpenView.cs +8 -0
- package/src/Revit/Tools/ParameterResolver.cs +94 -0
- package/src/Revit/Tools/QuerySpatialElements.cs +70 -0
- package/src/Revit/Tools/SearchApiDocs.cs +72 -4
- package/src/Revit/Tools/SetParameters.cs +50 -119
- package/src/Revit/Tools/SpatialBounds.cs +30 -0
- package/src/Revit/Tools/SummarizeElements.cs +94 -0
- package/src/Revit/Tools/ToolContract.cs +48 -0
- package/src/Revit/Tools/ToolSupport.cs +4 -0
- package/src/Revit/Tools/TransformElements.cs +73 -0
- package/workspace/AGENTS.md +54 -48
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scope monitor (inv:existing-objects-not-reused). Tool-agnostic: it reads the model_changes
|
|
3
|
+
* report the bridge attaches to every model-changing result, and the name_collision facts a
|
|
4
|
+
* rejected creation carries, never tool names. Per user request it keeps the IDs of objects
|
|
5
|
+
* created in that request. When a call changes an object that existed before the request and
|
|
6
|
+
* whose name the request mentions, or a name the request uses is already taken, it returns a
|
|
7
|
+
* note that the object predates the request; the platform protocol then requires asking or
|
|
8
|
+
* reporting. It steers and never blocks. Reset at the start of every user prompt.
|
|
9
|
+
*/
|
|
10
|
+
export interface ChangedItem { id: number; name?: string | null; category?: string | null }
|
|
11
|
+
export interface ModelChanges {
|
|
12
|
+
observed?: boolean;
|
|
13
|
+
rolled_back?: boolean;
|
|
14
|
+
/** Family types and parameters (not elements) added, removed or changed by the call, in a family document. */
|
|
15
|
+
family?: { types_added?: string[]; types_removed?: string[]; types_changed?: string[]; parameters_added?: string[]; parameters_removed?: string[] };
|
|
16
|
+
added?: { count: number; items?: ChangedItem[] };
|
|
17
|
+
modified?: { count: number; items?: ChangedItem[] };
|
|
18
|
+
deleted?: { count: number; ids?: number[] };
|
|
19
|
+
}
|
|
20
|
+
export interface NameCollision { existing_id: number | null; kind?: string; name?: string; sheet_number?: string }
|
|
21
|
+
|
|
22
|
+
/** The model_changes report of a bridge payload, or null (older bridge, read tool, non-object payload). */
|
|
23
|
+
export function readModelChanges(payload: unknown): ModelChanges | null {
|
|
24
|
+
const value = payload !== null && typeof payload === "object" ? (payload as { model_changes?: unknown }).model_changes : undefined;
|
|
25
|
+
return value !== null && typeof value === "object" ? value as ModelChanges : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Whether a call actually changed the model, from model_changes: a report without observed
|
|
30
|
+
* document changes means nothing changed (a read through a write-capable tool). Undefined only
|
|
31
|
+
* when there is no report (an older bridge), so declared metadata decides.
|
|
32
|
+
*/
|
|
33
|
+
export function changedModel(payload: unknown): boolean | undefined {
|
|
34
|
+
const changes = readModelChanges(payload);
|
|
35
|
+
if (!changes) return undefined;
|
|
36
|
+
if (!changes.observed) return false;
|
|
37
|
+
return (changes.added?.count ?? 0) + (changes.modified?.count ?? 0) + (changes.deleted?.count ?? 0) > 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Every name_collision object anywhere in a payload (rejected rows keep it next to their reason). */
|
|
41
|
+
export function findCollisions(payload: unknown, found: NameCollision[] = [], depth = 0): NameCollision[] {
|
|
42
|
+
if (depth > 6 || payload === null || typeof payload !== "object") return found;
|
|
43
|
+
for (const [key, value] of Object.entries(payload as Record<string, unknown>)) {
|
|
44
|
+
if (key === "name_collision" && value && typeof value === "object" && "existing_id" in value) found.push(value as NameCollision);
|
|
45
|
+
else if (key !== "model_changes") findCollisions(value, found, depth + 1);
|
|
46
|
+
}
|
|
47
|
+
return found;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const normalize = (text: string) => text.normalize("NFKC").toLowerCase().replace(/\s+/g, " ").trim();
|
|
51
|
+
const words = new Intl.Segmenter(undefined, { granularity: "word" });
|
|
52
|
+
const graphemes = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Word boundaries of a text in any writing system, from Unicode segmentation (ICU): spaces and
|
|
56
|
+
* punctuation where a script uses them, dictionary boundaries where it does not (Chinese,
|
|
57
|
+
* Japanese, Thai, Khmer, ...). No language or script list is involved.
|
|
58
|
+
*/
|
|
59
|
+
export function wordBoundaries(text: string): Set<number> {
|
|
60
|
+
const bounds = new Set([0, text.length]);
|
|
61
|
+
for (const segment of words.segment(text)) { bounds.add(segment.index); bounds.add(segment.index + segment.segment.length); }
|
|
62
|
+
return bounds;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Whether name occurs in text as whole words: 'Site' is not mentioned by 'opposite'. */
|
|
66
|
+
export function mentionsName(text: string, bounds: Set<number>, name: string | null | undefined, minGraphemes = 2): boolean {
|
|
67
|
+
const value = normalize(name ?? "");
|
|
68
|
+
if ([...graphemes.segment(value)].length < minGraphemes) return false;
|
|
69
|
+
for (let at = text.indexOf(value); at >= 0; at = text.indexOf(value, at + 1))
|
|
70
|
+
if (bounds.has(at) && bounds.has(at + value.length)) return true;
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function createScopeMonitor(options: { minNameGraphemes?: number } = {}) {
|
|
75
|
+
let request = "";
|
|
76
|
+
let bounds = new Set<number>();
|
|
77
|
+
const created = new Set<number>();
|
|
78
|
+
const createdFamilyTypes = new Set<string>();
|
|
79
|
+
const notedFamilyTypes = new Set<string>();
|
|
80
|
+
const noted = new Set<number>();
|
|
81
|
+
const notedCollisions = new Set<number | string>();
|
|
82
|
+
const mentioned = (name: string | null | undefined) => mentionsName(request, bounds, name, options.minNameGraphemes ?? 2);
|
|
83
|
+
return {
|
|
84
|
+
/** Start of a new user request; its text decides which object names the request mentions. */
|
|
85
|
+
reset(prompt: string) {
|
|
86
|
+
request = normalize(prompt ?? ""); bounds = wordBoundaries(request);
|
|
87
|
+
created.clear(); noted.clear(); notedCollisions.clear(); createdFamilyTypes.clear(); notedFamilyTypes.clear();
|
|
88
|
+
},
|
|
89
|
+
/** Record one tool result; returns a scope note to append, or null. */
|
|
90
|
+
afterResult(payload: unknown): string | null {
|
|
91
|
+
const notes: string[] = [];
|
|
92
|
+
const changes = readModelChanges(payload);
|
|
93
|
+
for (const item of changes?.added?.items ?? []) created.add(item.id);
|
|
94
|
+
for (const name of changes?.family?.types_added ?? []) createdFamilyTypes.add(name);
|
|
95
|
+
// Family types have no element ID; the same rule applies to them by name.
|
|
96
|
+
const familyTypes = (changes?.family?.types_changed ?? []).filter(name => !createdFamilyTypes.has(name) && !notedFamilyTypes.has(name) && mentioned(name));
|
|
97
|
+
for (const name of familyTypes) notedFamilyTypes.add(name);
|
|
98
|
+
const preexisting = [...(changes?.modified?.items ?? []).filter(item => !created.has(item.id) && !noted.has(item.id) && mentioned(item.name)),
|
|
99
|
+
...familyTypes.map(name => ({ id: null as unknown as number, name, category: "family type" }))];
|
|
100
|
+
for (const item of preexisting) if (item.id !== null) noted.add(item.id);
|
|
101
|
+
if (preexisting.length) notes.push(`PI-Revit scope note: this call changed ${preexisting.map(item => item.id === null ? `family type '${item.name}'` : `'${item.name}' (id ${item.id})`).join(", ")}, which existed before this request: no call in this request created it. `
|
|
102
|
+
+ "If the request asked you to create an object with this name, you changed an existing object instead: tell the user exactly what you changed and ask before changing it further. If the request asked you to change this existing object, continue.");
|
|
103
|
+
// Family types have no element ID; they are noted by name instead.
|
|
104
|
+
const key = (c: NameCollision) => c.existing_id ?? `${c.kind}:${c.name}`;
|
|
105
|
+
const collisions = findCollisions(payload).filter(collision => !notedCollisions.has(key(collision)));
|
|
106
|
+
for (const collision of collisions) notedCollisions.add(key(collision));
|
|
107
|
+
if (collisions.length) notes.push(`PI-Revit scope note: ${collisions.map(c => `'${c.name ?? c.sheet_number}' is already used by ${c.kind ?? "an object"}${c.existing_id !== null ? ` ${c.existing_id}` : ""}`).join("; ")}. `
|
|
108
|
+
+ "That object existed before this call. Do not edit, rename, reuse, replace or delete it to get past the collision: ask the user which object or name to use, or create under a distinct name and report the collision.");
|
|
109
|
+
return notes.length ? notes.join("\n") : null;
|
|
110
|
+
},
|
|
111
|
+
/** IDs of objects created in this request, as reported by model_changes. */
|
|
112
|
+
created: () => [...created],
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -0,0 +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";
|
|
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({
|
|
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
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
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";
|
|
8
|
+
|
|
9
|
+
export interface BridgeToolDescriptor {
|
|
10
|
+
name: string;
|
|
11
|
+
label?: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
category?: string;
|
|
14
|
+
tier?: string;
|
|
15
|
+
parameters?: unknown;
|
|
16
|
+
executionMode?: string;
|
|
17
|
+
write?: boolean;
|
|
18
|
+
effects?: string[];
|
|
19
|
+
requiresDocument?: boolean;
|
|
20
|
+
documentKinds?: string[] | null;
|
|
21
|
+
promptSnippet?: string | null;
|
|
22
|
+
promptGuidelines?: string[] | null;
|
|
23
|
+
keywords?: string[] | null;
|
|
24
|
+
limits?: unknown;
|
|
25
|
+
verification?: string | null;
|
|
26
|
+
}
|
|
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
|
+
|
|
60
|
+
/** The bridge registry is the source of truth; discovery never duplicates its schemas. */
|
|
61
|
+
export function createToolCatalog(pi: ExtensionAPI, discover: () => Promise<boolean>) {
|
|
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;
|
|
73
|
+
function hideAdvanced(names: string[] = [...entries.keys()]) {
|
|
74
|
+
const advanced = new Set(names.filter(name => entries.get(name)?.tier === "advanced"));
|
|
75
|
+
pi.setActiveTools(pi.getActiveTools().filter(name => !advanced.has(name)));
|
|
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
|
+
}
|
|
93
|
+
|
|
94
|
+
pi.registerTool({
|
|
95
|
+
name: "find_revit_tools",
|
|
96
|
+
label: "Find Revit Tools",
|
|
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."],
|
|
100
|
+
parameters: Type.Object({
|
|
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." })),
|
|
103
|
+
names: Type.Optional(Type.Array(Type.String(), { maxItems: 20, description: "Exact tool names; takes precedence over query." })),
|
|
104
|
+
activate: Type.Optional(Type.Boolean({ description: "Activate returned matches. Default true for a query or exact names; false for browsing." })),
|
|
105
|
+
offset: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
106
|
+
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 20 })),
|
|
107
|
+
}),
|
|
108
|
+
executionMode: "sequential",
|
|
109
|
+
async execute(_id, args) {
|
|
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));
|
|
118
|
+
const names = args.names?.length ? [...new Set(args.names)] : undefined;
|
|
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); }
|
|
127
|
+
const offset = Math.max(0, args.offset ?? 0);
|
|
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"));
|
|
131
|
+
if (activate && page.length) pi.setActiveTools([...new Set([...pi.getActiveTools(), ...page.map(entry => entry.name)])]);
|
|
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 })) : [];
|
|
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.",
|
|
138
|
+
total_count: matches.length, offset, returned_count: page.length,
|
|
139
|
+
next_offset: offset + page.length < matches.length ? offset + page.length : null,
|
|
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 } } : {}),
|
|
149
|
+
};
|
|
150
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result) }], details: result };
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
function reset() {
|
|
154
|
+
pi.setActiveTools(pi.getActiveTools().filter(name => !entries.has(name)));
|
|
155
|
+
entries.clear();
|
|
156
|
+
liveHashes.clear();
|
|
157
|
+
bridgeVersion = null;
|
|
158
|
+
observedAt = null;
|
|
159
|
+
}
|
|
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 };
|
|
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,59 +1,65 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "pi-revit",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Native Pi connector for Autodesk Revit. Run npx.cmd -y pi-revit for the full Windows install.",
|
|
5
|
-
"author": "Ahmad Altahlawi",
|
|
6
|
-
"license": "MIT",
|
|
7
|
-
"repository": {
|
|
8
|
-
"type": "git",
|
|
9
|
-
"url": "git+https://github.com/Triavision-ai/pi-revit.git"
|
|
10
|
-
},
|
|
11
|
-
"bugs": {
|
|
12
|
-
"url": "https://github.com/Triavision-ai/pi-revit/issues"
|
|
13
|
-
},
|
|
14
|
-
"homepage": "https://github.com/Triavision-ai/pi-revit#readme",
|
|
15
|
-
"keywords": [
|
|
16
|
-
"pi-package",
|
|
17
|
-
"pi-extension",
|
|
18
|
-
"revit",
|
|
19
|
-
"bim",
|
|
20
|
-
"autodesk",
|
|
21
|
-
"revit-api"
|
|
22
|
-
],
|
|
23
|
-
"bin": {
|
|
24
|
-
"pi-revit": "bin/pi-revit.js"
|
|
25
|
-
},
|
|
26
|
-
"scripts": {
|
|
27
|
-
"deploy": "powershell -ExecutionPolicy Bypass -File scripts/deploy.ps1",
|
|
28
|
-
"setup": "powershell -ExecutionPolicy Bypass -File scripts/setup-workspace.ps1",
|
|
29
|
-
"uninstall:revit": "powershell -ExecutionPolicy Bypass -File scripts/uninstall.ps1",
|
|
30
|
-
"test:search": "dotnet run --project tests/search-engine",
|
|
31
|
-
"test:
|
|
32
|
-
"test:
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
"
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-revit",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Native Pi connector for Autodesk Revit. Run npx.cmd -y pi-revit for the full Windows install.",
|
|
5
|
+
"author": "Ahmad Altahlawi",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/Triavision-ai/pi-revit.git"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/Triavision-ai/pi-revit/issues"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/Triavision-ai/pi-revit#readme",
|
|
15
|
+
"keywords": [
|
|
16
|
+
"pi-package",
|
|
17
|
+
"pi-extension",
|
|
18
|
+
"revit",
|
|
19
|
+
"bim",
|
|
20
|
+
"autodesk",
|
|
21
|
+
"revit-api"
|
|
22
|
+
],
|
|
23
|
+
"bin": {
|
|
24
|
+
"pi-revit": "bin/pi-revit.js"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"deploy": "powershell -ExecutionPolicy Bypass -File scripts/deploy.ps1",
|
|
28
|
+
"setup": "powershell -ExecutionPolicy Bypass -File scripts/setup-workspace.ps1",
|
|
29
|
+
"uninstall:revit": "powershell -ExecutionPolicy Bypass -File scripts/uninstall.ps1",
|
|
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",
|
|
33
|
+
"test:installer": "node --test tests/installer/*.test.cjs",
|
|
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"
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"bin/",
|
|
40
|
+
"extensions/",
|
|
41
|
+
"skills/",
|
|
42
|
+
"scripts/",
|
|
43
|
+
"src/",
|
|
44
|
+
"workspace/",
|
|
45
|
+
"docs/",
|
|
46
|
+
"AGENTS.md",
|
|
47
|
+
"README.md",
|
|
48
|
+
"CHANGELOG.md"
|
|
49
|
+
],
|
|
50
|
+
"pi": {
|
|
51
|
+
"extensions": [
|
|
52
|
+
"./extensions/pi-revit/index.ts"
|
|
53
|
+
],
|
|
54
|
+
"skills": [
|
|
55
|
+
"./skills"
|
|
56
|
+
]
|
|
57
|
+
},
|
|
58
|
+
"engines": {
|
|
59
|
+
"node": ">=20.3"
|
|
60
|
+
},
|
|
61
|
+
"peerDependencies": {
|
|
62
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
63
|
+
"typebox": "*"
|
|
64
|
+
}
|
|
65
|
+
}
|