@snaptrude/plugin-client 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,42 @@
1
1
  # @snaptrude/plugin-client
2
2
 
3
+ ## 0.7.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 8f22cb7: Expand `presentation.aiInspiration` to the full AI Inspiration surface (18 methods) and make it a required member of `PluginPresentationApi` now that the host implementation lands.
8
+ - New methods: `getModelCapabilities`, `getSelection`, `getPresetCatalog`, `getJob`, `cancelJob`, `listJobs`, `getWorkflowForShape`, `getSelectedBranchRerunPlan`, `rerunSelectedBranch`, `extractRecipeFromShape`, `listRecipes`, `saveRecipe`, `deleteRecipe`, `runRecipe` (plus job/workflow/recipe Zod schemas).
9
+ - `generate`/`refine` (and `rerunSelectedBranch`/`runRecipe`) accept `runMode: "blocking" | "job"`; `PluginAIInspirationRunResult` is accordingly loosened to `{ jobId?, output?, outputs?, metadata? }` — job-mode calls resolve with a `jobId` instead of outputs.
10
+
11
+ - Updated dependencies [8f22cb7]
12
+ - @snaptrude/plugin-core@0.7.1
13
+
14
+ ## 0.7.0
15
+
16
+ ### Minor Changes
17
+
18
+ - e1915e4: Plugin API 0.7.0 — new families and methods (supersedes the cancelled `design.update.setLabel` 0.6.1 patch, now folded in).
19
+ - **`core.proposals.*`** — design options as infrastructure: `list` / `get` / `getActive` / `listForComponent` / `isActive` (reads) plus `create` / `rename` / `setActive` / `delete` (non-undoable writes).
20
+ - **`program.layout.*`** — the space-solver promoted to a real async-job family: `arrange` / `pack` now start a backend job and return immediately, with `getState` (poll) and `cancel`.
21
+ - **Door & window catalogs + placement** — `design.doors.*` / `design.windows.*` gain `listCatalogGroups` / `listCatalog` / `getCatalogItem` / `exists` (shared catalog DTOs), and `design.create.door` / `design.create.window` place a catalog item into a host wall.
22
+ - **`design.create.smartLayout`** — clone an in-scene template cluster into one or more target spaces in a single undo batch.
23
+ - **Furniture categories** — `design.furniture.listCategories` plus a `category` filter on `listCatalog`, and `thumbnailUrl` on catalog items.
24
+ - **`design.transform.align`** — snap components' bounding-box edges (left/right/center/top/bottom/middle), optionally to a reference.
25
+ - **Per-face materials** — `design.materials.applyToFaces` / `resetFaces` / `getByFace` / `listByFace` (BREP faces by durable index, visual-only).
26
+ - **Staircase params** — `design.query.getStaircaseParams` (read) and `design.update.staircase` (sparse parametric write).
27
+ - **Footprint-area lock** — `design.lockArea` / `unlockArea` / `isAreaLocked` / `listAreaLocked` for Room/Department spaces.
28
+ - **`entity.story.duplicate`** — duplicate a storey (or a subset) up/down, auto-creating the target storey.
29
+ - **`presentation.import.svg`** — import a guaranteed-vector SVG onto the Present sheet.
30
+ - **View display settings** — `presentation.views.getSettings` / `updateSettings` (background, color mode, axis, edges, labels).
31
+ - **`design.update.setLabel`** — set any component's panel Label (write pair of `design.query.getLabel`); undoable, lock-aware, validated.
32
+ - **`program.areas`** — area members now report their `areaClass` (`NET` / `GROSS` / `EXCLUDED`), with net/gross documented as the plain-English synonyms for carpet/built-up.
33
+ - **Typed plugin API errors** — every host failure now crosses the wire as a structured envelope instead of a flattened `Execution error: <msg>` string: a stable `code` from the shared plugin-core error registry, the offending handle, an authored `hint`, and `retryAfterMs` / `didYouMean` enrichment where applicable. Zod validation failures forward the first 10 issues, each projected to `{path, code, message}` (full count in `details.issueCount`); internal faults are sanitized and host-logged under an `errorId`. Every handler across the API surface now throws typed codes, each doc page gained an Errors section, and a drift test pins thrown code literals to the registry.
34
+
35
+ ### Patch Changes
36
+
37
+ - Updated dependencies [e1915e4]
38
+ - @snaptrude/plugin-core@0.7.0
39
+
3
40
  ## 0.6.0
4
41
 
5
42
  ### Minor Changes
@@ -7,5 +7,7 @@ export interface HostApiWrapped {
7
7
  call<M extends PluginApiMethod>(payload: PluginApiCallPayload<M>): Promise<PluginApiCallResult<M>>;
8
8
  }
9
9
  export declare function createHostApi(endpoint?: Comlink.Endpoint): HostApi;
10
+ /** TEST SEAM ONLY: replace the Comlink host instance (pass `undefined` to reset). */
11
+ export declare function __setHostApiInstance(instance?: HostApi): void;
10
12
  export declare function getHostApi(): HostApiWrapped;
11
13
  //# sourceMappingURL=host-api.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"host-api.d.ts","sourceRoot":"","sources":["../src/host-api.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,OAAO,MAAM,SAAS,CAAA;AAClC,OAAO,KAAK,EACV,eAAe,EACf,oBAAoB,EACpB,0BAA0B,EAC1B,mBAAmB,EACpB,MAAM,wBAAwB,CAAA;AAE/B,MAAM,WAAW,OAAO;IACtB,IAAI,CAAC,CAAC,SAAS,eAAe,EAC5B,OAAO,EAAE,oBAAoB,CAAC,CAAC,CAAC,GAC/B,OAAO,CAAC,0BAA0B,CAAC,CAAC,CAAC,CAAC,CAAA;CAC1C;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,CAAC,CAAC,SAAS,eAAe,EAC5B,OAAO,EAAE,oBAAoB,CAAC,CAAC,CAAC,GAC/B,OAAO,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAAA;CACnC;AAED,wBAAgB,aAAa,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,QAAQ,GAAG,OAAO,CAIlE;AAID,wBAAgB,UAAU,IAAI,cAAc,CAkB3C"}
1
+ {"version":3,"file":"host-api.d.ts","sourceRoot":"","sources":["../src/host-api.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,OAAO,MAAM,SAAS,CAAA;AAClC,OAAO,KAAK,EACV,eAAe,EACf,oBAAoB,EACpB,0BAA0B,EAC1B,mBAAmB,EACpB,MAAM,wBAAwB,CAAA;AAG/B,MAAM,WAAW,OAAO;IACtB,IAAI,CAAC,CAAC,SAAS,eAAe,EAC5B,OAAO,EAAE,oBAAoB,CAAC,CAAC,CAAC,GAC/B,OAAO,CAAC,0BAA0B,CAAC,CAAC,CAAC,CAAC,CAAA;CAC1C;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,CAAC,CAAC,SAAS,eAAe,EAC5B,OAAO,EAAE,oBAAoB,CAAC,CAAC,CAAC,GAC/B,OAAO,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAAA;CACnC;AAED,wBAAgB,aAAa,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,QAAQ,GAAG,OAAO,CAIlE;AAID,qFAAqF;AACrF,wBAAgB,oBAAoB,CAAC,QAAQ,CAAC,EAAE,OAAO,GAAG,IAAI,CAE7D;AAED,wBAAgB,UAAU,IAAI,cAAc,CAyC3C"}
package/dist/index.cjs CHANGED
@@ -30,25 +30,46 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ CODE_META: () => import_plugin_core3.CODE_META,
33
34
  ClientPluginApi: () => ClientPluginApi,
35
+ PLUGIN_ERROR_CODES: () => import_plugin_core3.PLUGIN_ERROR_CODES,
36
+ PluginError: () => import_plugin_core3.PluginError,
37
+ PluginExecutionError: () => import_plugin_core3.PluginExecutionError,
38
+ PluginHandleError: () => import_plugin_core3.PluginHandleError,
39
+ PluginInternalError: () => import_plugin_core3.PluginInternalError,
40
+ PluginLifecycleError: () => import_plugin_core3.PluginLifecycleError,
41
+ PluginNotFoundError: () => import_plugin_core3.PluginNotFoundError,
42
+ PluginPermissionError: () => import_plugin_core3.PluginPermissionError,
43
+ PluginQuotaError: () => import_plugin_core3.PluginQuotaError,
44
+ PluginTimeoutError: () => import_plugin_core3.PluginTimeoutError,
45
+ PluginTransportError: () => import_plugin_core3.PluginTransportError,
46
+ PluginValidationError: () => import_plugin_core3.PluginValidationError,
34
47
  PluginWorker: () => PluginWorker,
48
+ __setHostApiInstance: () => __setHostApiInstance,
35
49
  createHostApi: () => createHostApi,
50
+ fromEnvelope: () => import_plugin_core3.fromEnvelope,
36
51
  getHostApi: () => getHostApi,
52
+ isErrorEnvelope: () => import_plugin_core3.isErrorEnvelope,
53
+ isPluginErrorCode: () => import_plugin_core3.isPluginErrorCode,
37
54
  snaptrude: () => snaptrude
38
55
  });
39
56
  module.exports = __toCommonJS(index_exports);
40
57
 
41
58
  // src/api/index.ts
42
- var import_plugin_core = require("@snaptrude/plugin-core");
59
+ var import_plugin_core2 = require("@snaptrude/plugin-core");
43
60
 
44
61
  // src/host-api.ts
45
62
  var Comlink = __toESM(require("comlink"), 1);
63
+ var import_plugin_core = require("@snaptrude/plugin-core");
46
64
  function createHostApi(endpoint) {
47
65
  return Comlink.wrap(
48
66
  endpoint ?? globalThis
49
67
  );
50
68
  }
51
69
  var _instance = null;
70
+ function __setHostApiInstance(instance) {
71
+ _instance = instance ?? null;
72
+ }
52
73
  function getHostApi() {
53
74
  if (!_instance) {
54
75
  _instance = createHostApi();
@@ -58,16 +79,35 @@ function getHostApi() {
58
79
  if (!_instance) {
59
80
  throw new Error("Host API not initialized");
60
81
  }
61
- return _instance.call(payload).then((result) => {
62
- if (result.success) {
63
- return result.data;
64
- } else {
65
- throw new Error(result.error);
66
- }
67
- });
82
+ let result;
83
+ try {
84
+ result = await _instance.call(payload);
85
+ } catch (transportErr) {
86
+ if (import_plugin_core.PluginError.is(transportErr)) throw transportErr;
87
+ throw rehydrate(
88
+ (0, import_plugin_core.makeClientEnvelope)(
89
+ "TRANSPORT_LOST",
90
+ transportErr instanceof Error ? transportErr.message : String(transportErr),
91
+ { methodPath: payload.method }
92
+ )
93
+ );
94
+ }
95
+ if (result.success) {
96
+ return result.data;
97
+ }
98
+ throw rehydrate(
99
+ result.errorInfo ?? (0, import_plugin_core.makeClientEnvelope)("UNKNOWN", result.error ?? "Unknown host error", {
100
+ methodPath: payload.method
101
+ })
102
+ );
68
103
  }
69
104
  };
70
105
  }
106
+ function rehydrate(envelope) {
107
+ const error = (0, import_plugin_core.fromEnvelope)(envelope);
108
+ Error.captureStackTrace?.(error, rehydrate);
109
+ return error;
110
+ }
71
111
 
72
112
  // src/rpc-proxy.ts
73
113
  function createRpcNamespace(basePath) {
@@ -90,7 +130,7 @@ var NOOP = () => {
90
130
  };
91
131
 
92
132
  // src/api/index.ts
93
- var ClientPluginApi = class _ClientPluginApi extends import_plugin_core.PluginApi {
133
+ var ClientPluginApi = class _ClientPluginApi extends import_plugin_core2.PluginApi {
94
134
  constructor() {
95
135
  super();
96
136
  this.core = createRpcNamespace("core");
@@ -167,13 +207,31 @@ var PluginWorker = class {
167
207
  };
168
208
 
169
209
  // src/index.ts
210
+ var import_plugin_core3 = require("@snaptrude/plugin-core");
170
211
  var snaptrude = ClientPluginApi.getInstance();
171
212
  // Annotate the CommonJS export names for ESM import in node:
172
213
  0 && (module.exports = {
214
+ CODE_META,
173
215
  ClientPluginApi,
216
+ PLUGIN_ERROR_CODES,
217
+ PluginError,
218
+ PluginExecutionError,
219
+ PluginHandleError,
220
+ PluginInternalError,
221
+ PluginLifecycleError,
222
+ PluginNotFoundError,
223
+ PluginPermissionError,
224
+ PluginQuotaError,
225
+ PluginTimeoutError,
226
+ PluginTransportError,
227
+ PluginValidationError,
174
228
  PluginWorker,
229
+ __setHostApiInstance,
175
230
  createHostApi,
231
+ fromEnvelope,
176
232
  getHostApi,
233
+ isErrorEnvelope,
234
+ isPluginErrorCode,
177
235
  snaptrude
178
236
  });
179
237
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/api/index.ts","../src/host-api.ts","../src/rpc-proxy.ts","../src/plugin-worker.ts"],"sourcesContent":["import { ClientPluginApi } from \"./api\"\n\nexport * from \"./api\"\nexport * from \"./host-api\"\nexport * from \"./plugin-worker\"\n\n/**\n * The Snaptrude plugin client API.\n *\n * The main entry point for plugins to interact with the Snaptrude platform.\n */\nexport const snaptrude = ClientPluginApi.getInstance()\n","import {\n PluginApi,\n PluginCoreApi,\n PluginDesignApi,\n PluginEntityApi,\n PluginProgramApi,\n PluginPresentationApi,\n PluginAnalysisApi,\n} from \"@snaptrude/plugin-core\"\nimport { createRpcNamespace } from \"../rpc-proxy\"\n\nexport class ClientPluginApi extends PluginApi {\n private static instance: ClientPluginApi\n\n /**\n * Every namespace is fully remote under the all-handle model: math/geom now\n * cross to the host (values are opaque handles), so there is no in-worker\n * compute left. All dispatch through a single generic RPC Proxy. Units live\n * under `core.units`, so they ride the `core` proxy.\n */\n public core: PluginCoreApi\n public design: PluginDesignApi\n public entity: PluginEntityApi\n public program: PluginProgramApi\n public presentation: PluginPresentationApi\n public analysis: PluginAnalysisApi\n\n private constructor() {\n super()\n this.core = createRpcNamespace<PluginCoreApi>(\"core\")\n this.design = createRpcNamespace<PluginDesignApi>(\"design\")\n this.entity = createRpcNamespace<PluginEntityApi>(\"entity\")\n this.program = createRpcNamespace<PluginProgramApi>(\"program\")\n this.presentation =\n createRpcNamespace<PluginPresentationApi>(\"presentation\")\n this.analysis = createRpcNamespace<PluginAnalysisApi>(\"analysis\")\n }\n\n static getInstance(): ClientPluginApi {\n if (!ClientPluginApi.instance) {\n ClientPluginApi.instance = new ClientPluginApi()\n }\n return ClientPluginApi.instance\n }\n}\n","import * as Comlink from \"comlink\"\nimport type {\n PluginApiMethod,\n PluginApiCallPayload,\n PluginApiCallWrappedResult,\n PluginApiCallResult,\n} from \"@snaptrude/plugin-core\"\n\nexport interface HostApi {\n call<M extends PluginApiMethod>(\n payload: PluginApiCallPayload<M>\n ): Promise<PluginApiCallWrappedResult<M>>\n}\n\nexport interface HostApiWrapped {\n call<M extends PluginApiMethod>(\n payload: PluginApiCallPayload<M>\n ): Promise<PluginApiCallResult<M>>\n}\n\nexport function createHostApi(endpoint?: Comlink.Endpoint): HostApi {\n return Comlink.wrap<HostApi>(\n endpoint ?? (globalThis as unknown as Comlink.Endpoint)\n ) as unknown as HostApi\n}\n\nlet _instance: HostApi | null = null\n\nexport function getHostApi(): HostApiWrapped {\n if (!_instance) {\n _instance = createHostApi()\n }\n return {\n call: async <M extends PluginApiMethod>(payload: PluginApiCallPayload<M>): Promise<PluginApiCallResult<M>> => {\n if (!_instance) {\n throw new Error(\"Host API not initialized\")\n }\n return _instance.call(payload).then(result => {\n if (result.success) {\n return result.data\n } else {\n throw new Error(result.error)\n }\n })\n }\n }\n}\n","import type {\n PluginApiCallPayload,\n PluginApiMethod,\n} from \"@snaptrude/plugin-core\"\nimport { getHostApi } from \"./host-api\"\n\n/**\n * Build a namespace object whose nested property access maps to a\n * dot-separated host RPC method path, and whose every call dispatches that\n * path through the host bridge.\n *\n * The host exposes the entire plugin API behind a single generic `call()`\n * (see the host `bridge.ts`), and the method string is exactly the property\n * path — so one Proxy replaces every hand-written per-method RPC wrapper for\n * every namespace (`core.*`, `design.*`, `entity.*`):\n *\n * The POSITIONAL transport forwards the whole argument tuple; the host router\n * spreads it back into the resolved method (`fn(...args)`):\n *\n * ```ts\n * snaptrude.core.math.vec3.new(1, 2, 3)\n * // → getHostApi().call({ method: \"core.math.vec3.new\", args: [1, 2, 3] })\n * ```\n *\n * Typed at the call site, e.g. `createRpcNamespace<PluginEntityApi>(\"entity\")`.\n * The Proxy is structurally cast to the abstract API type — argument and\n * return types are enforced by that type, while dispatch is dynamic.\n *\n * Every namespace uses this — including `core.math.*` and `core.geom.*`: under\n * the all-handle model there is no in-worker compute; math and geometry are\n * host calls like everything else.\n */\nexport function createRpcNamespace<T extends object>(basePath: string): T {\n const build = (path: string): unknown =>\n new Proxy(NOOP, {\n get(_target, prop) {\n // Symbols and `then` must not resolve to a callable proxy, otherwise\n // the namespace would look thenable and break Promise resolution if it\n // ever reached an `await`.\n if (typeof prop !== \"string\" || prop === \"then\") return undefined\n return build(`${path}.${prop}`)\n },\n apply(_target, _thisArg, argArray: unknown[]) {\n const payload = {\n method: path,\n args: argArray,\n } as unknown as PluginApiCallPayload<PluginApiMethod>\n return getHostApi().call(payload)\n },\n })\n\n return build(basePath) as T\n}\n\n/** Proxy target must be callable for the `apply` trap; identity is irrelevant. */\nconst NOOP = (): void => {}\n","import * as Comlink from \"comlink\"\n\nexport interface UIMessage {\n action: string\n payload: unknown\n}\n\ninterface PluginConfig {\n pluginId: string\n}\n\n/**\n * Base class for Snaptrude plugin workers.\n *\n * Handles Comlink wiring, host communication, and the standard lifecycle\n * methods (`init`, `destroy`, `ping`, `onUIMessage`). Subclass this and\n * override only the methods you need — then call `start()` to expose the\n * worker API.\n *\n * The plugin ID is received automatically from the host during\n * initialization — no need to pass it manually.\n *\n * @example\n * ```ts\n * import { PluginWorker } from \"@snaptrude/plugin-client\";\n *\n * class MyPlugin extends PluginWorker {\n * async onUIMessage(message: UIMessage) {\n * // handle messages from the UI panel\n * }\n * }\n *\n * new MyPlugin().start();\n * ```\n */\nexport abstract class PluginWorker {\n protected pluginId!: string\n private hostAPI: Comlink.Remote<Record<string, unknown>>\n\n constructor() {\n this.hostAPI = Comlink.wrap<Record<string, unknown>>(\n self as unknown as Comlink.Endpoint\n )\n }\n\n protected sendToUI(action: string, payload: unknown): void {\n ;(this.hostAPI as Record<string, any>).ui.sendToUI({ action, payload })\n }\n\n /**\n * Signal the host that this plugin has finished its work and should be\n * stopped. Use this in headless (UI-less) plugins that run a task and\n * self-terminate.\n */\n protected complete(): void {\n ;(this.hostAPI as Record<string, any>).lifecycle.complete()\n }\n\n async init(): Promise<void> {\n console.log(this.pluginId, \"init() called\")\n console.log(this.pluginId, \"Initialization complete\")\n }\n\n async destroy(): Promise<void> {\n console.log(this.pluginId, \"destroy() called — cleaning up\")\n }\n\n async ping(): Promise<string> {\n return \"pong\"\n }\n\n async onUIMessage(_message: UIMessage): Promise<void> {\n // Override in subclass to handle UI messages\n }\n\n /**\n * Expose the worker API via Comlink and start listening.\n * Call this once after constructing the plugin instance.\n *\n * The host calls `init(config)` with `{ pluginId }`,\n * which is captured here to set `this.pluginId` before the\n * subclass's `init()` runs.\n */\n start(): void {\n Comlink.expose(\n {\n init: (config: PluginConfig) => {\n this.pluginId = config.pluginId\n return this.init()\n },\n destroy: () => this.destroy(),\n ping: () => this.ping(),\n onUIMessage: (message: UIMessage) => this.onUIMessage(message),\n },\n self as unknown as Comlink.Endpoint\n )\n console.log(\"Worker loaded, API exposed via Comlink\")\n }\n}\n"],"mappings":"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","names":["Comlink"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/api/index.ts","../src/host-api.ts","../src/rpc-proxy.ts","../src/plugin-worker.ts"],"sourcesContent":["import { ClientPluginApi } from \"./api\"\n\nexport * from \"./api\"\nexport * from \"./host-api\"\nexport * from \"./plugin-worker\"\n\n// Error surface — plugins branch on `PluginError.is(e)` + `e.code`.\nexport {\n PluginError,\n PluginValidationError,\n PluginNotFoundError,\n PluginPermissionError,\n PluginHandleError,\n PluginQuotaError,\n PluginTimeoutError,\n PluginTransportError,\n PluginLifecycleError,\n PluginExecutionError,\n PluginInternalError,\n fromEnvelope,\n isErrorEnvelope,\n isPluginErrorCode,\n PLUGIN_ERROR_CODES,\n CODE_META,\n} from \"@snaptrude/plugin-core\"\nexport type {\n ErrorEnvelope,\n PluginErrorCode,\n WirePluginErrorCode,\n PluginErrorCategory,\n} from \"@snaptrude/plugin-core\"\n\n/**\n * The Snaptrude plugin client API.\n *\n * The main entry point for plugins to interact with the Snaptrude platform.\n */\nexport const snaptrude = ClientPluginApi.getInstance()\n","import {\n PluginApi,\n PluginCoreApi,\n PluginDesignApi,\n PluginEntityApi,\n PluginProgramApi,\n PluginPresentationApi,\n PluginAnalysisApi,\n} from \"@snaptrude/plugin-core\"\nimport { createRpcNamespace } from \"../rpc-proxy\"\n\nexport class ClientPluginApi extends PluginApi {\n private static instance: ClientPluginApi\n\n /**\n * Every namespace is fully remote under the all-handle model: math/geom now\n * cross to the host (values are opaque handles), so there is no in-worker\n * compute left. All dispatch through a single generic RPC Proxy. Units live\n * under `core.units`, so they ride the `core` proxy.\n */\n public core: PluginCoreApi\n public design: PluginDesignApi\n public entity: PluginEntityApi\n public program: PluginProgramApi\n public presentation: PluginPresentationApi\n public analysis: PluginAnalysisApi\n\n private constructor() {\n super()\n this.core = createRpcNamespace<PluginCoreApi>(\"core\")\n this.design = createRpcNamespace<PluginDesignApi>(\"design\")\n this.entity = createRpcNamespace<PluginEntityApi>(\"entity\")\n this.program = createRpcNamespace<PluginProgramApi>(\"program\")\n this.presentation =\n createRpcNamespace<PluginPresentationApi>(\"presentation\")\n this.analysis = createRpcNamespace<PluginAnalysisApi>(\"analysis\")\n }\n\n static getInstance(): ClientPluginApi {\n if (!ClientPluginApi.instance) {\n ClientPluginApi.instance = new ClientPluginApi()\n }\n return ClientPluginApi.instance\n }\n}\n","import * as Comlink from \"comlink\"\nimport type {\n PluginApiMethod,\n PluginApiCallPayload,\n PluginApiCallWrappedResult,\n PluginApiCallResult,\n} from \"@snaptrude/plugin-core\"\nimport { PluginError, fromEnvelope, makeClientEnvelope } from \"@snaptrude/plugin-core\"\n\nexport interface HostApi {\n call<M extends PluginApiMethod>(\n payload: PluginApiCallPayload<M>\n ): Promise<PluginApiCallWrappedResult<M>>\n}\n\nexport interface HostApiWrapped {\n call<M extends PluginApiMethod>(\n payload: PluginApiCallPayload<M>\n ): Promise<PluginApiCallResult<M>>\n}\n\nexport function createHostApi(endpoint?: Comlink.Endpoint): HostApi {\n return Comlink.wrap<HostApi>(\n endpoint ?? (globalThis as unknown as Comlink.Endpoint)\n ) as unknown as HostApi\n}\n\nlet _instance: HostApi | null = null\n\n/** TEST SEAM ONLY: replace the Comlink host instance (pass `undefined` to reset). */\nexport function __setHostApiInstance(instance?: HostApi): void {\n _instance = instance ?? null\n}\n\nexport function getHostApi(): HostApiWrapped {\n if (!_instance) {\n _instance = createHostApi()\n }\n return {\n call: async <M extends PluginApiMethod>(payload: PluginApiCallPayload<M>): Promise<PluginApiCallResult<M>> => {\n if (!_instance) {\n throw new Error(\"Host API not initialized\")\n }\n\n let result: PluginApiCallWrappedResult<M>\n try {\n result = await _instance.call(payload)\n } catch (transportErr) {\n // Comlink-level rejection: port closed, worker terminated, clone\n // failure. Never a routed failure — the router always RETURNS its\n // envelope — so normalize to a typed transport error.\n if (PluginError.is(transportErr)) throw transportErr\n throw rehydrate(\n makeClientEnvelope(\n \"TRANSPORT_LOST\",\n transportErr instanceof Error ? transportErr.message : String(transportErr),\n { methodPath: payload.method }\n )\n )\n }\n\n if (result.success) {\n return result.data\n }\n\n // Structured envelope when the host provides one; legacy hosts (string\n // `error` only) degrade to UNKNOWN with the message preserved.\n throw rehydrate(\n result.errorInfo ??\n makeClientEnvelope(\"UNKNOWN\", result.error ?? \"Unknown host error\", {\n methodPath: payload.method,\n })\n )\n }\n }\n}\n\n/**\n * Envelope → typed `PluginError`, with the stack trimmed to the plugin's call\n * site (V8 only; harmless no-op elsewhere) instead of transport internals.\n */\nfunction rehydrate(envelope: Parameters<typeof fromEnvelope>[0]): PluginError {\n const error = fromEnvelope(envelope)\n ;(Error as { captureStackTrace?: (target: object, ctor: Function) => void })\n .captureStackTrace?.(error, rehydrate)\n return error\n}\n","import type {\n PluginApiCallPayload,\n PluginApiMethod,\n} from \"@snaptrude/plugin-core\"\nimport { getHostApi } from \"./host-api\"\n\n/**\n * Build a namespace object whose nested property access maps to a\n * dot-separated host RPC method path, and whose every call dispatches that\n * path through the host bridge.\n *\n * The host exposes the entire plugin API behind a single generic `call()`\n * (see the host `bridge.ts`), and the method string is exactly the property\n * path — so one Proxy replaces every hand-written per-method RPC wrapper for\n * every namespace (`core.*`, `design.*`, `entity.*`):\n *\n * The POSITIONAL transport forwards the whole argument tuple; the host router\n * spreads it back into the resolved method (`fn(...args)`):\n *\n * ```ts\n * snaptrude.core.math.vec3.new(1, 2, 3)\n * // → getHostApi().call({ method: \"core.math.vec3.new\", args: [1, 2, 3] })\n * ```\n *\n * Typed at the call site, e.g. `createRpcNamespace<PluginEntityApi>(\"entity\")`.\n * The Proxy is structurally cast to the abstract API type — argument and\n * return types are enforced by that type, while dispatch is dynamic.\n *\n * Every namespace uses this — including `core.math.*` and `core.geom.*`: under\n * the all-handle model there is no in-worker compute; math and geometry are\n * host calls like everything else.\n */\nexport function createRpcNamespace<T extends object>(basePath: string): T {\n const build = (path: string): unknown =>\n new Proxy(NOOP, {\n get(_target, prop) {\n // Symbols and `then` must not resolve to a callable proxy, otherwise\n // the namespace would look thenable and break Promise resolution if it\n // ever reached an `await`.\n if (typeof prop !== \"string\" || prop === \"then\") return undefined\n return build(`${path}.${prop}`)\n },\n apply(_target, _thisArg, argArray: unknown[]) {\n const payload = {\n method: path,\n args: argArray,\n } as unknown as PluginApiCallPayload<PluginApiMethod>\n return getHostApi().call(payload)\n },\n })\n\n return build(basePath) as T\n}\n\n/** Proxy target must be callable for the `apply` trap; identity is irrelevant. */\nconst NOOP = (): void => {}\n","import * as Comlink from \"comlink\"\n\nexport interface UIMessage {\n action: string\n payload: unknown\n}\n\ninterface PluginConfig {\n pluginId: string\n}\n\n/**\n * Base class for Snaptrude plugin workers.\n *\n * Handles Comlink wiring, host communication, and the standard lifecycle\n * methods (`init`, `destroy`, `ping`, `onUIMessage`). Subclass this and\n * override only the methods you need — then call `start()` to expose the\n * worker API.\n *\n * The plugin ID is received automatically from the host during\n * initialization — no need to pass it manually.\n *\n * @example\n * ```ts\n * import { PluginWorker } from \"@snaptrude/plugin-client\";\n *\n * class MyPlugin extends PluginWorker {\n * async onUIMessage(message: UIMessage) {\n * // handle messages from the UI panel\n * }\n * }\n *\n * new MyPlugin().start();\n * ```\n */\nexport abstract class PluginWorker {\n protected pluginId!: string\n private hostAPI: Comlink.Remote<Record<string, unknown>>\n\n constructor() {\n this.hostAPI = Comlink.wrap<Record<string, unknown>>(\n self as unknown as Comlink.Endpoint\n )\n }\n\n protected sendToUI(action: string, payload: unknown): void {\n ;(this.hostAPI as Record<string, any>).ui.sendToUI({ action, payload })\n }\n\n /**\n * Signal the host that this plugin has finished its work and should be\n * stopped. Use this in headless (UI-less) plugins that run a task and\n * self-terminate.\n */\n protected complete(): void {\n ;(this.hostAPI as Record<string, any>).lifecycle.complete()\n }\n\n async init(): Promise<void> {\n console.log(this.pluginId, \"init() called\")\n console.log(this.pluginId, \"Initialization complete\")\n }\n\n async destroy(): Promise<void> {\n console.log(this.pluginId, \"destroy() called — cleaning up\")\n }\n\n async ping(): Promise<string> {\n return \"pong\"\n }\n\n async onUIMessage(_message: UIMessage): Promise<void> {\n // Override in subclass to handle UI messages\n }\n\n /**\n * Expose the worker API via Comlink and start listening.\n * Call this once after constructing the plugin instance.\n *\n * The host calls `init(config)` with `{ pluginId }`,\n * which is captured here to set `this.pluginId` before the\n * subclass's `init()` runs.\n */\n start(): void {\n Comlink.expose(\n {\n init: (config: PluginConfig) => {\n this.pluginId = config.pluginId\n return this.init()\n },\n destroy: () => this.destroy(),\n ping: () => this.ping(),\n onUIMessage: (message: UIMessage) => this.onUIMessage(message),\n },\n self as unknown as Comlink.Endpoint\n )\n console.log(\"Worker loaded, API exposed via Comlink\")\n }\n}\n"],"mappings":"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","names":["import_plugin_core","Comlink","import_plugin_core"]}
package/dist/index.d.ts CHANGED
@@ -2,6 +2,8 @@ import { ClientPluginApi } from "./api";
2
2
  export * from "./api";
3
3
  export * from "./host-api";
4
4
  export * from "./plugin-worker";
5
+ export { PluginError, PluginValidationError, PluginNotFoundError, PluginPermissionError, PluginHandleError, PluginQuotaError, PluginTimeoutError, PluginTransportError, PluginLifecycleError, PluginExecutionError, PluginInternalError, fromEnvelope, isErrorEnvelope, isPluginErrorCode, PLUGIN_ERROR_CODES, CODE_META, } from "@snaptrude/plugin-core";
6
+ export type { ErrorEnvelope, PluginErrorCode, WirePluginErrorCode, PluginErrorCategory, } from "@snaptrude/plugin-core";
5
7
  /**
6
8
  * The Snaptrude plugin client API.
7
9
  *
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,OAAO,CAAA;AAEvC,cAAc,OAAO,CAAA;AACrB,cAAc,YAAY,CAAA;AAC1B,cAAc,iBAAiB,CAAA;AAE/B;;;;GAIG;AACH,eAAO,MAAM,SAAS,iBAAgC,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,OAAO,CAAA;AAEvC,cAAc,OAAO,CAAA;AACrB,cAAc,YAAY,CAAA;AAC1B,cAAc,iBAAiB,CAAA;AAG/B,OAAO,EACL,WAAW,EACX,qBAAqB,EACrB,mBAAmB,EACnB,qBAAqB,EACrB,iBAAiB,EACjB,gBAAgB,EAChB,kBAAkB,EAClB,oBAAoB,EACpB,oBAAoB,EACpB,oBAAoB,EACpB,mBAAmB,EACnB,YAAY,EACZ,eAAe,EACf,iBAAiB,EACjB,kBAAkB,EAClB,SAAS,GACV,MAAM,wBAAwB,CAAA;AAC/B,YAAY,EACV,aAAa,EACb,eAAe,EACf,mBAAmB,EACnB,mBAAmB,GACpB,MAAM,wBAAwB,CAAA;AAE/B;;;;GAIG;AACH,eAAO,MAAM,SAAS,iBAAgC,CAAA"}
package/dist/index.js CHANGED
@@ -5,12 +5,16 @@ import {
5
5
 
6
6
  // src/host-api.ts
7
7
  import * as Comlink from "comlink";
8
+ import { PluginError, fromEnvelope, makeClientEnvelope } from "@snaptrude/plugin-core";
8
9
  function createHostApi(endpoint) {
9
10
  return Comlink.wrap(
10
11
  endpoint ?? globalThis
11
12
  );
12
13
  }
13
14
  var _instance = null;
15
+ function __setHostApiInstance(instance) {
16
+ _instance = instance ?? null;
17
+ }
14
18
  function getHostApi() {
15
19
  if (!_instance) {
16
20
  _instance = createHostApi();
@@ -20,16 +24,35 @@ function getHostApi() {
20
24
  if (!_instance) {
21
25
  throw new Error("Host API not initialized");
22
26
  }
23
- return _instance.call(payload).then((result) => {
24
- if (result.success) {
25
- return result.data;
26
- } else {
27
- throw new Error(result.error);
28
- }
29
- });
27
+ let result;
28
+ try {
29
+ result = await _instance.call(payload);
30
+ } catch (transportErr) {
31
+ if (PluginError.is(transportErr)) throw transportErr;
32
+ throw rehydrate(
33
+ makeClientEnvelope(
34
+ "TRANSPORT_LOST",
35
+ transportErr instanceof Error ? transportErr.message : String(transportErr),
36
+ { methodPath: payload.method }
37
+ )
38
+ );
39
+ }
40
+ if (result.success) {
41
+ return result.data;
42
+ }
43
+ throw rehydrate(
44
+ result.errorInfo ?? makeClientEnvelope("UNKNOWN", result.error ?? "Unknown host error", {
45
+ methodPath: payload.method
46
+ })
47
+ );
30
48
  }
31
49
  };
32
50
  }
51
+ function rehydrate(envelope) {
52
+ const error = fromEnvelope(envelope);
53
+ Error.captureStackTrace?.(error, rehydrate);
54
+ return error;
55
+ }
33
56
 
34
57
  // src/rpc-proxy.ts
35
58
  function createRpcNamespace(basePath) {
@@ -129,12 +152,47 @@ var PluginWorker = class {
129
152
  };
130
153
 
131
154
  // src/index.ts
155
+ import {
156
+ PluginError as PluginError2,
157
+ PluginValidationError,
158
+ PluginNotFoundError,
159
+ PluginPermissionError,
160
+ PluginHandleError,
161
+ PluginQuotaError,
162
+ PluginTimeoutError,
163
+ PluginTransportError,
164
+ PluginLifecycleError,
165
+ PluginExecutionError,
166
+ PluginInternalError,
167
+ fromEnvelope as fromEnvelope2,
168
+ isErrorEnvelope,
169
+ isPluginErrorCode,
170
+ PLUGIN_ERROR_CODES,
171
+ CODE_META
172
+ } from "@snaptrude/plugin-core";
132
173
  var snaptrude = ClientPluginApi.getInstance();
133
174
  export {
175
+ CODE_META,
134
176
  ClientPluginApi,
177
+ PLUGIN_ERROR_CODES,
178
+ PluginError2 as PluginError,
179
+ PluginExecutionError,
180
+ PluginHandleError,
181
+ PluginInternalError,
182
+ PluginLifecycleError,
183
+ PluginNotFoundError,
184
+ PluginPermissionError,
185
+ PluginQuotaError,
186
+ PluginTimeoutError,
187
+ PluginTransportError,
188
+ PluginValidationError,
135
189
  PluginWorker,
190
+ __setHostApiInstance,
136
191
  createHostApi,
192
+ fromEnvelope2 as fromEnvelope,
137
193
  getHostApi,
194
+ isErrorEnvelope,
195
+ isPluginErrorCode,
138
196
  snaptrude
139
197
  };
140
198
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/api/index.ts","../src/host-api.ts","../src/rpc-proxy.ts","../src/plugin-worker.ts","../src/index.ts"],"sourcesContent":["import {\n PluginApi,\n PluginCoreApi,\n PluginDesignApi,\n PluginEntityApi,\n PluginProgramApi,\n PluginPresentationApi,\n PluginAnalysisApi,\n} from \"@snaptrude/plugin-core\"\nimport { createRpcNamespace } from \"../rpc-proxy\"\n\nexport class ClientPluginApi extends PluginApi {\n private static instance: ClientPluginApi\n\n /**\n * Every namespace is fully remote under the all-handle model: math/geom now\n * cross to the host (values are opaque handles), so there is no in-worker\n * compute left. All dispatch through a single generic RPC Proxy. Units live\n * under `core.units`, so they ride the `core` proxy.\n */\n public core: PluginCoreApi\n public design: PluginDesignApi\n public entity: PluginEntityApi\n public program: PluginProgramApi\n public presentation: PluginPresentationApi\n public analysis: PluginAnalysisApi\n\n private constructor() {\n super()\n this.core = createRpcNamespace<PluginCoreApi>(\"core\")\n this.design = createRpcNamespace<PluginDesignApi>(\"design\")\n this.entity = createRpcNamespace<PluginEntityApi>(\"entity\")\n this.program = createRpcNamespace<PluginProgramApi>(\"program\")\n this.presentation =\n createRpcNamespace<PluginPresentationApi>(\"presentation\")\n this.analysis = createRpcNamespace<PluginAnalysisApi>(\"analysis\")\n }\n\n static getInstance(): ClientPluginApi {\n if (!ClientPluginApi.instance) {\n ClientPluginApi.instance = new ClientPluginApi()\n }\n return ClientPluginApi.instance\n }\n}\n","import * as Comlink from \"comlink\"\nimport type {\n PluginApiMethod,\n PluginApiCallPayload,\n PluginApiCallWrappedResult,\n PluginApiCallResult,\n} from \"@snaptrude/plugin-core\"\n\nexport interface HostApi {\n call<M extends PluginApiMethod>(\n payload: PluginApiCallPayload<M>\n ): Promise<PluginApiCallWrappedResult<M>>\n}\n\nexport interface HostApiWrapped {\n call<M extends PluginApiMethod>(\n payload: PluginApiCallPayload<M>\n ): Promise<PluginApiCallResult<M>>\n}\n\nexport function createHostApi(endpoint?: Comlink.Endpoint): HostApi {\n return Comlink.wrap<HostApi>(\n endpoint ?? (globalThis as unknown as Comlink.Endpoint)\n ) as unknown as HostApi\n}\n\nlet _instance: HostApi | null = null\n\nexport function getHostApi(): HostApiWrapped {\n if (!_instance) {\n _instance = createHostApi()\n }\n return {\n call: async <M extends PluginApiMethod>(payload: PluginApiCallPayload<M>): Promise<PluginApiCallResult<M>> => {\n if (!_instance) {\n throw new Error(\"Host API not initialized\")\n }\n return _instance.call(payload).then(result => {\n if (result.success) {\n return result.data\n } else {\n throw new Error(result.error)\n }\n })\n }\n }\n}\n","import type {\n PluginApiCallPayload,\n PluginApiMethod,\n} from \"@snaptrude/plugin-core\"\nimport { getHostApi } from \"./host-api\"\n\n/**\n * Build a namespace object whose nested property access maps to a\n * dot-separated host RPC method path, and whose every call dispatches that\n * path through the host bridge.\n *\n * The host exposes the entire plugin API behind a single generic `call()`\n * (see the host `bridge.ts`), and the method string is exactly the property\n * path — so one Proxy replaces every hand-written per-method RPC wrapper for\n * every namespace (`core.*`, `design.*`, `entity.*`):\n *\n * The POSITIONAL transport forwards the whole argument tuple; the host router\n * spreads it back into the resolved method (`fn(...args)`):\n *\n * ```ts\n * snaptrude.core.math.vec3.new(1, 2, 3)\n * // → getHostApi().call({ method: \"core.math.vec3.new\", args: [1, 2, 3] })\n * ```\n *\n * Typed at the call site, e.g. `createRpcNamespace<PluginEntityApi>(\"entity\")`.\n * The Proxy is structurally cast to the abstract API type — argument and\n * return types are enforced by that type, while dispatch is dynamic.\n *\n * Every namespace uses this — including `core.math.*` and `core.geom.*`: under\n * the all-handle model there is no in-worker compute; math and geometry are\n * host calls like everything else.\n */\nexport function createRpcNamespace<T extends object>(basePath: string): T {\n const build = (path: string): unknown =>\n new Proxy(NOOP, {\n get(_target, prop) {\n // Symbols and `then` must not resolve to a callable proxy, otherwise\n // the namespace would look thenable and break Promise resolution if it\n // ever reached an `await`.\n if (typeof prop !== \"string\" || prop === \"then\") return undefined\n return build(`${path}.${prop}`)\n },\n apply(_target, _thisArg, argArray: unknown[]) {\n const payload = {\n method: path,\n args: argArray,\n } as unknown as PluginApiCallPayload<PluginApiMethod>\n return getHostApi().call(payload)\n },\n })\n\n return build(basePath) as T\n}\n\n/** Proxy target must be callable for the `apply` trap; identity is irrelevant. */\nconst NOOP = (): void => {}\n","import * as Comlink from \"comlink\"\n\nexport interface UIMessage {\n action: string\n payload: unknown\n}\n\ninterface PluginConfig {\n pluginId: string\n}\n\n/**\n * Base class for Snaptrude plugin workers.\n *\n * Handles Comlink wiring, host communication, and the standard lifecycle\n * methods (`init`, `destroy`, `ping`, `onUIMessage`). Subclass this and\n * override only the methods you need — then call `start()` to expose the\n * worker API.\n *\n * The plugin ID is received automatically from the host during\n * initialization — no need to pass it manually.\n *\n * @example\n * ```ts\n * import { PluginWorker } from \"@snaptrude/plugin-client\";\n *\n * class MyPlugin extends PluginWorker {\n * async onUIMessage(message: UIMessage) {\n * // handle messages from the UI panel\n * }\n * }\n *\n * new MyPlugin().start();\n * ```\n */\nexport abstract class PluginWorker {\n protected pluginId!: string\n private hostAPI: Comlink.Remote<Record<string, unknown>>\n\n constructor() {\n this.hostAPI = Comlink.wrap<Record<string, unknown>>(\n self as unknown as Comlink.Endpoint\n )\n }\n\n protected sendToUI(action: string, payload: unknown): void {\n ;(this.hostAPI as Record<string, any>).ui.sendToUI({ action, payload })\n }\n\n /**\n * Signal the host that this plugin has finished its work and should be\n * stopped. Use this in headless (UI-less) plugins that run a task and\n * self-terminate.\n */\n protected complete(): void {\n ;(this.hostAPI as Record<string, any>).lifecycle.complete()\n }\n\n async init(): Promise<void> {\n console.log(this.pluginId, \"init() called\")\n console.log(this.pluginId, \"Initialization complete\")\n }\n\n async destroy(): Promise<void> {\n console.log(this.pluginId, \"destroy() called — cleaning up\")\n }\n\n async ping(): Promise<string> {\n return \"pong\"\n }\n\n async onUIMessage(_message: UIMessage): Promise<void> {\n // Override in subclass to handle UI messages\n }\n\n /**\n * Expose the worker API via Comlink and start listening.\n * Call this once after constructing the plugin instance.\n *\n * The host calls `init(config)` with `{ pluginId }`,\n * which is captured here to set `this.pluginId` before the\n * subclass's `init()` runs.\n */\n start(): void {\n Comlink.expose(\n {\n init: (config: PluginConfig) => {\n this.pluginId = config.pluginId\n return this.init()\n },\n destroy: () => this.destroy(),\n ping: () => this.ping(),\n onUIMessage: (message: UIMessage) => this.onUIMessage(message),\n },\n self as unknown as Comlink.Endpoint\n )\n console.log(\"Worker loaded, API exposed via Comlink\")\n }\n}\n","import { ClientPluginApi } from \"./api\"\n\nexport * from \"./api\"\nexport * from \"./host-api\"\nexport * from \"./plugin-worker\"\n\n/**\n * The Snaptrude plugin client API.\n *\n * The main entry point for plugins to interact with the Snaptrude platform.\n */\nexport const snaptrude = ClientPluginApi.getInstance()\n"],"mappings":"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","names":["Comlink"]}
1
+ {"version":3,"sources":["../src/api/index.ts","../src/host-api.ts","../src/rpc-proxy.ts","../src/plugin-worker.ts","../src/index.ts"],"sourcesContent":["import {\n PluginApi,\n PluginCoreApi,\n PluginDesignApi,\n PluginEntityApi,\n PluginProgramApi,\n PluginPresentationApi,\n PluginAnalysisApi,\n} from \"@snaptrude/plugin-core\"\nimport { createRpcNamespace } from \"../rpc-proxy\"\n\nexport class ClientPluginApi extends PluginApi {\n private static instance: ClientPluginApi\n\n /**\n * Every namespace is fully remote under the all-handle model: math/geom now\n * cross to the host (values are opaque handles), so there is no in-worker\n * compute left. All dispatch through a single generic RPC Proxy. Units live\n * under `core.units`, so they ride the `core` proxy.\n */\n public core: PluginCoreApi\n public design: PluginDesignApi\n public entity: PluginEntityApi\n public program: PluginProgramApi\n public presentation: PluginPresentationApi\n public analysis: PluginAnalysisApi\n\n private constructor() {\n super()\n this.core = createRpcNamespace<PluginCoreApi>(\"core\")\n this.design = createRpcNamespace<PluginDesignApi>(\"design\")\n this.entity = createRpcNamespace<PluginEntityApi>(\"entity\")\n this.program = createRpcNamespace<PluginProgramApi>(\"program\")\n this.presentation =\n createRpcNamespace<PluginPresentationApi>(\"presentation\")\n this.analysis = createRpcNamespace<PluginAnalysisApi>(\"analysis\")\n }\n\n static getInstance(): ClientPluginApi {\n if (!ClientPluginApi.instance) {\n ClientPluginApi.instance = new ClientPluginApi()\n }\n return ClientPluginApi.instance\n }\n}\n","import * as Comlink from \"comlink\"\nimport type {\n PluginApiMethod,\n PluginApiCallPayload,\n PluginApiCallWrappedResult,\n PluginApiCallResult,\n} from \"@snaptrude/plugin-core\"\nimport { PluginError, fromEnvelope, makeClientEnvelope } from \"@snaptrude/plugin-core\"\n\nexport interface HostApi {\n call<M extends PluginApiMethod>(\n payload: PluginApiCallPayload<M>\n ): Promise<PluginApiCallWrappedResult<M>>\n}\n\nexport interface HostApiWrapped {\n call<M extends PluginApiMethod>(\n payload: PluginApiCallPayload<M>\n ): Promise<PluginApiCallResult<M>>\n}\n\nexport function createHostApi(endpoint?: Comlink.Endpoint): HostApi {\n return Comlink.wrap<HostApi>(\n endpoint ?? (globalThis as unknown as Comlink.Endpoint)\n ) as unknown as HostApi\n}\n\nlet _instance: HostApi | null = null\n\n/** TEST SEAM ONLY: replace the Comlink host instance (pass `undefined` to reset). */\nexport function __setHostApiInstance(instance?: HostApi): void {\n _instance = instance ?? null\n}\n\nexport function getHostApi(): HostApiWrapped {\n if (!_instance) {\n _instance = createHostApi()\n }\n return {\n call: async <M extends PluginApiMethod>(payload: PluginApiCallPayload<M>): Promise<PluginApiCallResult<M>> => {\n if (!_instance) {\n throw new Error(\"Host API not initialized\")\n }\n\n let result: PluginApiCallWrappedResult<M>\n try {\n result = await _instance.call(payload)\n } catch (transportErr) {\n // Comlink-level rejection: port closed, worker terminated, clone\n // failure. Never a routed failure — the router always RETURNS its\n // envelope — so normalize to a typed transport error.\n if (PluginError.is(transportErr)) throw transportErr\n throw rehydrate(\n makeClientEnvelope(\n \"TRANSPORT_LOST\",\n transportErr instanceof Error ? transportErr.message : String(transportErr),\n { methodPath: payload.method }\n )\n )\n }\n\n if (result.success) {\n return result.data\n }\n\n // Structured envelope when the host provides one; legacy hosts (string\n // `error` only) degrade to UNKNOWN with the message preserved.\n throw rehydrate(\n result.errorInfo ??\n makeClientEnvelope(\"UNKNOWN\", result.error ?? \"Unknown host error\", {\n methodPath: payload.method,\n })\n )\n }\n }\n}\n\n/**\n * Envelope → typed `PluginError`, with the stack trimmed to the plugin's call\n * site (V8 only; harmless no-op elsewhere) instead of transport internals.\n */\nfunction rehydrate(envelope: Parameters<typeof fromEnvelope>[0]): PluginError {\n const error = fromEnvelope(envelope)\n ;(Error as { captureStackTrace?: (target: object, ctor: Function) => void })\n .captureStackTrace?.(error, rehydrate)\n return error\n}\n","import type {\n PluginApiCallPayload,\n PluginApiMethod,\n} from \"@snaptrude/plugin-core\"\nimport { getHostApi } from \"./host-api\"\n\n/**\n * Build a namespace object whose nested property access maps to a\n * dot-separated host RPC method path, and whose every call dispatches that\n * path through the host bridge.\n *\n * The host exposes the entire plugin API behind a single generic `call()`\n * (see the host `bridge.ts`), and the method string is exactly the property\n * path — so one Proxy replaces every hand-written per-method RPC wrapper for\n * every namespace (`core.*`, `design.*`, `entity.*`):\n *\n * The POSITIONAL transport forwards the whole argument tuple; the host router\n * spreads it back into the resolved method (`fn(...args)`):\n *\n * ```ts\n * snaptrude.core.math.vec3.new(1, 2, 3)\n * // → getHostApi().call({ method: \"core.math.vec3.new\", args: [1, 2, 3] })\n * ```\n *\n * Typed at the call site, e.g. `createRpcNamespace<PluginEntityApi>(\"entity\")`.\n * The Proxy is structurally cast to the abstract API type — argument and\n * return types are enforced by that type, while dispatch is dynamic.\n *\n * Every namespace uses this — including `core.math.*` and `core.geom.*`: under\n * the all-handle model there is no in-worker compute; math and geometry are\n * host calls like everything else.\n */\nexport function createRpcNamespace<T extends object>(basePath: string): T {\n const build = (path: string): unknown =>\n new Proxy(NOOP, {\n get(_target, prop) {\n // Symbols and `then` must not resolve to a callable proxy, otherwise\n // the namespace would look thenable and break Promise resolution if it\n // ever reached an `await`.\n if (typeof prop !== \"string\" || prop === \"then\") return undefined\n return build(`${path}.${prop}`)\n },\n apply(_target, _thisArg, argArray: unknown[]) {\n const payload = {\n method: path,\n args: argArray,\n } as unknown as PluginApiCallPayload<PluginApiMethod>\n return getHostApi().call(payload)\n },\n })\n\n return build(basePath) as T\n}\n\n/** Proxy target must be callable for the `apply` trap; identity is irrelevant. */\nconst NOOP = (): void => {}\n","import * as Comlink from \"comlink\"\n\nexport interface UIMessage {\n action: string\n payload: unknown\n}\n\ninterface PluginConfig {\n pluginId: string\n}\n\n/**\n * Base class for Snaptrude plugin workers.\n *\n * Handles Comlink wiring, host communication, and the standard lifecycle\n * methods (`init`, `destroy`, `ping`, `onUIMessage`). Subclass this and\n * override only the methods you need — then call `start()` to expose the\n * worker API.\n *\n * The plugin ID is received automatically from the host during\n * initialization — no need to pass it manually.\n *\n * @example\n * ```ts\n * import { PluginWorker } from \"@snaptrude/plugin-client\";\n *\n * class MyPlugin extends PluginWorker {\n * async onUIMessage(message: UIMessage) {\n * // handle messages from the UI panel\n * }\n * }\n *\n * new MyPlugin().start();\n * ```\n */\nexport abstract class PluginWorker {\n protected pluginId!: string\n private hostAPI: Comlink.Remote<Record<string, unknown>>\n\n constructor() {\n this.hostAPI = Comlink.wrap<Record<string, unknown>>(\n self as unknown as Comlink.Endpoint\n )\n }\n\n protected sendToUI(action: string, payload: unknown): void {\n ;(this.hostAPI as Record<string, any>).ui.sendToUI({ action, payload })\n }\n\n /**\n * Signal the host that this plugin has finished its work and should be\n * stopped. Use this in headless (UI-less) plugins that run a task and\n * self-terminate.\n */\n protected complete(): void {\n ;(this.hostAPI as Record<string, any>).lifecycle.complete()\n }\n\n async init(): Promise<void> {\n console.log(this.pluginId, \"init() called\")\n console.log(this.pluginId, \"Initialization complete\")\n }\n\n async destroy(): Promise<void> {\n console.log(this.pluginId, \"destroy() called — cleaning up\")\n }\n\n async ping(): Promise<string> {\n return \"pong\"\n }\n\n async onUIMessage(_message: UIMessage): Promise<void> {\n // Override in subclass to handle UI messages\n }\n\n /**\n * Expose the worker API via Comlink and start listening.\n * Call this once after constructing the plugin instance.\n *\n * The host calls `init(config)` with `{ pluginId }`,\n * which is captured here to set `this.pluginId` before the\n * subclass's `init()` runs.\n */\n start(): void {\n Comlink.expose(\n {\n init: (config: PluginConfig) => {\n this.pluginId = config.pluginId\n return this.init()\n },\n destroy: () => this.destroy(),\n ping: () => this.ping(),\n onUIMessage: (message: UIMessage) => this.onUIMessage(message),\n },\n self as unknown as Comlink.Endpoint\n )\n console.log(\"Worker loaded, API exposed via Comlink\")\n }\n}\n","import { ClientPluginApi } from \"./api\"\n\nexport * from \"./api\"\nexport * from \"./host-api\"\nexport * from \"./plugin-worker\"\n\n// Error surface — plugins branch on `PluginError.is(e)` + `e.code`.\nexport {\n PluginError,\n PluginValidationError,\n PluginNotFoundError,\n PluginPermissionError,\n PluginHandleError,\n PluginQuotaError,\n PluginTimeoutError,\n PluginTransportError,\n PluginLifecycleError,\n PluginExecutionError,\n PluginInternalError,\n fromEnvelope,\n isErrorEnvelope,\n isPluginErrorCode,\n PLUGIN_ERROR_CODES,\n CODE_META,\n} from \"@snaptrude/plugin-core\"\nexport type {\n ErrorEnvelope,\n PluginErrorCode,\n WirePluginErrorCode,\n PluginErrorCategory,\n} from \"@snaptrude/plugin-core\"\n\n/**\n * The Snaptrude plugin client API.\n *\n * The main entry point for plugins to interact with the Snaptrude platform.\n */\nexport const snaptrude = ClientPluginApi.getInstance()\n"],"mappings":"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","names":["Comlink","PluginError","fromEnvelope"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@snaptrude/plugin-client",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -18,7 +18,7 @@
18
18
  },
19
19
  "dependencies": {
20
20
  "comlink": "^4.4.2",
21
- "@snaptrude/plugin-core": "0.6.0"
21
+ "@snaptrude/plugin-core": "0.7.1"
22
22
  },
23
23
  "devDependencies": {
24
24
  "tsup": "^8.5.1",
package/src/host-api.ts CHANGED
@@ -5,6 +5,7 @@ import type {
5
5
  PluginApiCallWrappedResult,
6
6
  PluginApiCallResult,
7
7
  } from "@snaptrude/plugin-core"
8
+ import { PluginError, fromEnvelope, makeClientEnvelope } from "@snaptrude/plugin-core"
8
9
 
9
10
  export interface HostApi {
10
11
  call<M extends PluginApiMethod>(
@@ -26,6 +27,11 @@ export function createHostApi(endpoint?: Comlink.Endpoint): HostApi {
26
27
 
27
28
  let _instance: HostApi | null = null
28
29
 
30
+ /** TEST SEAM ONLY: replace the Comlink host instance (pass `undefined` to reset). */
31
+ export function __setHostApiInstance(instance?: HostApi): void {
32
+ _instance = instance ?? null
33
+ }
34
+
29
35
  export function getHostApi(): HostApiWrapped {
30
36
  if (!_instance) {
31
37
  _instance = createHostApi()
@@ -35,13 +41,47 @@ export function getHostApi(): HostApiWrapped {
35
41
  if (!_instance) {
36
42
  throw new Error("Host API not initialized")
37
43
  }
38
- return _instance.call(payload).then(result => {
39
- if (result.success) {
40
- return result.data
41
- } else {
42
- throw new Error(result.error)
43
- }
44
- })
44
+
45
+ let result: PluginApiCallWrappedResult<M>
46
+ try {
47
+ result = await _instance.call(payload)
48
+ } catch (transportErr) {
49
+ // Comlink-level rejection: port closed, worker terminated, clone
50
+ // failure. Never a routed failure — the router always RETURNS its
51
+ // envelope — so normalize to a typed transport error.
52
+ if (PluginError.is(transportErr)) throw transportErr
53
+ throw rehydrate(
54
+ makeClientEnvelope(
55
+ "TRANSPORT_LOST",
56
+ transportErr instanceof Error ? transportErr.message : String(transportErr),
57
+ { methodPath: payload.method }
58
+ )
59
+ )
60
+ }
61
+
62
+ if (result.success) {
63
+ return result.data
64
+ }
65
+
66
+ // Structured envelope when the host provides one; legacy hosts (string
67
+ // `error` only) degrade to UNKNOWN with the message preserved.
68
+ throw rehydrate(
69
+ result.errorInfo ??
70
+ makeClientEnvelope("UNKNOWN", result.error ?? "Unknown host error", {
71
+ methodPath: payload.method,
72
+ })
73
+ )
45
74
  }
46
75
  }
47
76
  }
77
+
78
+ /**
79
+ * Envelope → typed `PluginError`, with the stack trimmed to the plugin's call
80
+ * site (V8 only; harmless no-op elsewhere) instead of transport internals.
81
+ */
82
+ function rehydrate(envelope: Parameters<typeof fromEnvelope>[0]): PluginError {
83
+ const error = fromEnvelope(envelope)
84
+ ;(Error as { captureStackTrace?: (target: object, ctor: Function) => void })
85
+ .captureStackTrace?.(error, rehydrate)
86
+ return error
87
+ }
package/src/index.ts CHANGED
@@ -4,6 +4,32 @@ export * from "./api"
4
4
  export * from "./host-api"
5
5
  export * from "./plugin-worker"
6
6
 
7
+ // Error surface — plugins branch on `PluginError.is(e)` + `e.code`.
8
+ export {
9
+ PluginError,
10
+ PluginValidationError,
11
+ PluginNotFoundError,
12
+ PluginPermissionError,
13
+ PluginHandleError,
14
+ PluginQuotaError,
15
+ PluginTimeoutError,
16
+ PluginTransportError,
17
+ PluginLifecycleError,
18
+ PluginExecutionError,
19
+ PluginInternalError,
20
+ fromEnvelope,
21
+ isErrorEnvelope,
22
+ isPluginErrorCode,
23
+ PLUGIN_ERROR_CODES,
24
+ CODE_META,
25
+ } from "@snaptrude/plugin-core"
26
+ export type {
27
+ ErrorEnvelope,
28
+ PluginErrorCode,
29
+ WirePluginErrorCode,
30
+ PluginErrorCategory,
31
+ } from "@snaptrude/plugin-core"
32
+
7
33
  /**
8
34
  * The Snaptrude plugin client API.
9
35
  *
@@ -0,0 +1,101 @@
1
+ // Node built-in test runner suite for host-api error rehydration.
2
+ // Run: node --test test/host-api-errors.test.mjs
3
+ // Imports the BUILT package (dist) — run `pnpm build` first.
4
+ import test from "node:test"
5
+ import assert from "node:assert/strict"
6
+ import {
7
+ getHostApi,
8
+ __setHostApiInstance,
9
+ PluginError,
10
+ PluginQuotaError,
11
+ PluginTransportError,
12
+ PluginInternalError,
13
+ } from "../dist/index.js"
14
+
15
+ const PAYLOAD = { method: "design.boolean.union", args: [] }
16
+
17
+ /** Install a fake Comlink instance whose call() resolves/rejects as directed. */
18
+ function stubHost(behavior) {
19
+ __setHostApiInstance({ call: behavior })
20
+ }
21
+
22
+ test.afterEach(() => {
23
+ __setHostApiInstance(undefined)
24
+ })
25
+
26
+ test("success path returns data verbatim (handles are branded strings on the wire)", async () => {
27
+ const data = { value: 42, h: "mass_x" }
28
+ stubHost(async () => ({ success: true, data }))
29
+ const result = await getHostApi().call(PAYLOAD)
30
+ assert.equal(result, data) // pass-through: no cloning, no re-wrapping
31
+ assert.equal(result.value, 42)
32
+ assert.equal(result.h, "mass_x") // a handle IS its plain string id
33
+ })
34
+
35
+ test("errorInfo envelope rehydrates into the typed subclass with fields intact", async () => {
36
+ stubHost(async () => ({
37
+ success: false,
38
+ error: "Execution error: Rate limit exceeded",
39
+ errorInfo: {
40
+ envelopeVersion: 1,
41
+ code: "RATE_LIMITED",
42
+ message: "Rate limit exceeded",
43
+ errorId: "h-1",
44
+ details: { retryAfterMs: 250 },
45
+ methodPath: "design.boolean.union",
46
+ },
47
+ }))
48
+ await assert.rejects(getHostApi().call(PAYLOAD), (err) => {
49
+ assert.equal(PluginError.is(err), true)
50
+ assert.ok(err instanceof PluginQuotaError)
51
+ assert.equal(err.code, "RATE_LIMITED")
52
+ assert.equal(err.errorId, "h-1")
53
+ assert.deepEqual(err.details, { retryAfterMs: 250 })
54
+ assert.equal(err.methodPath, "design.boolean.union")
55
+ return true
56
+ })
57
+ })
58
+
59
+ test("unknown envelope code still rehydrates as a PluginError (forward compat)", async () => {
60
+ stubHost(async () => ({
61
+ success: false,
62
+ error: "Execution error: new thing",
63
+ errorInfo: {
64
+ envelopeVersion: 1,
65
+ code: "FUTURE_CODE",
66
+ message: "new thing",
67
+ errorId: "h-2",
68
+ },
69
+ }))
70
+ await assert.rejects(getHostApi().call(PAYLOAD), (err) => {
71
+ assert.equal(PluginError.is(err), true)
72
+ assert.equal(err.code, "FUTURE_CODE")
73
+ return true
74
+ })
75
+ })
76
+
77
+ test("legacy host (string error only) degrades to UNKNOWN with the message preserved", async () => {
78
+ stubHost(async () => ({ success: false, error: "Execution error: something broke" }))
79
+ await assert.rejects(getHostApi().call(PAYLOAD), (err) => {
80
+ assert.equal(PluginError.is(err), true)
81
+ assert.ok(err instanceof PluginInternalError)
82
+ assert.equal(err.code, "UNKNOWN")
83
+ assert.equal(err.message, "Execution error: something broke")
84
+ assert.ok(err.errorId.startsWith("c-"))
85
+ assert.equal(err.methodPath, "design.boolean.union")
86
+ return true
87
+ })
88
+ })
89
+
90
+ test("transport-level rejection normalizes to TRANSPORT_LOST", async () => {
91
+ stubHost(async () => {
92
+ throw new Error("port closed")
93
+ })
94
+ await assert.rejects(getHostApi().call(PAYLOAD), (err) => {
95
+ assert.ok(err instanceof PluginTransportError)
96
+ assert.equal(err.code, "TRANSPORT_LOST")
97
+ assert.equal(err.message, "port closed")
98
+ assert.ok(err.errorId.startsWith("c-"))
99
+ return true
100
+ })
101
+ })