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
|
@@ -1,21 +1,26 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Type, type TSchema } from "typebox";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
3
4
|
import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
|
|
4
5
|
import { randomUUID } from "node:crypto";
|
|
5
6
|
import os from "node:os";
|
|
6
7
|
import path from "node:path";
|
|
7
8
|
import { fileURLToPath } from "node:url";
|
|
8
9
|
import { version as packageVersion } from "../../package.json";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
10
|
+
import { createToolCatalog, type BridgeToolDescriptor } from "./tool-catalog.js";
|
|
11
|
+
import { createInstanceRouter, type BridgeInfo } from "./instance-router.js";
|
|
12
|
+
import { registerScriptLibrary } from "./script-library.js";
|
|
13
|
+
import { documentationRevision, documentedTools, manualDirectory, skillRoot } from "./tool-documentation.js";
|
|
14
|
+
import { publicBridgeSchema } from "./tool-schema.js";
|
|
15
|
+
import { packagedContracts } from "./contracts.js";
|
|
16
|
+
import { buildPlatformSection, hoistSharedGuidelines } from "./platform-prompt.js";
|
|
17
|
+
import { createCompletionMonitor } from "./completion-monitor.js";
|
|
18
|
+
import { changedModel, createScopeMonitor } from "./scope-monitor.js";
|
|
19
|
+
|
|
20
|
+
type BridgeResolver = (operationId?: string) => Promise<BridgeInfo>;
|
|
16
21
|
|
|
17
22
|
interface ContentBlock {
|
|
18
|
-
type:
|
|
23
|
+
type: "text";
|
|
19
24
|
text: string;
|
|
20
25
|
}
|
|
21
26
|
|
|
@@ -30,21 +35,6 @@ interface BridgeToolResponse {
|
|
|
30
35
|
hasActiveDocument?: boolean;
|
|
31
36
|
}
|
|
32
37
|
|
|
33
|
-
/** One entry of GET /tools, as served by ToolRegistry.Describe() on the bridge. */
|
|
34
|
-
interface BridgeToolDescriptor {
|
|
35
|
-
name: string;
|
|
36
|
-
label?: string;
|
|
37
|
-
description?: string;
|
|
38
|
-
category?: string;
|
|
39
|
-
tier?: string;
|
|
40
|
-
parameters?: unknown;
|
|
41
|
-
executionMode?: string;
|
|
42
|
-
write?: boolean;
|
|
43
|
-
requiresDocument?: boolean;
|
|
44
|
-
promptSnippet?: string | null;
|
|
45
|
-
promptGuidelines?: string[] | null;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
38
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
49
39
|
const LONG_TIMEOUT_MS = 120_000;
|
|
50
40
|
const DISCOVERY_TIMEOUT_MS = 10_000;
|
|
@@ -113,11 +103,11 @@ function timeoutError(timeoutMs: number): Error {
|
|
|
113
103
|
|
|
114
104
|
export async function bridgeRequest(
|
|
115
105
|
pathname: string,
|
|
116
|
-
init: { method: "GET" | "POST"; body?: string; query?: Record<string, string
|
|
106
|
+
init: { method: "GET" | "POST"; body?: string; query?: Record<string, string>; bridge?: BridgeInfo },
|
|
117
107
|
signal?: AbortSignal,
|
|
118
108
|
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
119
109
|
): Promise<unknown> {
|
|
120
|
-
const info = await readBridgeInfo();
|
|
110
|
+
const info = init.bridge ?? await readBridgeInfo();
|
|
121
111
|
const query = new URLSearchParams({ ...(init.query ?? {}), token: info.token });
|
|
122
112
|
const url = `${info.baseUrl}${pathname}?${query.toString()}`;
|
|
123
113
|
|
|
@@ -243,33 +233,82 @@ function registerResultReader(pi: ExtensionAPI) {
|
|
|
243
233
|
});
|
|
244
234
|
}
|
|
245
235
|
|
|
246
|
-
async function runBridgeTool(name: string, args: unknown, signal: AbortSignal | undefined, timeoutMs: number
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
236
|
+
async function runBridgeTool(name: string, args: unknown, signal: AbortSignal | undefined, timeoutMs: number, resolve: BridgeResolver,
|
|
237
|
+
prepared?: (receipt: { operation_id: string; bridge_id: string }) => Promise<void>) {
|
|
238
|
+
const body = { ...(args as Record<string, unknown> ?? {}) };
|
|
239
|
+
const retryId = body._operation_id;
|
|
240
|
+
delete body._operation_id;
|
|
241
|
+
if (retryId !== undefined && (typeof retryId !== "string" || !retryId)) throw new Error("_operation_id must be the exact ID of a previous request.");
|
|
242
|
+
const info = await resolve(retryId as string | undefined);
|
|
243
|
+
if (retryId && (!info.supportsOperationTracking || !info.bridgeId)) throw new Error("This bridge does not support operation receipts; the request was not sent.");
|
|
244
|
+
const operationId = info.supportsOperationTracking && info.bridgeId ? (retryId as string | undefined) ?? `${info.bridgeId}:${randomUUID()}` : undefined;
|
|
245
|
+
if (operationId && !operationId.startsWith(`${info.bridgeId}:`)) throw new Error("This operation belongs to a different bridge session. Its outcome is unknown here; it was not replayed.");
|
|
246
|
+
if (prepared && (!operationId || !info.bridgeId)) throw new Error("Script library runs require a bridge with operation receipts. The script was not sent.");
|
|
247
|
+
try {
|
|
248
|
+
if (prepared) await prepared({ operation_id: operationId!, bridge_id: info.bridgeId! });
|
|
249
|
+
const payload = (await bridgeRequest(
|
|
250
|
+
`/tools/${encodeURIComponent(name)}/execute`,
|
|
251
|
+
{ method: "POST", body: JSON.stringify(body), bridge: info,
|
|
252
|
+
query: { timeout_ms: String(timeoutMs), ...(operationId ? { operation_id: operationId } : {}) } },
|
|
253
|
+
signal, timeoutMs,
|
|
254
|
+
)) as BridgeToolResponse;
|
|
255
|
+
const content = await modelContent(name, payload);
|
|
256
|
+
if (operationId) content.push({ type: "text", text: `Operation ID: ${operationId}. Check get_revit_operation after a timeout; retrying with this exact _operation_id and identical arguments will not repeat the action.` });
|
|
257
|
+
return { content, details: operationId ? { ...(payload.details as object ?? {}), operation_id: operationId, bridge_id: info.bridgeId } : payload.details };
|
|
258
|
+
} catch (error) {
|
|
259
|
+
if (!operationId) throw error;
|
|
260
|
+
throw new Error(`${error instanceof Error ? error.message : String(error)}\nOperation ID: ${operationId}. Use get_revit_operation to inspect its outcome. Do not retry an edit with a new ID until its effects are known.`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function registerOperationReader(pi: ExtensionAPI, resolve: BridgeResolver) {
|
|
265
|
+
pi.registerTool({
|
|
266
|
+
name: "get_revit_operation",
|
|
267
|
+
label: "Get Revit Operation",
|
|
268
|
+
description: "Read an operation receipt without waiting for Revit's model thread. Reports queued, running, succeeded, failed, expired_before_start, result_unavailable or unknown, with the original result when retained. Unknown after restart is not proof that the edit never ran. Full results are bounded to the latest 128 receipts / 32 MiB; IDs remain reserved for up to 10,000 operations per bridge session so expired results never cause re-execution.",
|
|
269
|
+
promptSnippet: "Check the outcome of a timed-out Revit operation before retrying an edit.",
|
|
270
|
+
parameters: Type.Object({ operation_id: Type.String({ minLength: 1, maxLength: 120 }) }),
|
|
271
|
+
executionMode: "sequential",
|
|
272
|
+
async execute(_id, args, signal) {
|
|
273
|
+
const bridge = await resolve(args.operation_id);
|
|
274
|
+
const result = await bridgeRequest(`/operations/${encodeURIComponent(args.operation_id)}`, { method: "GET", bridge }, signal, 10_000);
|
|
275
|
+
return { content: await modelContent("get_revit_operation", { details: { payload: result } }), details: result };
|
|
253
276
|
},
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
)) as BridgeToolResponse;
|
|
277
|
+
});
|
|
278
|
+
}
|
|
257
279
|
|
|
258
|
-
|
|
280
|
+
interface RequestMonitors { completion: ReturnType<typeof createCompletionMonitor>; scope: ReturnType<typeof createScopeMonitor> }
|
|
281
|
+
|
|
282
|
+
/** The bridge value of a tool result (details.payload for current and older bridges). */
|
|
283
|
+
function resultPayload(details: unknown): unknown {
|
|
284
|
+
return details !== null && typeof details === "object" && Object.hasOwn(details, "payload") ? (details as { payload: unknown }).payload : details;
|
|
259
285
|
}
|
|
260
286
|
|
|
261
|
-
|
|
287
|
+
/** Notes the request monitors add to a result: scope first (it can require asking the user), then completion. */
|
|
288
|
+
function monitorNotes(monitors: RequestMonitors | undefined, tool: string, params: unknown, meta: { write?: boolean; effects?: string[] | null }, details: unknown): string[] {
|
|
289
|
+
if (!monitors) return [];
|
|
290
|
+
const payload = resultPayload(details);
|
|
291
|
+
return [monitors.scope.afterResult(payload), monitors.completion.afterCall(tool, params, meta, changedModel(payload))].filter((note): note is string => !!note);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** `guidelines` are this tool's own rules; rules shared by several tools live once in the platform section. */
|
|
295
|
+
function registerBridgeTool(pi: ExtensionAPI, descriptor: BridgeToolDescriptor, resolve: BridgeResolver, guidelines: string[],
|
|
296
|
+
monitors?: RequestMonitors) {
|
|
262
297
|
const timeoutMs = toolTimeoutMs(descriptor.name);
|
|
298
|
+
const schema = publicBridgeSchema(descriptor.parameters);
|
|
263
299
|
pi.registerTool({
|
|
264
300
|
name: descriptor.name,
|
|
265
301
|
label: descriptor.label ?? descriptor.name,
|
|
266
302
|
description: descriptor.description ?? `Revit bridge tool '${descriptor.name}'.`,
|
|
267
|
-
parameters: Type.Unsafe(
|
|
303
|
+
parameters: Type.Unsafe(schema as TSchema),
|
|
268
304
|
promptSnippet: descriptor.promptSnippet ?? undefined,
|
|
269
|
-
promptGuidelines:
|
|
305
|
+
promptGuidelines: guidelines,
|
|
270
306
|
executionMode: descriptor.executionMode === "parallel" ? "parallel" : "sequential",
|
|
271
307
|
async execute(_toolCallId, params, signal) {
|
|
272
|
-
|
|
308
|
+
const result = await runBridgeTool(descriptor.name, params, signal, timeoutMs, resolve);
|
|
309
|
+
// Result- and metadata-driven steering: objects that predate the request, and repeated re-verification after edits.
|
|
310
|
+
const notes = monitorNotes(monitors, descriptor.name, params, descriptor, result.details);
|
|
311
|
+
return notes.length ? { ...result, content: [...result.content, ...notes.map(text => ({ type: "text" as const, text }))] } : result;
|
|
273
312
|
},
|
|
274
313
|
});
|
|
275
314
|
}
|
|
@@ -352,17 +391,51 @@ async function announceUpdateOnce(notify: (message: string, level: "info") => vo
|
|
|
352
391
|
}
|
|
353
392
|
}
|
|
354
393
|
|
|
355
|
-
|
|
394
|
+
const packageRoot = fileURLToPath(new URL("../../", import.meta.url));
|
|
395
|
+
const nativeToolNames = new Set(documentedTools.filter(tool => tool.source === "native").map(tool => tool.name));
|
|
396
|
+
|
|
397
|
+
/** Branch and commit when the extension runs from a source checkout; null for an installed package. */
|
|
398
|
+
function sourceRevision(): { branch: string | null; commit: string | null } | null {
|
|
399
|
+
try {
|
|
400
|
+
const gitDir = path.join(packageRoot, ".git");
|
|
401
|
+
const head = readFileSync(path.join(gitDir, "HEAD"), "utf8").trim();
|
|
402
|
+
if (!head.startsWith("ref: ")) return { branch: null, commit: head };
|
|
403
|
+
const ref = head.slice(5);
|
|
404
|
+
let commit: string | null = null;
|
|
405
|
+
try { commit = readFileSync(path.join(gitDir, ref), "utf8").trim(); }
|
|
406
|
+
catch { commit = readFileSync(path.join(gitDir, "packed-refs"), "utf8").split(/\r?\n/).find(line => line.endsWith(` ${ref}`))?.split(" ")[0] ?? null; }
|
|
407
|
+
return { branch: ref.replace(/^refs\/heads\//, ""), commit };
|
|
408
|
+
} catch { return null; }
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** What is actually loaded: package, guidance revision, source revision and per-tool contract agreement. */
|
|
412
|
+
function loadedStatus(live: Map<string, string>) {
|
|
413
|
+
const matching: string[] = [], changed: string[] = [], undocumented: string[] = [];
|
|
414
|
+
for (const [name, hash] of live) {
|
|
415
|
+
const packaged = packagedContracts.get(name);
|
|
416
|
+
if (!packaged) undocumented.push(name);
|
|
417
|
+
else if (packaged.contract_hash === hash) matching.push(name);
|
|
418
|
+
else changed.push(name);
|
|
419
|
+
}
|
|
420
|
+
return {
|
|
421
|
+
extension_package: packageVersion, extension_root: packageRoot, documentation_revision: documentationRevision, source: sourceRevision(),
|
|
422
|
+
contracts: live.size ? { matching: matching.length, changed, undocumented,
|
|
423
|
+
packaged_not_advertised: [...packagedContracts.values()].filter(tool => tool.source === "bridge" && !live.has(tool.name)).map(tool => tool.name) }
|
|
424
|
+
: "no bridge catalogue discovered yet",
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function registerPing(pi: ExtensionAPI, resolve: BridgeResolver, onBridgeAlive?: () => Promise<"ready" | "registered" | "failed">, status?: () => unknown) {
|
|
356
429
|
pi.registerTool({
|
|
357
430
|
name: "ping",
|
|
358
431
|
label: "Ping Revit Bridge",
|
|
359
|
-
description: "Check that the Revit bridge is reachable and report the Revit version.",
|
|
432
|
+
description: "Check that the Revit bridge is reachable and report the Revit version, plus which PI-Revit extension package, guidance revision, source revision and tool contracts are loaded (changed or undocumented contracts mean manuals may not match the bridge).",
|
|
360
433
|
parameters: Type.Object({}),
|
|
361
434
|
promptSnippet: "Check Revit bridge availability.",
|
|
362
|
-
promptGuidelines: ["Use ping when Revit tools fail or bridge availability is unclear."],
|
|
435
|
+
promptGuidelines: ["Use ping when Revit tools fail or bridge availability is unclear; it also reports which PI-Revit package, guidance revision and contracts are loaded."],
|
|
363
436
|
executionMode: "sequential",
|
|
364
437
|
async execute(_toolCallId, _params, signal) {
|
|
365
|
-
const payload = await bridgeRequest("/ping", { method: "GET" }, signal, 10_000);
|
|
438
|
+
const payload = await bridgeRequest("/ping", { method: "GET", bridge: await resolve() }, signal, 10_000);
|
|
366
439
|
const warning = versionMismatch((payload as { addinVersion?: string }).addinVersion);
|
|
367
440
|
// The bridge is alive: if this session started before Revit and only has
|
|
368
441
|
// ping, register the bridge tools now and tell the model they arrived.
|
|
@@ -374,9 +447,11 @@ function registerPing(pi: ExtensionAPI, onBridgeAlive?: () => Promise<"ready" |
|
|
|
374
447
|
else if (state === "failed")
|
|
375
448
|
registrationNote = "\nNOTE: Bridge tool discovery failed even though ping succeeded; retry ping or restart pi.";
|
|
376
449
|
}
|
|
450
|
+
const loaded = status?.();
|
|
377
451
|
return {
|
|
378
|
-
content: [{ type: "text", text: JSON.stringify(payload) + (warning ? `\nWARNING: ${warning}` : "") + registrationNote
|
|
379
|
-
|
|
452
|
+
content: [{ type: "text", text: JSON.stringify(payload) + (warning ? `\nWARNING: ${warning}` : "") + registrationNote
|
|
453
|
+
+ (loaded ? `\nPI-Revit loaded: ${JSON.stringify(loaded)}` : "") }],
|
|
454
|
+
details: loaded ? { ...(payload as object), pi_revit: loaded } : payload,
|
|
380
455
|
};
|
|
381
456
|
},
|
|
382
457
|
});
|
|
@@ -385,35 +460,92 @@ function registerPing(pi: ExtensionAPI, onBridgeAlive?: () => Promise<"ready" |
|
|
|
385
460
|
const REDISCOVERY_INTERVAL_MS = 15_000;
|
|
386
461
|
|
|
387
462
|
export default async function revitConnector(pi: ExtensionAPI) {
|
|
463
|
+
const bridgeVersions = new Map<string, string | null>();
|
|
464
|
+
const bridgeKey = (info: BridgeInfo) => info.bridgeId ?? `${info.baseUrl}\0${info.token}`;
|
|
465
|
+
const instances = createInstanceRouter(readBridgeInfo, async info => {
|
|
466
|
+
const payload = await bridgeRequest("/ping", { method: "GET", bridge: info }, undefined, 2000) as Record<string, unknown>;
|
|
467
|
+
bridgeVersions.set(bridgeKey(info), typeof payload.addinVersion === "string" ? payload.addinVersion : null);
|
|
468
|
+
return payload;
|
|
469
|
+
});
|
|
388
470
|
registerResultReader(pi);
|
|
471
|
+
registerOperationReader(pi, instances.resolve);
|
|
472
|
+
const monitors: RequestMonitors = { completion: createCompletionMonitor(), scope: createScopeMonitor() };
|
|
473
|
+
registerScriptLibrary(pi, async (args, signal, prepared) => {
|
|
474
|
+
const result = await runBridgeTool("execute_csharp", args, signal, LONG_TIMEOUT_MS, instances.resolve, prepared);
|
|
475
|
+
const notes = monitorNotes(monitors, "manage_revit_scripts", args, { write: true }, result.details);
|
|
476
|
+
return notes.length ? { ...result, content: [...result.content, ...notes.map(text => ({ type: "text" as const, text }))] } : result;
|
|
477
|
+
},
|
|
478
|
+
async value => ({ content: await modelContent("manage_revit_scripts", { details: { payload: value } }), details: value }));
|
|
479
|
+
pi.registerTool({
|
|
480
|
+
name: "manage_revit_instances", label: "Manage Revit Instances",
|
|
481
|
+
description: "List reachable local Revit bridge sessions or select one for this Pi session. The first sole instance is bound automatically; multiple instances require explicit selection before model calls. After that session closes or restarts, select its new bridge_id: calls never fall back to another session. Selection refreshes the tool catalogue. Operation receipt lookups and identical retries use their original session. Read get_model_overview again after switching; document IDs are session-specific.",
|
|
482
|
+
parameters: Type.Object({ action: Type.Optional(Type.Union([Type.Literal("list"), Type.Literal("select")])), bridge_id: Type.Optional(Type.String()) }),
|
|
483
|
+
executionMode: "sequential",
|
|
484
|
+
async execute(_id, args) {
|
|
485
|
+
let result: unknown;
|
|
486
|
+
if ((args.action ?? "list") === "list") result = { instances: await instances.list() };
|
|
487
|
+
else if (args.action === "select" && args.bridge_id) {
|
|
488
|
+
// Drain discovery for the previous target before changing selection.
|
|
489
|
+
if (discoveryInFlight) await discoveryInFlight;
|
|
490
|
+
const selection = await instances.select(args.bridge_id);
|
|
491
|
+
// A retry timer may have started another discovery while selection probed.
|
|
492
|
+
// Drain that request too, then reset synchronously before fetching anew.
|
|
493
|
+
if (discoveryInFlight) await discoveryInFlight;
|
|
494
|
+
bridgeToolsRegistered = false;
|
|
495
|
+
catalog.reset();
|
|
496
|
+
const ready = await discoverAndRegister();
|
|
497
|
+
result = { ...selection, tool_catalog_ready: ready };
|
|
498
|
+
if (!ready && sessionActive) startRetry();
|
|
499
|
+
} else throw new Error("select requires bridge_id from the instance list.");
|
|
500
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }], details: result };
|
|
501
|
+
},
|
|
502
|
+
});
|
|
389
503
|
// Self-healing discovery: when pi starts before Revit is ready, the initial
|
|
390
|
-
// GET /tools fails and only
|
|
504
|
+
// GET /tools fails and only native utilities are registered. Rather than requiring a
|
|
391
505
|
// fresh pi start (/reload does not reliably re-run async registration), a
|
|
392
506
|
// background retry keeps probing until the bridge appears, and a successful
|
|
393
507
|
// ping also triggers an immediate attempt.
|
|
394
508
|
let bridgeToolsRegistered = false;
|
|
509
|
+
let sharedRules: string[] = [];
|
|
395
510
|
let discoveryInFlight: Promise<boolean> | null = null;
|
|
511
|
+
let sessionActive = false;
|
|
512
|
+
let disposed = false;
|
|
513
|
+
let timer: ReturnType<typeof setInterval> | undefined;
|
|
514
|
+
const catalog = createToolCatalog(pi, discoverAndRegister);
|
|
396
515
|
|
|
397
516
|
async function discoverAndRegister(): Promise<boolean> {
|
|
517
|
+
if (disposed) return false;
|
|
398
518
|
if (bridgeToolsRegistered) return true;
|
|
399
519
|
if (discoveryInFlight) return discoveryInFlight;
|
|
400
520
|
discoveryInFlight = (async () => {
|
|
401
521
|
try {
|
|
402
|
-
const
|
|
522
|
+
const selectedBridge = await instances.resolve();
|
|
523
|
+
const payload = (await bridgeRequest("/tools", { method: "GET", bridge: selectedBridge }, undefined, DISCOVERY_TIMEOUT_MS)) as {
|
|
403
524
|
tools?: BridgeToolDescriptor[];
|
|
404
525
|
};
|
|
405
526
|
const descriptors = Array.isArray(payload?.tools) ? payload.tools : [];
|
|
406
|
-
if (
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
527
|
+
if (disposed || !Array.isArray(payload?.tools)) return false;
|
|
528
|
+
catalog.setBridgeVersion(bridgeVersions.get(bridgeKey(selectedBridge)) ?? null);
|
|
529
|
+
// Native tool names are reserved: a bridge cannot replace an extension utility.
|
|
530
|
+
const valid = descriptors.filter(descriptor => descriptor && typeof descriptor.name === "string" && descriptor.name
|
|
531
|
+
&& !nativeToolNames.has(descriptor.name));
|
|
532
|
+
const { shared, perTool } = hoistSharedGuidelines(valid);
|
|
533
|
+
sharedRules = shared;
|
|
534
|
+
const added: string[] = [];
|
|
535
|
+
for (const descriptor of valid) {
|
|
536
|
+
registerBridgeTool(pi, descriptor, instances.resolve, perTool.get(descriptor.name) ?? [], monitors);
|
|
537
|
+
catalog.add(descriptor);
|
|
538
|
+
added.push(descriptor.name);
|
|
539
|
+
}
|
|
540
|
+
if (sessionActive) {
|
|
541
|
+
catalog.hideAdvanced(added);
|
|
542
|
+
pi.setActiveTools([...new Set([...pi.getActiveTools(), ...descriptors.filter(d => added.includes(d.name) && d.tier !== "advanced").map(d => d.name)])]);
|
|
411
543
|
}
|
|
412
544
|
bridgeToolsRegistered = true;
|
|
413
545
|
return true;
|
|
414
546
|
} catch {
|
|
415
547
|
// Bridge down (Revit closed, still starting, stale bridge.json):
|
|
416
|
-
//
|
|
548
|
+
// keep native utilities available and try again later.
|
|
417
549
|
return false;
|
|
418
550
|
} finally {
|
|
419
551
|
discoveryInFlight = null;
|
|
@@ -426,18 +558,30 @@ export default async function revitConnector(pi: ExtensionAPI) {
|
|
|
426
558
|
// bridge is down, so it is never part of /tools discovery. A successful
|
|
427
559
|
// ping doubles as a re-discovery trigger — the natural first call in a
|
|
428
560
|
// session that finds itself without bridge tools.
|
|
429
|
-
registerPing(pi, async () => {
|
|
561
|
+
registerPing(pi, instances.resolve, async () => {
|
|
430
562
|
if (bridgeToolsRegistered) return "ready";
|
|
431
563
|
return (await discoverAndRegister()) ? "registered" : "failed";
|
|
564
|
+
}, () => loadedStatus(catalog.liveContracts()));
|
|
565
|
+
|
|
566
|
+
// One always-visible platform section per run: global protocols plus rules shared
|
|
567
|
+
// by several tools. It does not depend on the skill being read or on tool count.
|
|
568
|
+
pi.on("before_agent_start", async event => {
|
|
569
|
+
monitors.completion.reset();
|
|
570
|
+
monitors.scope.reset((event as { prompt?: string }).prompt ?? "");
|
|
571
|
+
const options = (event as { systemPromptOptions?: { sections?: Record<string, string> } }).systemPromptOptions;
|
|
572
|
+
if (options?.sections) options.sections.pi_revit = buildPlatformSection({ manualDirectory, skillRoot, sharedRules });
|
|
432
573
|
});
|
|
433
574
|
|
|
434
575
|
// Surface an incomplete update (see versionMismatch) once per session, right
|
|
435
576
|
// where the user lands after running `pi update --extensions`. Bridge down at
|
|
436
577
|
// session start is the normal Revit-closed case: stay quiet.
|
|
437
578
|
pi.on("session_start", async (_event, ctx) => {
|
|
579
|
+
sessionActive = true;
|
|
580
|
+
catalog.hideAdvanced();
|
|
581
|
+
if (!bridgeToolsRegistered) startRetry();
|
|
438
582
|
await announceUpdateOnce((message, level) => ctx.ui.notify(message, level));
|
|
439
583
|
try {
|
|
440
|
-
const payload = (await bridgeRequest("/ping", { method: "GET" }, undefined, 3_000)) as { addinVersion?: string };
|
|
584
|
+
const payload = (await bridgeRequest("/ping", { method: "GET", bridge: await instances.resolve() }, undefined, 3_000)) as { addinVersion?: string };
|
|
441
585
|
const warning = versionMismatch(payload.addinVersion);
|
|
442
586
|
if (warning) ctx.ui.notify(warning, "warning");
|
|
443
587
|
} catch {
|
|
@@ -445,33 +589,35 @@ export default async function revitConnector(pi: ExtensionAPI) {
|
|
|
445
589
|
}
|
|
446
590
|
});
|
|
447
591
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
592
|
+
pi.on("session_shutdown", async () => {
|
|
593
|
+
disposed = true;
|
|
594
|
+
sessionActive = false;
|
|
595
|
+
if (timer) clearInterval(timer);
|
|
596
|
+
timer = undefined;
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
function startRetry() {
|
|
600
|
+
if (timer || disposed) return;
|
|
601
|
+
timer = setInterval(async () => {
|
|
602
|
+
if (!(await discoverAndRegister())) return;
|
|
603
|
+
if (timer) clearInterval(timer);
|
|
604
|
+
timer = undefined;
|
|
605
|
+
if (disposed) return;
|
|
606
|
+
// Refresh the model's knowledge on its next turn without interrupting the user.
|
|
607
|
+
try {
|
|
608
|
+
pi.sendMessage(
|
|
609
|
+
{
|
|
610
|
+
customType: "pi-revit",
|
|
611
|
+
content: "Revit is now reachable. Core bridge tools are available; use find_revit_tools to activate specialist tools.",
|
|
612
|
+
display: true,
|
|
613
|
+
},
|
|
614
|
+
{ deliverAs: "nextTurn" },
|
|
615
|
+
);
|
|
616
|
+
} catch {
|
|
617
|
+
// Tool registration remains valid if the session cannot accept a message.
|
|
618
|
+
}
|
|
619
|
+
}, REDISCOVERY_INTERVAL_MS);
|
|
620
|
+
timer.unref?.();
|
|
621
|
+
}
|
|
622
|
+
await discoverAndRegister();
|
|
477
623
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
|
|
6
|
+
export interface BridgeInfo {
|
|
7
|
+
baseUrl: string;
|
|
8
|
+
token: string;
|
|
9
|
+
pid?: number;
|
|
10
|
+
revitVersion?: string;
|
|
11
|
+
bridgeId?: string;
|
|
12
|
+
supportsOperationTracking?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Selection belongs to one extension instance. A selected session never silently falls back. */
|
|
16
|
+
export function createInstanceRouter(readLegacy: () => Promise<BridgeInfo>, probe: (info: BridgeInfo) => Promise<Record<string, unknown>>) {
|
|
17
|
+
let selected: string | undefined;
|
|
18
|
+
// Older bridges have no generation ID. Derive a stable opaque selector from
|
|
19
|
+
// their per-start credentials without exposing the credential itself.
|
|
20
|
+
const identity = (info: BridgeInfo) => info.bridgeId ?? createHash("sha256").update(info.baseUrl + "\0" + info.token).digest("hex").slice(0, 32);
|
|
21
|
+
const directory = () => path.join(process.env.APPDATA ?? path.join(os.homedir(), "AppData", "Roaming"), "RevitBridge", "instances");
|
|
22
|
+
async function candidates(): Promise<BridgeInfo[]> {
|
|
23
|
+
const entries = new Map<string, BridgeInfo>();
|
|
24
|
+
let files: string[] = [];
|
|
25
|
+
try { files = (await readdir(directory())).filter(name => /^[0-9a-f]{32}\.json$/.test(name)); } catch { }
|
|
26
|
+
// Old crash records may accumulate; probe only records whose process still exists.
|
|
27
|
+
for (const file of files) {
|
|
28
|
+
try {
|
|
29
|
+
const info = JSON.parse(await readFile(path.join(directory(), file), "utf8")) as BridgeInfo;
|
|
30
|
+
if (info.bridgeId !== file.slice(0, -5) || !info.baseUrl || !info.token || !Number.isSafeInteger(info.pid)) continue;
|
|
31
|
+
try { process.kill(info.pid!, 0); } catch { continue; }
|
|
32
|
+
entries.set(info.bridgeId, info);
|
|
33
|
+
} catch { }
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const legacy = await readLegacy();
|
|
37
|
+
entries.set(identity(legacy), legacy);
|
|
38
|
+
} catch { }
|
|
39
|
+
return [...entries.values()];
|
|
40
|
+
}
|
|
41
|
+
async function live() {
|
|
42
|
+
const found = await Promise.all((await candidates()).map(async info => {
|
|
43
|
+
try {
|
|
44
|
+
const ping = await probe(info);
|
|
45
|
+
if (info.bridgeId && ping.bridgeId !== info.bridgeId) return null;
|
|
46
|
+
return { info, ping };
|
|
47
|
+
} catch { return null; }
|
|
48
|
+
}));
|
|
49
|
+
return found.filter((entry): entry is NonNullable<typeof entry> => entry !== null);
|
|
50
|
+
}
|
|
51
|
+
async function resolve(operationId?: string): Promise<BridgeInfo> {
|
|
52
|
+
const target = operationId?.split(":")[0] ?? selected;
|
|
53
|
+
if (target) {
|
|
54
|
+
const info = (await candidates()).find(entry => identity(entry) === target);
|
|
55
|
+
if (!info) throw new Error("The original or selected Revit bridge session is unavailable. Its outcome is unknown here; no action was sent to another instance. Use manage_revit_instances to select an available session.");
|
|
56
|
+
return info;
|
|
57
|
+
}
|
|
58
|
+
const entries = await candidates();
|
|
59
|
+
// Bind only a verified live session; a crash record must not prevent
|
|
60
|
+
// startup discovery from recovering when Revit is launched later.
|
|
61
|
+
if (entries.length === 1) {
|
|
62
|
+
const ping = await probe(entries[0]);
|
|
63
|
+
if (entries[0].bridgeId && ping.bridgeId !== entries[0].bridgeId)
|
|
64
|
+
throw new Error("Revit discovery points to a different bridge generation. Refresh the instance list.");
|
|
65
|
+
selected = identity(entries[0]); return entries[0];
|
|
66
|
+
}
|
|
67
|
+
const available = await live();
|
|
68
|
+
if (available.length > 1) throw new Error("Several Revit instances are open. Use manage_revit_instances to list and select the intended bridge_id before calling model tools.");
|
|
69
|
+
if (available.length === 1) { selected = identity(available[0].info); return available[0].info; }
|
|
70
|
+
return readLegacy();
|
|
71
|
+
}
|
|
72
|
+
async function list() {
|
|
73
|
+
return (await live()).map(({ info, ping }) => ({ bridge_id: identity(info), pid: info.pid ?? null,
|
|
74
|
+
revit_version: info.revitVersion ?? null, addin_version: ping.addinVersion ?? null,
|
|
75
|
+
selected: selected === identity(info), supports_operation_tracking: info.supportsOperationTracking === true }));
|
|
76
|
+
}
|
|
77
|
+
async function select(id: string) {
|
|
78
|
+
if (!/^[0-9a-f]{32}$/.test(id)) throw new Error("bridge_id must be an exact session ID from manage_revit_instances.");
|
|
79
|
+
const match = (await live()).find(entry => identity(entry.info) === id);
|
|
80
|
+
if (!match) throw new Error("That bridge session is no longer reachable; selection was unchanged.");
|
|
81
|
+
selected = id;
|
|
82
|
+
return { bridge_id: id, pid: match.info.pid, addin_version: match.ping.addinVersion,
|
|
83
|
+
instructions: "Selection applies to this Pi extension session. Read get_model_overview for a fresh exact document identity before editing. Operation receipts and identical retries are routed to their original bridge session." };
|
|
84
|
+
}
|
|
85
|
+
return { resolve, list, select };
|
|
86
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The PI-Revit platform section: cross-cutting protocols stated exactly once, always in
|
|
5
|
+
* context (injected through before_agent_start), independent of whether the skill is read
|
|
6
|
+
* and of how many tools exist. Tool-specific facts stay in each tool's own guidelines.
|
|
7
|
+
*/
|
|
8
|
+
export const PLATFORM_PROTOCOL = [
|
|
9
|
+
"PI-Revit protocol. It applies to every Revit tool, present or future, whether or not the pi-revit skill has been read.",
|
|
10
|
+
"1. Capability: before saying PI-Revit can or cannot do something, check. find_revit_tools (scope=documentation also works while Revit is closed) returns matching tools, their declared limits with alternatives, and related workflows. If a tool does not cover the request, follow its declared alternative. If nothing dedicated fits, verify all needed API members with search_api_docs, up to 10 names per call separated by ';' and use execute_csharp within the requested scope. Say an operation is not possible only after that check, and name what you checked; distinguish \"no dedicated tool\", \"the Revit API does not offer it\" and \"needs the user\".",
|
|
11
|
+
"2. Scope and completion: explanations do not change the model and inspections do not repair it. For a change, list the request's explicit requirements, do only those, and verify each with the tool's declared verification method. Then stop and report what changed, how it was verified and anything unmet. Offer further improvements as suggestions instead of making them. Never hide or remove content the request asks to show. An object made from an existing one inherits its state: results report it as inherited_state, and model_changes lists what each call added, modified or deleted, with the visibility of new views. Check that state against the request and say what you derived from.",
|
|
12
|
+
"3. Existing objects: objects that existed before the request are not yours. If one already has a name the request asks you to create, or a creation is rejected as a name collision, do not edit, reuse, replace or delete it: ask the user, or use a distinct name and report the collision.",
|
|
13
|
+
"4. Evidence: base claims about capabilities and model state on tool results, manuals or API documentation checked in this session, and say which.",
|
|
14
|
+
"5. Identity: for model changes, previews, exports, view activation and selection changes, pass project.documentId from get_model_overview unchanged as expected_document_id; project.documentKind says whether it is a project or a family, and tools refuse kinds they do not declare. Refresh it after reopening, restarting or switching instances. Save As keeps the ID but changes which file a later save affects.",
|
|
15
|
+
"6. Language: the user may write in any language, and the model's Revit UI may be localized. Reply in the user's language. Search tools and API documentation with English terms. Read localized category and parameter names from tool results, and prefer exact identities (BuiltInParameter names, guid:<GUID>) over translated display names.",
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
export function buildPlatformSection(options: { manualDirectory: string; skillRoot: string; sharedRules: readonly string[] }): string {
|
|
19
|
+
const lines = [
|
|
20
|
+
...PLATFORM_PROTOCOL,
|
|
21
|
+
`7. Guidance: each tool's manual is ${path.join(options.manualDirectory, "<tool_name>.md")}; read only the manual of a tool you will use. find_revit_tools returns exact paths and whether a manual matches the selected bridge's contract. Shared rules: ${path.join(options.skillRoot, "references", "execution-rules.md")}. Uncertain outcomes: ${path.join(options.skillRoot, "references", "operation-recovery.md")}.`,
|
|
22
|
+
];
|
|
23
|
+
if (options.sharedRules.length) lines.push("Rules shared by several Revit tools:", ...options.sharedRules.map(rule => `- ${rule}`));
|
|
24
|
+
return lines.join("\n");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Split bridge guidelines into per-tool rules and rules shared by two or more tools.
|
|
29
|
+
* A tool's own name is normalized away first, so "set_parameters: use X" and
|
|
30
|
+
* "open_view: use X" are recognized as one shared rule. Works for any bridge version.
|
|
31
|
+
*/
|
|
32
|
+
export function hoistSharedGuidelines(descriptors: readonly { name: string; promptGuidelines?: string[] | null }[]) {
|
|
33
|
+
const normalized = (name: string, rule: string) => rule.split(name).join("{tool}").replace(/^\{tool\}:\s*/, "").trim();
|
|
34
|
+
const counts = new Map<string, number>();
|
|
35
|
+
for (const d of descriptors) for (const rule of new Set((d.promptGuidelines ?? []).map(r => normalized(d.name, r)))) counts.set(rule, (counts.get(rule) ?? 0) + 1);
|
|
36
|
+
const shared = [...counts].filter(([, count]) => count >= 2).map(([rule]) => rule);
|
|
37
|
+
const sharedSet = new Set(shared);
|
|
38
|
+
const perTool = new Map(descriptors.map(d => [d.name, (d.promptGuidelines ?? []).filter(rule => !sharedSet.has(normalized(d.name, rule)))]));
|
|
39
|
+
return { shared: shared.map(rule => rule.replaceAll("{tool}", "the tool")), perTool };
|
|
40
|
+
}
|