refactor: add xai plugin-sdk boundary canary (#61548)

* docs: plan real plugin-sdk workspace rollout

* build: add xai plugin-sdk boundary canary

* build: generate plugin-sdk package types

* build: hide plugin-sdk core export

* build: alias scoped plugin-sdk runtime imports

* build: repair plugin-sdk boundary drift

* fix(plugins): remove duplicated plugin-sdk entrypoints

* test(plugins): make tsc boundary canary portable

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
Harold Hunt
2026-04-06 09:13:11 -04:00
committed by GitHub
parent 0430bab070
commit 0bd0097557
53 changed files with 952 additions and 51 deletions

View File

@@ -0,0 +1,580 @@
---
title: refactor: Make plugin-sdk a real workspace package incrementally
type: refactor
status: active
date: 2026-04-05
---
# refactor: Make plugin-sdk a real workspace package incrementally
## Overview
This plan introduces a real workspace package for the plugin SDK at
`packages/plugin-sdk` and uses it to opt in a small first wave of extensions to
compiler-enforced package boundaries. The goal is to make illegal relative
imports fail under normal `tsc` for a selected set of bundled provider
extensions, without forcing a repo-wide migration or a giant merge-conflict
surface.
The key incremental move is to run two modes in parallel for a while:
| Mode | Import shape | Who uses it | Enforcement |
| ----------- | ------------------------ | ------------------------------------ | -------------------------------------------- |
| Legacy mode | `openclaw/plugin-sdk/*` | all existing non-opted-in extensions | current permissive behavior remains |
| Opt-in mode | `@openclaw/plugin-sdk/*` | first-wave extensions only | package-local `rootDir` + project references |
## Problem Frame
The current repo exports a large public plugin SDK surface, but it is not a real
workspace package. Instead:
- root `tsconfig.json` maps `openclaw/plugin-sdk/*` directly to
`src/plugin-sdk/*.ts`
- extensions that were not opted into the previous experiment still share that
global source-alias behavior
- adding `rootDir` only works when allowed SDK imports stop resolving into raw
repo source
That means the repo can describe the desired boundary policy, but TypeScript
does not enforce it cleanly for most extensions.
You want an incremental path that:
- makes `plugin-sdk` real
- moves the SDK toward a workspace package named `@openclaw/plugin-sdk`
- changes only about 10 extensions in the first PR
- leaves the rest of the extension tree on the old scheme until later cleanup
- avoids the `tsconfig.plugin-sdk.dts.json` + postinstall-generated declaration
workflow as the primary mechanism for the first-wave rollout
## Requirements Trace
- R1. Create a real workspace package for the plugin SDK under `packages/`.
- R2. Name the new package `@openclaw/plugin-sdk`.
- R3. Give the new SDK package its own `package.json` and `tsconfig.json`.
- R4. Keep legacy `openclaw/plugin-sdk/*` imports working for non-opted-in
extensions during the migration window.
- R5. Opt in only a small first wave of extensions in the first PR.
- R6. The first-wave extensions must fail closed for relative imports that leave
their package root.
- R7. The first-wave extensions must consume the SDK through a package
dependency and a TS project reference, not through root `paths` aliases.
- R8. The plan must avoid a repo-wide mandatory postinstall generation step for
editor correctness.
- R9. The first-wave rollout must be reviewable and mergeable as a moderate PR,
not a repo-wide 300+ file refactor.
## Scope Boundaries
- No full migration of all bundled extensions in the first PR.
- No requirement to delete `src/plugin-sdk` in the first PR.
- No requirement to rewire every root build or test path to use the new package
immediately.
- No attempt to force VS Code squiggles for every non-opted-in extension.
- No broad lint cleanup for the rest of the extension tree.
- No large runtime behavior changes beyond import resolution, package ownership,
and boundary enforcement for the opted-in extensions.
## Context & Research
### Relevant Code and Patterns
- `pnpm-workspace.yaml` already includes `packages/*` and `extensions/*`, so a
new workspace package under `packages/plugin-sdk` fits the existing repo
layout.
- Existing workspace packages such as `packages/memory-host-sdk/package.json`
and `packages/plugin-package-contract/package.json` already use package-local
`exports` maps rooted in `src/*.ts`.
- Root `package.json` currently publishes the SDK surface through `./plugin-sdk`
and `./plugin-sdk/*` exports backed by `dist/plugin-sdk/*.js` and
`dist/plugin-sdk/*.d.ts`.
- `src/plugin-sdk/entrypoints.ts` and `scripts/lib/plugin-sdk-entrypoints.json`
already act as the canonical entrypoint inventory for the SDK surface.
- Root `tsconfig.json` currently maps:
- `openclaw/plugin-sdk` -> `src/plugin-sdk/index.ts`
- `openclaw/plugin-sdk/*` -> `src/plugin-sdk/*.ts`
- The previous boundary experiment showed that package-local `rootDir` works for
illegal relative imports only after allowed SDK imports stop resolving to raw
source outside the extension package.
### First-Wave Extension Set
This plan assumes the first wave is the provider-heavy set that is least likely
to drag in complex channel-runtime edge cases:
- `extensions/anthropic`
- `extensions/exa`
- `extensions/firecrawl`
- `extensions/groq`
- `extensions/mistral`
- `extensions/openai`
- `extensions/perplexity`
- `extensions/tavily`
- `extensions/together`
- `extensions/xai`
### First-Wave SDK Surface Inventory
The first-wave extensions currently import a manageable subset of SDK subpaths.
The initial `@openclaw/plugin-sdk` package only needs to cover these:
- `agent-runtime`
- `cli-runtime`
- `config-runtime`
- `core`
- `image-generation`
- `media-runtime`
- `media-understanding`
- `plugin-entry`
- `plugin-runtime`
- `provider-auth`
- `provider-auth-api-key`
- `provider-auth-login`
- `provider-auth-runtime`
- `provider-catalog-shared`
- `provider-entry`
- `provider-http`
- `provider-model-shared`
- `provider-onboard`
- `provider-stream-family`
- `provider-stream-shared`
- `provider-tools`
- `provider-usage`
- `provider-web-fetch`
- `provider-web-search`
- `realtime-transcription`
- `realtime-voice`
- `runtime-env`
- `secret-input`
- `security-runtime`
- `speech`
- `testing`
### Institutional Learnings
- No relevant `docs/solutions/` entries were present in this worktree.
### External References
- No external research was needed for this plan. The repo already contains the
relevant workspace-package and SDK-export patterns.
## Key Technical Decisions
- Introduce `@openclaw/plugin-sdk` as a new workspace package while keeping the
legacy root `openclaw/plugin-sdk/*` surface alive during migration.
Rationale: this lets a first-wave extension set move onto real package
resolution without forcing every extension and every root build path to change
at once.
- Use a dedicated opt-in boundary base config such as
`extensions/tsconfig.package-boundary.base.json` instead of replacing the
existing extension base for everyone.
Rationale: the repo needs to support both legacy and opt-in extension modes
simultaneously during migration.
- Use TS project references from first-wave extensions to
`packages/plugin-sdk/tsconfig.json` and set
`disableSourceOfProjectReferenceRedirect` for the opt-in boundary mode.
Rationale: this gives `tsc` a real package graph while discouraging editor and
compiler fallback to raw source traversal.
- Keep `@openclaw/plugin-sdk` private in the first wave.
Rationale: the immediate goal is internal boundary enforcement and migration
safety, not publishing a second external SDK contract before the surface is
stable.
- Move only the first-wave SDK subpaths in the first implementation slice, and
keep compatibility bridges for the rest.
Rationale: physically moving all 315 `src/plugin-sdk/*.ts` files in one PR is
exactly the merge-conflict surface this plan is trying to avoid.
- Do not rely on `scripts/postinstall-bundled-plugins.mjs` to build SDK
declarations for the first wave.
Rationale: explicit build/reference flows are easier to reason about and keep
repo behavior more predictable.
## Open Questions
### Resolved During Planning
- Which extensions should be in the first wave?
Use the 10 provider/web-search extensions listed above because they are more
structurally isolated than the heavier channel packages.
- Should the first PR replace the entire extension tree?
No. The first PR should support two modes in parallel and only opt in the
first wave.
- Should the first wave require a postinstall declaration build?
No. The package/reference graph should be explicit, and CI should run the
relevant package-local typecheck intentionally.
### Deferred to Implementation
- Whether the first-wave package can point directly at package-local `src/*.ts`
via project references alone, or whether a small declaration-emission step is
still required for the `@openclaw/plugin-sdk` package.
This is an implementation-owned TS graph validation question.
- Whether the root `openclaw` package should proxy first-wave SDK subpaths to
`packages/plugin-sdk` outputs immediately or continue using generated
compatibility shims under `src/plugin-sdk`.
This is a compatibility and build-shape detail that depends on the minimal
implementation path that keeps CI green.
## High-Level Technical Design
> This illustrates the intended approach and is directional guidance for review, not implementation specification. The implementing agent should treat it as context, not code to reproduce.
```mermaid
flowchart TB
subgraph Legacy["Legacy extensions (unchanged)"]
L1["extensions/*\nopenclaw/plugin-sdk/*"]
L2["root tsconfig paths"]
L1 --> L2
L2 --> L3["src/plugin-sdk/*"]
end
subgraph OptIn["First-wave extensions"]
O1["10 opted-in extensions"]
O2["extensions/tsconfig.package-boundary.base.json"]
O3["rootDir = '.'\nproject reference"]
O4["@openclaw/plugin-sdk"]
O1 --> O2
O2 --> O3
O3 --> O4
end
subgraph SDK["New workspace package"]
P1["packages/plugin-sdk/package.json"]
P2["packages/plugin-sdk/tsconfig.json"]
P3["packages/plugin-sdk/src/<first-wave-subpaths>.ts"]
P1 --> P2
P2 --> P3
end
O4 --> SDK
```
## Implementation Units
- [ ] **Unit 1: Introduce the real `@openclaw/plugin-sdk` workspace package**
**Goal:** Create a real workspace package for the SDK that can own the
first-wave subpath surface without forcing a repo-wide migration.
**Requirements:** R1, R2, R3, R8, R9
**Dependencies:** None
**Files:**
- Create: `packages/plugin-sdk/package.json`
- Create: `packages/plugin-sdk/tsconfig.json`
- Create: `packages/plugin-sdk/src/index.ts`
- Create: `packages/plugin-sdk/src/*.ts` for the first-wave SDK subpaths
- Modify: `pnpm-workspace.yaml` only if package-glob adjustments are needed
- Modify: `package.json`
- Modify: `src/plugin-sdk/entrypoints.ts`
- Modify: `scripts/lib/plugin-sdk-entrypoints.json`
- Test: `src/plugins/contracts/plugin-sdk-workspace-package.contract.test.ts`
**Approach:**
- Add a new workspace package named `@openclaw/plugin-sdk`.
- Start with the first-wave SDK subpaths only, not the entire 315-file tree.
- If directly moving a first-wave entrypoint would create an oversized diff, the
first PR may introduce that subpath in `packages/plugin-sdk/src` as a thin
package wrapper first and then flip the source of truth to the package in a
follow-up PR for that subpath cluster.
- Reuse the existing entrypoint inventory machinery so the first-wave package
surface is declared in one canonical place.
- Keep the root package exports alive for legacy users while the workspace
package becomes the new opt-in contract.
**Patterns to follow:**
- `packages/memory-host-sdk/package.json`
- `packages/plugin-package-contract/package.json`
- `src/plugin-sdk/entrypoints.ts`
**Test scenarios:**
- Happy path: the workspace package exports every first-wave subpath listed in
the plan and no required first-wave export is missing.
- Edge case: package export metadata remains stable when the first-wave entry
list is re-generated or compared against the canonical inventory.
- Integration: root package legacy SDK exports remain present after introducing
the new workspace package.
**Verification:**
- The repo contains a valid `@openclaw/plugin-sdk` workspace package with a
stable first-wave export map and no legacy export regression in root
`package.json`.
- [ ] **Unit 2: Add an opt-in TS boundary mode for package-enforced extensions**
**Goal:** Define the TS configuration mode that opted-in extensions will use,
while leaving the existing extension TS behavior unchanged for everyone else.
**Requirements:** R4, R6, R7, R8, R9
**Dependencies:** Unit 1
**Files:**
- Create: `extensions/tsconfig.package-boundary.base.json`
- Create: `tsconfig.boundary-optin.json`
- Modify: `extensions/xai/tsconfig.json`
- Modify: `extensions/openai/tsconfig.json`
- Modify: `extensions/anthropic/tsconfig.json`
- Modify: `extensions/mistral/tsconfig.json`
- Modify: `extensions/groq/tsconfig.json`
- Modify: `extensions/together/tsconfig.json`
- Modify: `extensions/perplexity/tsconfig.json`
- Modify: `extensions/tavily/tsconfig.json`
- Modify: `extensions/exa/tsconfig.json`
- Modify: `extensions/firecrawl/tsconfig.json`
- Test: `src/plugins/contracts/extension-package-project-boundaries.test.ts`
- Test: `test/extension-package-tsc-boundary.test.ts`
**Approach:**
- Leave `extensions/tsconfig.base.json` in place for legacy extensions.
- Add a new opt-in base config that:
- sets `rootDir: "."`
- references `packages/plugin-sdk`
- enables `composite`
- disables project-reference source redirect when needed
- Add a dedicated solution config for the first-wave typecheck graph instead of
reshaping the root repo TS project in the same PR.
**Execution note:** Start with a failing package-local canary typecheck for one
opted-in extension before applying the pattern to all 10.
**Patterns to follow:**
- Existing package-local extension `tsconfig.json` pattern from the prior
boundary work
- Workspace package pattern from `packages/memory-host-sdk`
**Test scenarios:**
- Happy path: each opted-in extension typechecks successfully through the
package-boundary TS config.
- Error path: a canary relative import from `../../src/cli/acp-cli.ts` fails
with `TS6059` for an opted-in extension.
- Integration: non-opted-in extensions remain untouched and do not need to
participate in the new solution config.
**Verification:**
- There is a dedicated typecheck graph for the 10 opted-in extensions, and bad
relative imports from one of them fail through normal `tsc`.
- [ ] **Unit 3: Migrate the first-wave extensions onto `@openclaw/plugin-sdk`**
**Goal:** Change the first-wave extensions to consume the real SDK package
through dependency metadata, project references, and package-name imports.
**Requirements:** R5, R6, R7, R9
**Dependencies:** Unit 2
**Files:**
- Modify: `extensions/anthropic/package.json`
- Modify: `extensions/exa/package.json`
- Modify: `extensions/firecrawl/package.json`
- Modify: `extensions/groq/package.json`
- Modify: `extensions/mistral/package.json`
- Modify: `extensions/openai/package.json`
- Modify: `extensions/perplexity/package.json`
- Modify: `extensions/tavily/package.json`
- Modify: `extensions/together/package.json`
- Modify: `extensions/xai/package.json`
- Modify: production and test imports under each of the 10 extension roots that
currently reference `openclaw/plugin-sdk/*`
**Approach:**
- Add `@openclaw/plugin-sdk: workspace:*` to the first-wave extension
`devDependencies`.
- Replace `openclaw/plugin-sdk/*` imports in those packages with
`@openclaw/plugin-sdk/*`.
- Keep local extension-internal imports on local barrels such as `./api.ts` and
`./runtime-api.ts`.
- Do not change non-opted-in extensions in this PR.
**Patterns to follow:**
- Existing extension-local import barrels (`api.ts`, `runtime-api.ts`)
- Package dependency shape used by other `@openclaw/*` workspace packages
**Test scenarios:**
- Happy path: each migrated extension still registers/loads through its existing
plugin tests after the import rewrite.
- Edge case: test-only SDK imports in the opted-in extension set still resolve
correctly through the new package.
- Integration: migrated extensions do not require root `openclaw/plugin-sdk/*`
aliases for typechecking.
**Verification:**
- The first-wave extensions build and test against `@openclaw/plugin-sdk`
without needing the legacy root SDK alias path.
- [ ] **Unit 4: Preserve legacy compatibility while the migration is partial**
**Goal:** Keep the rest of the repo working while the SDK exists in both legacy
and new-package forms during migration.
**Requirements:** R4, R8, R9
**Dependencies:** Units 1-3
**Files:**
- Modify: `src/plugin-sdk/*.ts` for first-wave compatibility shims as needed
- Modify: `package.json`
- Modify: build or export plumbing that assembles SDK artifacts
- Test: `src/plugins/contracts/plugin-sdk-runtime-api-guardrails.test.ts`
- Test: `src/plugins/contracts/plugin-sdk-index.bundle.test.ts`
**Approach:**
- Keep root `openclaw/plugin-sdk/*` as the compatibility surface for legacy
extensions and for external consumers that are not moving yet.
- Use either generated shims or root-export proxy wiring for the first-wave
subpaths that have moved into `packages/plugin-sdk`.
- Do not attempt to retire the root SDK surface in this phase.
**Patterns to follow:**
- Existing root SDK export generation via `src/plugin-sdk/entrypoints.ts`
- Existing package export compatibility in root `package.json`
**Test scenarios:**
- Happy path: a legacy root SDK import still resolves for a non-opted-in
extension after the new package exists.
- Edge case: a first-wave subpath works through both the legacy root surface and
the new package surface during the migration window.
- Integration: plugin-sdk index/bundle contract tests continue to see a coherent
public surface.
**Verification:**
- The repo supports both legacy and opt-in SDK consumption modes without
breaking unchanged extensions.
- [ ] **Unit 5: Add scoped enforcement and document the migration contract**
**Goal:** Land CI and contributor guidance that enforce the new behavior for the
first wave without pretending the entire extension tree is migrated.
**Requirements:** R5, R6, R8, R9
**Dependencies:** Units 1-4
**Files:**
- Modify: `package.json`
- Modify: CI workflow files that should run the opt-in boundary typecheck
- Modify: `AGENTS.md`
- Modify: `docs/plugins/sdk-overview.md`
- Modify: `docs/plugins/sdk-entrypoints.md`
- Modify: `docs/plans/2026-04-05-001-refactor-extension-package-resolution-boundary-plan.md`
**Approach:**
- Add an explicit first-wave gate, such as a dedicated `tsc -b` solution run for
`packages/plugin-sdk` plus the 10 opted-in extensions.
- Document that the repo now supports both legacy and opt-in extension modes,
and that new extension boundary work should prefer the new package route.
- Record the next-wave migration rule so later PRs can add more extensions
without re-litigating the architecture.
**Patterns to follow:**
- Existing contract tests under `src/plugins/contracts/`
- Existing docs updates that explain staged migrations
**Test scenarios:**
- Happy path: the new first-wave typecheck gate passes for the workspace package
and the opted-in extensions.
- Error path: introducing a new illegal relative import in an opted-in
extension fails the scoped typecheck gate.
- Integration: CI does not require non-opted-in extensions to satisfy the new
package-boundary mode yet.
**Verification:**
- The first-wave enforcement path is documented, tested, and runnable without
forcing the entire extension tree to migrate.
## System-Wide Impact
- **Interaction graph:** this work touches the SDK source-of-truth, root package
exports, extension package metadata, TS graph layout, and CI verification.
- **Error propagation:** the main intended failure mode becomes compile-time TS
errors (`TS6059`) in opted-in extensions instead of custom script-only
failures.
- **State lifecycle risks:** dual-surface migration introduces drift risk between
root compatibility exports and the new workspace package.
- **API surface parity:** first-wave subpaths must remain semantically identical
through both `openclaw/plugin-sdk/*` and `@openclaw/plugin-sdk/*` during the
transition.
- **Integration coverage:** unit tests are not enough; scoped package-graph
typechecks are required to prove the boundary.
- **Unchanged invariants:** non-opted-in extensions keep their current behavior
in PR 1. This plan does not claim repo-wide import-boundary enforcement.
## Risks & Dependencies
| Risk | Mitigation |
| ------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- |
| The first-wave package still resolves back into raw source and `rootDir` does not actually fail closed | Make the first implementation step a package-reference canary on one opted-in extension before widening to the full set |
| Moving too much SDK source at once recreates the original merge-conflict problem | Move only the first-wave subpaths in the first PR and keep root compatibility bridges |
| Legacy and new SDK surfaces drift semantically | Keep a single entrypoint inventory, add compatibility contract tests, and make dual-surface parity explicit |
| Root repo build/test paths accidentally start depending on the new package in uncontrolled ways | Use a dedicated opt-in solution config and keep root-wide TS topology changes out of the first PR |
## Phased Delivery
### Phase 1
- Introduce `@openclaw/plugin-sdk`
- Define the first-wave subpath surface
- Prove one opted-in extension can fail closed through `rootDir`
### Phase 2
- Opt in the 10 first-wave extensions
- Keep root compatibility alive for everyone else
### Phase 3
- Add more extensions in later PRs
- Move more SDK subpaths into the workspace package
- Retire root compatibility only after the legacy extension set is gone
## Documentation / Operational Notes
- The first PR should explicitly describe itself as a dual-mode migration, not a
repo-wide enforcement completion.
- The migration guide should make it easy for later PRs to add more extensions
by following the same package/dependency/reference pattern.
## Sources & References
- Prior plan: `docs/plans/2026-04-05-001-refactor-extension-package-resolution-boundary-plan.md`
- Workspace config: `pnpm-workspace.yaml`
- Existing SDK entrypoint inventory: `src/plugin-sdk/entrypoints.ts`
- Existing root SDK exports: `package.json`
- Existing workspace package patterns:
- `packages/memory-host-sdk/package.json`
- `packages/plugin-package-contract/package.json`

View File

@@ -0,0 +1,10 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"baseUrl": "..",
"ignoreDeprecations": "6.0",
"paths": {
"@openclaw/plugin-sdk/*": ["packages/plugin-sdk/dist/packages/plugin-sdk/src/*.d.ts"]
}
}
}

View File

@@ -3,11 +3,11 @@ import {
normalizeNativeXaiModelId,
normalizeProviderId,
resolveProviderEndpoint,
} from "openclaw/plugin-sdk/provider-model-shared";
} from "@openclaw/plugin-sdk/provider-model-shared";
import {
applyXaiModelCompat,
resolveXaiModelCompatPatch,
} from "openclaw/plugin-sdk/provider-tools";
} from "@openclaw/plugin-sdk/provider-tools";
export { buildXaiProvider } from "./provider-catalog.js";
export { applyXaiConfig, applyXaiProviderConfig } from "./onboard.js";
@@ -27,7 +27,7 @@ export {
HTML_ENTITY_TOOL_CALL_ARGUMENTS_ENCODING,
XAI_TOOL_SCHEMA_PROFILE,
resolveXaiModelCompatPatch,
} from "openclaw/plugin-sdk/provider-tools";
} from "@openclaw/plugin-sdk/provider-tools";
function isXaiNativeEndpoint(baseUrl: unknown): boolean {
return (

View File

@@ -1,4 +1,4 @@
import { withFetchPreconnect } from "openclaw/plugin-sdk/testing";
import { withFetchPreconnect } from "@openclaw/plugin-sdk/testing";
import { afterEach, describe, expect, it, vi } from "vitest";
import { createCodeExecutionTool } from "./code-execution.js";

View File

@@ -1,7 +1,7 @@
import { getRuntimeConfigSnapshot } from "@openclaw/plugin-sdk/config-runtime";
import type { OpenClawConfig } from "@openclaw/plugin-sdk/plugin-entry";
import { jsonResult, readStringParam } from "@openclaw/plugin-sdk/provider-web-search";
import { Type } from "@sinclair/typebox";
import { getRuntimeConfigSnapshot } from "openclaw/plugin-sdk/config-runtime";
import type { OpenClawConfig } from "openclaw/plugin-sdk/plugin-entry";
import { jsonResult, readStringParam } from "openclaw/plugin-sdk/provider-web-search";
import {
buildXaiCodeExecutionPayload,
requestXaiCodeExecution,

View File

@@ -1,8 +1,8 @@
import type { OpenClawConfig } from "@openclaw/plugin-sdk/plugin-entry";
import { defineSingleProviderPluginEntry } from "@openclaw/plugin-sdk/provider-entry";
import { buildProviderReplayFamilyHooks } from "@openclaw/plugin-sdk/provider-model-shared";
import { jsonResult, readProviderEnvValue } from "@openclaw/plugin-sdk/provider-web-search";
import { Type } from "@sinclair/typebox";
import type { OpenClawConfig } from "openclaw/plugin-sdk/plugin-entry";
import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry";
import { buildProviderReplayFamilyHooks } from "openclaw/plugin-sdk/provider-model-shared";
import { jsonResult, readProviderEnvValue } from "openclaw/plugin-sdk/provider-web-search";
import {
applyXaiModelCompat,
normalizeXaiModelId,

View File

@@ -1,4 +1,4 @@
import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-model-shared";
import type { ModelDefinitionConfig } from "@openclaw/plugin-sdk/provider-model-shared";
export const XAI_BASE_URL = "https://api.x.ai/v1";
export const XAI_DEFAULT_MODEL_ID = "grok-4";

View File

@@ -1,7 +1,7 @@
import {
resolveAgentModelFallbackValues,
resolveAgentModelPrimaryValue,
} from "openclaw/plugin-sdk/provider-onboard";
} from "@openclaw/plugin-sdk/provider-onboard";
import { describe, expect, it } from "vitest";
import {
createConfigWithFallbacks,

View File

@@ -1,7 +1,7 @@
import {
createDefaultModelsPresetAppliers,
type OpenClawConfig,
} from "openclaw/plugin-sdk/provider-onboard";
} from "@openclaw/plugin-sdk/provider-onboard";
import { XAI_BASE_URL, XAI_DEFAULT_MODEL_ID } from "./model-definitions.js";
import { buildXaiCatalogModels } from "./model-definitions.js";

View File

@@ -4,6 +4,9 @@
"private": true,
"description": "OpenClaw xAI plugin",
"type": "module",
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*"
},
"openclaw": {
"extensions": [
"./index.ts"

View File

@@ -1,4 +1,4 @@
import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-shared";
import type { ModelProviderConfig } from "@openclaw/plugin-sdk/provider-model-shared";
import { buildXaiCatalogModels, XAI_BASE_URL } from "./model-definitions.js";
export function buildXaiProvider(

View File

@@ -1,8 +1,8 @@
import type {
ProviderResolveDynamicModelContext,
ProviderRuntimeModel,
} from "openclaw/plugin-sdk/core";
import { normalizeModelCompat } from "openclaw/plugin-sdk/provider-model-shared";
} from "@openclaw/plugin-sdk/plugin-entry";
import { normalizeModelCompat } from "@openclaw/plugin-sdk/provider-model-shared";
import { applyXaiModelCompat } from "./api.js";
import { resolveXaiCatalogEntry, XAI_BASE_URL } from "./model-definitions.js";

View File

@@ -1,4 +1,4 @@
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { definePluginEntry } from "@openclaw/plugin-sdk/plugin-entry";
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);

View File

@@ -1,4 +1,4 @@
import { postTrustedWebToolsJson } from "openclaw/plugin-sdk/provider-web-search";
import { postTrustedWebToolsJson } from "@openclaw/plugin-sdk/provider-web-search";
import {
buildXaiResponsesToolBody,
resolveXaiResponseTextAndCitations,

View File

@@ -1,4 +1,4 @@
import { NON_ENV_SECRETREF_MARKER } from "openclaw/plugin-sdk/provider-auth-runtime";
import { NON_ENV_SECRETREF_MARKER } from "@openclaw/plugin-sdk/provider-auth-runtime";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
isXaiToolEnabled,

View File

@@ -1,14 +1,14 @@
import type { OpenClawConfig } from "openclaw/plugin-sdk/plugin-entry";
import type { OpenClawConfig } from "@openclaw/plugin-sdk/plugin-entry";
import {
coerceSecretRef,
resolveNonEnvSecretRefApiKeyMarker,
} from "openclaw/plugin-sdk/provider-auth";
} from "@openclaw/plugin-sdk/provider-auth";
import {
readProviderEnvValue,
readConfiguredSecretString,
resolveProviderWebSearchPluginConfig,
} from "openclaw/plugin-sdk/provider-web-search";
import { normalizeSecretInputString } from "openclaw/plugin-sdk/secret-input";
} from "@openclaw/plugin-sdk/provider-web-search";
import { normalizeSecretInputString } from "@openclaw/plugin-sdk/secret-input";
export type XaiFallbackAuth = {
apiKey: string;

View File

@@ -1,4 +1,4 @@
import { postTrustedWebToolsJson, wrapWebContent } from "openclaw/plugin-sdk/provider-web-search";
import { postTrustedWebToolsJson, wrapWebContent } from "@openclaw/plugin-sdk/provider-web-search";
import { normalizeXaiModelId } from "../model-id.js";
import {
buildXaiResponsesToolBody,

View File

@@ -1,4 +1,4 @@
import type { OpenClawConfig } from "openclaw/plugin-sdk/plugin-entry";
import type { OpenClawConfig } from "@openclaw/plugin-sdk/plugin-entry";
type JsonRecord = Record<string, unknown>;

View File

@@ -1,4 +1,4 @@
import { postTrustedWebToolsJson, wrapWebContent } from "openclaw/plugin-sdk/provider-web-search";
import { postTrustedWebToolsJson, wrapWebContent } from "@openclaw/plugin-sdk/provider-web-search";
import {
buildXaiResponsesToolBody,
resolveXaiResponseTextAndCitations,

View File

@@ -1,10 +1,10 @@
import type { StreamFn } from "@mariozechner/pi-agent-core";
import { streamSimple } from "@mariozechner/pi-ai";
import type { ProviderWrapStreamFnContext } from "openclaw/plugin-sdk/plugin-entry";
import type { ProviderWrapStreamFnContext } from "@openclaw/plugin-sdk/plugin-entry";
import {
composeProviderStreamWrappers,
createToolStreamWrapper,
} from "openclaw/plugin-sdk/provider-stream-shared";
} from "@openclaw/plugin-sdk/provider-stream-shared";
const XAI_FAST_MODEL_IDS = new Map<string, string>([
["grok-3", "grok-3-fast"],

View File

@@ -0,0 +1,8 @@
{
"extends": "../tsconfig.package-boundary.base.json",
"compilerOptions": {
"rootDir": "."
},
"include": ["./*.ts", "./src/**/*.ts"],
"exclude": ["./**/*.test.ts", "./dist/**", "./node_modules/**"]
}

View File

@@ -20,11 +20,11 @@ const {
})),
}));
vi.mock("openclaw/plugin-sdk/provider-auth-runtime", () => ({
vi.mock("@openclaw/plugin-sdk/provider-auth-runtime", () => ({
resolveApiKeyForProvider: resolveApiKeyForProviderMock,
}));
vi.mock("openclaw/plugin-sdk/provider-http", () => ({
vi.mock("@openclaw/plugin-sdk/provider-http", () => ({
assertOkOrThrowHttpError: assertOkOrThrowHttpErrorMock,
fetchWithTimeout: fetchWithTimeoutMock,
postJsonRequest: postJsonRequestMock,

View File

@@ -1,17 +1,17 @@
import { isProviderApiKeyConfigured } from "openclaw/plugin-sdk/provider-auth";
import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth-runtime";
import { isProviderApiKeyConfigured } from "@openclaw/plugin-sdk/provider-auth";
import { resolveApiKeyForProvider } from "@openclaw/plugin-sdk/provider-auth-runtime";
import {
assertOkOrThrowHttpError,
fetchWithTimeout,
postJsonRequest,
resolveProviderHttpRequestConfig,
} from "openclaw/plugin-sdk/provider-http";
} from "@openclaw/plugin-sdk/provider-http";
import type {
GeneratedVideoAsset,
VideoGenerationProvider,
VideoGenerationRequest,
VideoGenerationSourceAsset,
} from "openclaw/plugin-sdk/video-generation";
} from "@openclaw/plugin-sdk/video-generation";
const DEFAULT_XAI_VIDEO_BASE_URL = "https://api.x.ai/v1";
const DEFAULT_XAI_VIDEO_MODEL = "grok-imagine-video";

View File

@@ -1,6 +1,6 @@
import { NON_ENV_SECRETREF_MARKER } from "openclaw/plugin-sdk/provider-auth-runtime";
import { createNonExitingRuntime } from "openclaw/plugin-sdk/runtime-env";
import { withEnv } from "openclaw/plugin-sdk/testing";
import { NON_ENV_SECRETREF_MARKER } from "@openclaw/plugin-sdk/provider-auth-runtime";
import { createNonExitingRuntime } from "@openclaw/plugin-sdk/runtime-env";
import { withEnv } from "@openclaw/plugin-sdk/testing";
import { describe, expect, it, vi } from "vitest";
import { capturePluginRegistration } from "../../src/plugins/captured-registration.js";
import { createWizardPrompter } from "../../test/helpers/wizard-prompter.js";

View File

@@ -1,4 +1,3 @@
import { Type } from "@sinclair/typebox";
import {
DEFAULT_CACHE_TTL_MINUTES,
DEFAULT_TIMEOUT_SECONDS,
@@ -19,7 +18,8 @@ import {
type WebSearchProviderSetupContext,
type WebSearchProviderPlugin,
writeCache,
} from "openclaw/plugin-sdk/provider-web-search";
} from "@openclaw/plugin-sdk/provider-web-search";
import { Type } from "@sinclair/typebox";
import {
buildXaiWebSearchPayload,
extractXaiWebSearchContent,

View File

@@ -1,4 +1,4 @@
import { withFetchPreconnect } from "openclaw/plugin-sdk/testing";
import { withFetchPreconnect } from "@openclaw/plugin-sdk/testing";
import { afterEach, describe, expect, it, vi } from "vitest";
import { createXSearchTool } from "./x-search.js";

View File

@@ -1,6 +1,5 @@
import { Type } from "@sinclair/typebox";
import { getRuntimeConfigSnapshot } from "openclaw/plugin-sdk/config-runtime";
import type { OpenClawConfig } from "openclaw/plugin-sdk/plugin-entry";
import { getRuntimeConfigSnapshot } from "@openclaw/plugin-sdk/config-runtime";
import type { OpenClawConfig } from "@openclaw/plugin-sdk/plugin-entry";
import {
jsonResult,
readCache,
@@ -9,7 +8,8 @@ import {
resolveCacheTtlMs,
resolveTimeoutSeconds,
writeCache,
} from "openclaw/plugin-sdk/provider-web-search";
} from "@openclaw/plugin-sdk/provider-web-search";
import { Type } from "@sinclair/typebox";
import { isXaiToolEnabled, resolveXaiToolApiKey } from "./src/tool-auth-shared.js";
import {
resolveEffectiveXSearchConfig,

View File

@@ -0,0 +1,68 @@
{
"name": "@openclaw/plugin-sdk",
"version": "0.0.0-private",
"private": true,
"type": "module",
"exports": {
"./config-runtime": {
"types": "./dist/packages/plugin-sdk/src/config-runtime.d.ts",
"default": "./src/config-runtime.ts"
},
"./plugin-entry": {
"types": "./dist/packages/plugin-sdk/src/plugin-entry.d.ts",
"default": "./src/plugin-entry.ts"
},
"./provider-auth": {
"types": "./dist/packages/plugin-sdk/src/provider-auth.d.ts",
"default": "./src/provider-auth.ts"
},
"./provider-auth-runtime": {
"types": "./dist/packages/plugin-sdk/src/provider-auth-runtime.d.ts",
"default": "./src/provider-auth-runtime.ts"
},
"./provider-entry": {
"types": "./dist/packages/plugin-sdk/src/provider-entry.d.ts",
"default": "./src/provider-entry.ts"
},
"./provider-http": {
"types": "./dist/packages/plugin-sdk/src/provider-http.d.ts",
"default": "./src/provider-http.ts"
},
"./provider-model-shared": {
"types": "./dist/packages/plugin-sdk/src/provider-model-shared.d.ts",
"default": "./src/provider-model-shared.ts"
},
"./provider-onboard": {
"types": "./dist/packages/plugin-sdk/src/provider-onboard.d.ts",
"default": "./src/provider-onboard.ts"
},
"./provider-stream-shared": {
"types": "./dist/packages/plugin-sdk/src/provider-stream-shared.d.ts",
"default": "./src/provider-stream-shared.ts"
},
"./provider-tools": {
"types": "./dist/packages/plugin-sdk/src/provider-tools.d.ts",
"default": "./src/provider-tools.ts"
},
"./provider-web-search": {
"types": "./dist/packages/plugin-sdk/src/provider-web-search.d.ts",
"default": "./src/provider-web-search.ts"
},
"./runtime-env": {
"types": "./dist/packages/plugin-sdk/src/runtime-env.d.ts",
"default": "./src/runtime-env.ts"
},
"./secret-input": {
"types": "./dist/packages/plugin-sdk/src/secret-input.d.ts",
"default": "./src/secret-input.ts"
},
"./testing": {
"types": "./dist/packages/plugin-sdk/src/testing.d.ts",
"default": "./src/testing.ts"
},
"./video-generation": {
"types": "./dist/packages/plugin-sdk/src/video-generation.d.ts",
"default": "./src/video-generation.ts"
}
}
}

View File

@@ -0,0 +1 @@
export * from "../../../src/plugin-sdk/config-runtime.js";

View File

@@ -0,0 +1 @@
export * from "../../../src/plugin-sdk/plugin-entry.js";

View File

@@ -0,0 +1 @@
export * from "../../../src/plugin-sdk/provider-auth-runtime.js";

View File

@@ -0,0 +1 @@
export * from "../../../src/plugin-sdk/provider-auth.js";

View File

@@ -0,0 +1 @@
export * from "../../../src/plugin-sdk/provider-entry.js";

View File

@@ -0,0 +1 @@
export * from "../../../src/plugin-sdk/provider-http.js";

View File

@@ -0,0 +1 @@
export * from "../../../src/plugin-sdk/provider-model-shared.js";

View File

@@ -0,0 +1 @@
export * from "../../../src/plugin-sdk/provider-onboard.js";

View File

@@ -0,0 +1 @@
export * from "../../../src/plugin-sdk/provider-stream-shared.js";

View File

@@ -0,0 +1 @@
export * from "../../../src/plugin-sdk/provider-tools.js";

View File

@@ -0,0 +1 @@
export * from "../../../src/plugin-sdk/provider-web-search.js";

View File

@@ -0,0 +1 @@
export * from "../../../src/plugin-sdk/runtime-env.js";

View File

@@ -0,0 +1 @@
export * from "../../../src/plugin-sdk/secret-input.js";

View File

@@ -0,0 +1 @@
export * from "../../../src/plugin-sdk/testing.js";

View File

@@ -0,0 +1 @@
export * from "../../../src/plugin-sdk/video-generation.js";

View File

@@ -0,0 +1,19 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"declaration": true,
"declarationMap": false,
"emitDeclarationOnly": true,
"ignoreDeprecations": "6.0",
"noEmit": false,
"noEmitOnError": false,
"outDir": "dist",
"rootDir": "../.."
},
"include": [
"src/**/*.ts",
"../../src/types/**/*.d.ts",
"../../packages/memory-host-sdk/src/**/*.ts"
],
"exclude": ["node_modules", "dist", "../../src/**/*.test.ts"]
}

8
pnpm-lock.yaml generated
View File

@@ -752,7 +752,11 @@ importers:
specifier: workspace:*
version: link:../..
extensions/xai: {}
extensions/xai:
devDependencies:
'@openclaw/plugin-sdk':
specifier: workspace:*
version: link:../../packages/plugin-sdk
extensions/xiaomi: {}
@@ -797,6 +801,8 @@ importers:
packages/plugin-package-contract: {}
packages/plugin-sdk: {}
ui:
dependencies:
'@create-markdown/preview':

View File

@@ -15,6 +15,11 @@ export type {
export type { OpenClawConfig } from "../config/config.js";
export { describeFailoverError, isFailoverError } from "../agents/failover-error.js";
export {
buildNoCapabilityModelConfiguredMessage,
resolveCapabilityModelCandidates,
throwCapabilityGenerationFailure,
} from "../media-generation/runtime-shared.js";
export {
resolveAgentModelFallbackValues,
resolveAgentModelPrimaryValue,

View File

@@ -20,6 +20,11 @@ export type {
export type { OpenClawConfig } from "../config/config.js";
export { describeFailoverError, isFailoverError } from "../agents/failover-error.js";
export {
buildNoCapabilityModelConfiguredMessage,
resolveCapabilityModelCandidates,
throwCapabilityGenerationFailure,
} from "../media-generation/runtime-shared.js";
export {
resolveAgentModelFallbackValues,
resolveAgentModelPrimaryValue,

View File

@@ -33,7 +33,11 @@ function applyVitestCapabilityAliasOverrides(params: {
return params.aliasMap;
}
const { ["openclaw/plugin-sdk"]: _ignoredRootAlias, ...scopedAliasMap } = params.aliasMap;
const {
["openclaw/plugin-sdk"]: _ignoredLegacyRootAlias,
["@openclaw/plugin-sdk"]: _ignoredScopedRootAlias,
...scopedAliasMap
} = params.aliasMap;
return {
...scopedAliasMap,
// Capability contract loads only need a narrow SDK slice. Keep those
@@ -42,18 +46,33 @@ function applyVitestCapabilityAliasOverrides(params: {
"openclaw/plugin-sdk/llm-task": fileURLToPath(
new URL("./capability-runtime-vitest-shims/llm-task.ts", import.meta.url),
),
"@openclaw/plugin-sdk/llm-task": fileURLToPath(
new URL("./capability-runtime-vitest-shims/llm-task.ts", import.meta.url),
),
"openclaw/plugin-sdk/config-runtime": fileURLToPath(
new URL("./capability-runtime-vitest-shims/config-runtime.ts", import.meta.url),
),
"@openclaw/plugin-sdk/config-runtime": fileURLToPath(
new URL("./capability-runtime-vitest-shims/config-runtime.ts", import.meta.url),
),
"openclaw/plugin-sdk/media-runtime": fileURLToPath(
new URL("./capability-runtime-vitest-shims/media-runtime.ts", import.meta.url),
),
"@openclaw/plugin-sdk/media-runtime": fileURLToPath(
new URL("./capability-runtime-vitest-shims/media-runtime.ts", import.meta.url),
),
"openclaw/plugin-sdk/provider-onboard": fileURLToPath(
new URL("../plugin-sdk/provider-onboard.ts", import.meta.url),
),
"@openclaw/plugin-sdk/provider-onboard": fileURLToPath(
new URL("../plugin-sdk/provider-onboard.ts", import.meta.url),
),
"openclaw/plugin-sdk/speech-core": fileURLToPath(
new URL("./capability-runtime-vitest-shims/speech-core.ts", import.meta.url),
),
"@openclaw/plugin-sdk/speech-core": fileURLToPath(
new URL("./capability-runtime-vitest-shims/speech-core.ts", import.meta.url),
),
};
}

View File

@@ -0,0 +1,79 @@
import { existsSync, readFileSync } from "node:fs";
import { resolve } from "node:path";
import { describe, expect, it } from "vitest";
const REPO_ROOT = resolve(import.meta.dirname, "../../..");
type TsConfigJson = {
extends?: unknown;
compilerOptions?: {
paths?: unknown;
rootDir?: unknown;
outDir?: unknown;
declaration?: unknown;
emitDeclarationOnly?: unknown;
};
include?: unknown;
exclude?: unknown;
};
type PackageJson = {
name?: unknown;
exports?: Record<string, { types?: unknown; default?: unknown }>;
devDependencies?: Record<string, string>;
};
function readJsonFile<T>(relativePath: string): T {
return JSON.parse(readFileSync(resolve(REPO_ROOT, relativePath), "utf8")) as T;
}
describe("opt-in extension package boundaries", () => {
it("keeps the opt-in extension base on real package resolution", () => {
const tsconfig = readJsonFile<TsConfigJson>("extensions/tsconfig.package-boundary.base.json");
expect(tsconfig.extends).toBe("../tsconfig.json");
expect(tsconfig.compilerOptions?.paths).toEqual({
"@openclaw/plugin-sdk/*": ["packages/plugin-sdk/dist/packages/plugin-sdk/src/*.d.ts"],
});
});
it("roots xai inside its own package and depends on the package sdk", () => {
const tsconfig = readJsonFile<TsConfigJson>("extensions/xai/tsconfig.json");
expect(tsconfig.extends).toBe("../tsconfig.package-boundary.base.json");
expect(tsconfig.compilerOptions?.rootDir).toBe(".");
expect(tsconfig.include).toEqual(["./*.ts", "./src/**/*.ts"]);
expect(tsconfig.exclude).toEqual(["./**/*.test.ts", "./dist/**", "./node_modules/**"]);
const packageJson = readJsonFile<PackageJson>("extensions/xai/package.json");
expect(packageJson.devDependencies?.["@openclaw/plugin-sdk"]).toBe("workspace:*");
});
it("keeps plugin-sdk package types generated from the package build, not a hand-maintained types bridge", () => {
const tsconfig = readJsonFile<TsConfigJson>("packages/plugin-sdk/tsconfig.json");
expect(tsconfig.extends).toBe("../../tsconfig.json");
expect(tsconfig.compilerOptions?.declaration).toBe(true);
expect(tsconfig.compilerOptions?.emitDeclarationOnly).toBe(true);
expect(tsconfig.compilerOptions?.outDir).toBe("dist");
expect(tsconfig.compilerOptions?.rootDir).toBe("../..");
expect(tsconfig.include).toEqual([
"src/**/*.ts",
"../../src/types/**/*.d.ts",
"../../packages/memory-host-sdk/src/**/*.ts",
]);
const packageJson = readJsonFile<PackageJson>("packages/plugin-sdk/package.json");
expect(packageJson.name).toBe("@openclaw/plugin-sdk");
expect(packageJson.exports?.["./core"]).toBeUndefined();
expect(packageJson.exports?.["./plugin-entry"]?.types).toBe(
"./dist/packages/plugin-sdk/src/plugin-entry.d.ts",
);
expect(packageJson.exports?.["./provider-http"]?.types).toBe(
"./dist/packages/plugin-sdk/src/provider-http.d.ts",
);
expect(packageJson.exports?.["./video-generation"]?.types).toBe(
"./dist/packages/plugin-sdk/src/video-generation.d.ts",
);
expect(existsSync(resolve(REPO_ROOT, "packages/plugin-sdk/types/plugin-entry.d.ts"))).toBe(
false,
);
});
});

View File

@@ -134,7 +134,12 @@ export function getPluginBoundaryJiti(
modulePath,
});
const aliasMap = {
...(pluginSdkAlias ? { "openclaw/plugin-sdk": pluginSdkAlias } : {}),
...(pluginSdkAlias
? {
"openclaw/plugin-sdk": pluginSdkAlias,
"@openclaw/plugin-sdk": pluginSdkAlias,
}
: {}),
...resolvePluginSdkScopedAliasMap({ modulePath }),
};
const loader = createJiti(import.meta.url, {

View File

@@ -221,10 +221,16 @@ function expectPluginSdkAliasTargets(
expect(fs.realpathSync(aliases["openclaw/plugin-sdk"] ?? "")).toBe(
fs.realpathSync(params.rootAliasPath),
);
expect(fs.realpathSync(aliases["@openclaw/plugin-sdk"] ?? "")).toBe(
fs.realpathSync(params.rootAliasPath),
);
if (params.channelRuntimePath) {
expect(fs.realpathSync(aliases["openclaw/plugin-sdk/channel-runtime"] ?? "")).toBe(
fs.realpathSync(params.channelRuntimePath),
);
expect(fs.realpathSync(aliases["@openclaw/plugin-sdk/channel-runtime"] ?? "")).toBe(
fs.realpathSync(params.channelRuntimePath),
);
}
}
@@ -715,7 +721,7 @@ describe("plugin sdk alias helpers", () => {
fs.writeFileSync(jitiBaseFile, "export {};\n", "utf-8");
fs.writeFileSync(
path.join(copiedSourceDir, "channel.runtime.ts"),
`import { resolveOutboundSendDep } from "openclaw/plugin-sdk/infra-runtime";
`import { resolveOutboundSendDep } from "@openclaw/plugin-sdk/infra-runtime";
export const syntheticRuntimeMarker = {
resolveOutboundSendDep,
@@ -745,6 +751,7 @@ export const syntheticRuntimeMarker = {
const withAlias = createJiti(jitiBaseUrl, {
...buildPluginLoaderJitiOptions({
"openclaw/plugin-sdk/infra-runtime": copiedChannelRuntimeShim,
"@openclaw/plugin-sdk/infra-runtime": copiedChannelRuntimeShim,
}),
tryNative: false,
});

View File

@@ -250,6 +250,7 @@ export function resolvePluginSdkAliasFile(params: {
const cachedPluginSdkExportedSubpaths = new Map<string, string[]>();
const cachedPluginSdkScopedAliasMaps = new Map<string, Record<string, string>>();
const PLUGIN_SDK_PACKAGE_NAMES = ["openclaw/plugin-sdk", "@openclaw/plugin-sdk"] as const;
export function listPluginSdkExportedSubpaths(
params: {
@@ -318,7 +319,9 @@ export function resolvePluginSdkScopedAliasMap(
for (const kind of orderedKinds) {
const candidate = candidateMap[kind];
if (fs.existsSync(candidate)) {
aliasMap[`openclaw/plugin-sdk/${subpath}`] = candidate;
for (const packageName of PLUGIN_SDK_PACKAGE_NAMES) {
aliasMap[`${packageName}/${subpath}`] = candidate;
}
break;
}
}
@@ -376,7 +379,12 @@ export function buildPluginLoaderAliasMap(
? { "openclaw/extension-api": normalizeJitiAliasTargetPath(extensionApiAlias) }
: {}),
...(pluginSdkAlias
? { "openclaw/plugin-sdk": normalizeJitiAliasTargetPath(pluginSdkAlias) }
? Object.fromEntries(
PLUGIN_SDK_PACKAGE_NAMES.map((packageName) => [
packageName,
normalizeJitiAliasTargetPath(pluginSdkAlias),
]),
)
: {}),
...Object.fromEntries(
Object.entries(

View File

@@ -0,0 +1,64 @@
import { spawnSync } from "node:child_process";
import { rmSync, writeFileSync } from "node:fs";
import { createRequire } from "node:module";
import { resolve } from "node:path";
import { describe, expect, it } from "vitest";
const REPO_ROOT = resolve(import.meta.dirname, "..");
const XAI_ROOT = resolve(REPO_ROOT, "extensions/xai");
const require = createRequire(import.meta.url);
const TSC_BIN = require.resolve("typescript/bin/tsc");
const PLUGIN_SDK_PACKAGE_TSCONFIG = resolve(REPO_ROOT, "packages/plugin-sdk/tsconfig.json");
function runTsc(args: string[]) {
return spawnSync(process.execPath, [TSC_BIN, ...args], {
cwd: REPO_ROOT,
encoding: "utf8",
});
}
describe("xai package TypeScript boundary", () => {
it("typechecks cleanly through @openclaw/plugin-sdk", () => {
const prepareResult = runTsc(["-p", PLUGIN_SDK_PACKAGE_TSCONFIG]);
expect(prepareResult.status, `${prepareResult.stdout}\n${prepareResult.stderr}`).toBe(0);
const result = runTsc(["-p", resolve(XAI_ROOT, "tsconfig.json"), "--noEmit"]);
expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0);
});
it("fails when xai imports src/cli through a relative path", () => {
const canaryPath = resolve(XAI_ROOT, "__rootdir_boundary_canary__.ts");
const tsconfigPath = resolve(XAI_ROOT, "tsconfig.rootdir-canary.json");
try {
writeFileSync(
canaryPath,
'import * as foo from "../../src/cli/acp-cli.ts";\nvoid foo;\nexport {};\n',
"utf8",
);
writeFileSync(
tsconfigPath,
JSON.stringify(
{
extends: "./tsconfig.json",
include: ["./__rootdir_boundary_canary__.ts"],
exclude: [],
},
null,
2,
),
"utf8",
);
const result = runTsc(["-p", tsconfigPath, "--noEmit"]);
const output = `${result.stdout}\n${result.stderr}`;
expect(result.status).not.toBe(0);
expect(output).toContain("TS6059");
expect(output).toContain("src/cli/acp-cli.ts");
} finally {
rmSync(canaryPath, { force: true });
rmSync(tsconfigPath, { force: true });
}
});
});