diff --git a/docs/plans/2026-04-05-002-refactor-real-plugin-sdk-workspace-plan.md b/docs/plans/2026-04-05-002-refactor-real-plugin-sdk-workspace-plan.md new file mode 100644 index 00000000000..7d57191e553 --- /dev/null +++ b/docs/plans/2026-04-05-002-refactor-real-plugin-sdk-workspace-plan.md @@ -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/.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` diff --git a/extensions/tsconfig.package-boundary.base.json b/extensions/tsconfig.package-boundary.base.json new file mode 100644 index 00000000000..debd7beb5a5 --- /dev/null +++ b/extensions/tsconfig.package-boundary.base.json @@ -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"] + } + } +} diff --git a/extensions/xai/api.ts b/extensions/xai/api.ts index 1a0d9fe8bb9..3208079cb44 100644 --- a/extensions/xai/api.ts +++ b/extensions/xai/api.ts @@ -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 ( diff --git a/extensions/xai/code-execution.test.ts b/extensions/xai/code-execution.test.ts index 4810bebcc8a..876cebc7d10 100644 --- a/extensions/xai/code-execution.test.ts +++ b/extensions/xai/code-execution.test.ts @@ -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"; diff --git a/extensions/xai/code-execution.ts b/extensions/xai/code-execution.ts index 1542e6e6e2b..49345b7f7d4 100644 --- a/extensions/xai/code-execution.ts +++ b/extensions/xai/code-execution.ts @@ -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, diff --git a/extensions/xai/index.ts b/extensions/xai/index.ts index 30662e77347..11643320d4b 100644 --- a/extensions/xai/index.ts +++ b/extensions/xai/index.ts @@ -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, diff --git a/extensions/xai/model-definitions.ts b/extensions/xai/model-definitions.ts index 628f59a2583..3de78c5b741 100644 --- a/extensions/xai/model-definitions.ts +++ b/extensions/xai/model-definitions.ts @@ -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"; diff --git a/extensions/xai/onboard.test.ts b/extensions/xai/onboard.test.ts index 769ed6ef6ef..a8c0f330f5a 100644 --- a/extensions/xai/onboard.test.ts +++ b/extensions/xai/onboard.test.ts @@ -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, diff --git a/extensions/xai/onboard.ts b/extensions/xai/onboard.ts index bf4b4967fdf..9a9cddd0bfa 100644 --- a/extensions/xai/onboard.ts +++ b/extensions/xai/onboard.ts @@ -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"; diff --git a/extensions/xai/package.json b/extensions/xai/package.json index 3e093f83bbd..148964f2a40 100644 --- a/extensions/xai/package.json +++ b/extensions/xai/package.json @@ -4,6 +4,9 @@ "private": true, "description": "OpenClaw xAI plugin", "type": "module", + "devDependencies": { + "@openclaw/plugin-sdk": "workspace:*" + }, "openclaw": { "extensions": [ "./index.ts" diff --git a/extensions/xai/provider-catalog.ts b/extensions/xai/provider-catalog.ts index f8c248d60de..9dca2fb1957 100644 --- a/extensions/xai/provider-catalog.ts +++ b/extensions/xai/provider-catalog.ts @@ -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( diff --git a/extensions/xai/provider-models.ts b/extensions/xai/provider-models.ts index cac7a1bf59d..214044bef39 100644 --- a/extensions/xai/provider-models.ts +++ b/extensions/xai/provider-models.ts @@ -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"; diff --git a/extensions/xai/setup-api.ts b/extensions/xai/setup-api.ts index 9e7a0382d48..57e617c85e3 100644 --- a/extensions/xai/setup-api.ts +++ b/extensions/xai/setup-api.ts @@ -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 { return Boolean(value) && typeof value === "object" && !Array.isArray(value); diff --git a/extensions/xai/src/code-execution-shared.ts b/extensions/xai/src/code-execution-shared.ts index f274348eba5..3c34c269bde 100644 --- a/extensions/xai/src/code-execution-shared.ts +++ b/extensions/xai/src/code-execution-shared.ts @@ -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, diff --git a/extensions/xai/src/tool-auth-shared.test.ts b/extensions/xai/src/tool-auth-shared.test.ts index 2c281b6a06e..6a6bd80f3ee 100644 --- a/extensions/xai/src/tool-auth-shared.test.ts +++ b/extensions/xai/src/tool-auth-shared.test.ts @@ -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, diff --git a/extensions/xai/src/tool-auth-shared.ts b/extensions/xai/src/tool-auth-shared.ts index 0ef880bb673..3122ddf0b22 100644 --- a/extensions/xai/src/tool-auth-shared.ts +++ b/extensions/xai/src/tool-auth-shared.ts @@ -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; diff --git a/extensions/xai/src/web-search-shared.ts b/extensions/xai/src/web-search-shared.ts index 1c914e642fd..af1d8ad8c64 100644 --- a/extensions/xai/src/web-search-shared.ts +++ b/extensions/xai/src/web-search-shared.ts @@ -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, diff --git a/extensions/xai/src/x-search-config.ts b/extensions/xai/src/x-search-config.ts index a692059e14f..2cb9baeec24 100644 --- a/extensions/xai/src/x-search-config.ts +++ b/extensions/xai/src/x-search-config.ts @@ -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; diff --git a/extensions/xai/src/x-search-shared.ts b/extensions/xai/src/x-search-shared.ts index d4295063b80..f853f58c3b4 100644 --- a/extensions/xai/src/x-search-shared.ts +++ b/extensions/xai/src/x-search-shared.ts @@ -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, diff --git a/extensions/xai/stream.ts b/extensions/xai/stream.ts index 412f24a0e82..c55050b85e2 100644 --- a/extensions/xai/stream.ts +++ b/extensions/xai/stream.ts @@ -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([ ["grok-3", "grok-3-fast"], diff --git a/extensions/xai/tsconfig.json b/extensions/xai/tsconfig.json new file mode 100644 index 00000000000..b1eac8b8a69 --- /dev/null +++ b/extensions/xai/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.package-boundary.base.json", + "compilerOptions": { + "rootDir": "." + }, + "include": ["./*.ts", "./src/**/*.ts"], + "exclude": ["./**/*.test.ts", "./dist/**", "./node_modules/**"] +} diff --git a/extensions/xai/video-generation-provider.test.ts b/extensions/xai/video-generation-provider.test.ts index 314ee80ac0f..1ca9eae31d0 100644 --- a/extensions/xai/video-generation-provider.test.ts +++ b/extensions/xai/video-generation-provider.test.ts @@ -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, diff --git a/extensions/xai/video-generation-provider.ts b/extensions/xai/video-generation-provider.ts index ed45b0bd8ff..9cfbc435b79 100644 --- a/extensions/xai/video-generation-provider.ts +++ b/extensions/xai/video-generation-provider.ts @@ -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"; diff --git a/extensions/xai/web-search.test.ts b/extensions/xai/web-search.test.ts index 9681a6fdecf..3e422730de7 100644 --- a/extensions/xai/web-search.test.ts +++ b/extensions/xai/web-search.test.ts @@ -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"; diff --git a/extensions/xai/web-search.ts b/extensions/xai/web-search.ts index 34b81b95be8..257f17afc75 100644 --- a/extensions/xai/web-search.ts +++ b/extensions/xai/web-search.ts @@ -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, diff --git a/extensions/xai/x-search.test.ts b/extensions/xai/x-search.test.ts index f6e57052abe..fa2c2199ce6 100644 --- a/extensions/xai/x-search.test.ts +++ b/extensions/xai/x-search.test.ts @@ -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"; diff --git a/extensions/xai/x-search.ts b/extensions/xai/x-search.ts index 24fc47f45d1..fa448f5b72a 100644 --- a/extensions/xai/x-search.ts +++ b/extensions/xai/x-search.ts @@ -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, diff --git a/packages/plugin-sdk/package.json b/packages/plugin-sdk/package.json new file mode 100644 index 00000000000..51f66a39d89 --- /dev/null +++ b/packages/plugin-sdk/package.json @@ -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" + } + } +} diff --git a/packages/plugin-sdk/src/config-runtime.ts b/packages/plugin-sdk/src/config-runtime.ts new file mode 100644 index 00000000000..32ca4641d68 --- /dev/null +++ b/packages/plugin-sdk/src/config-runtime.ts @@ -0,0 +1 @@ +export * from "../../../src/plugin-sdk/config-runtime.js"; diff --git a/packages/plugin-sdk/src/plugin-entry.ts b/packages/plugin-sdk/src/plugin-entry.ts new file mode 100644 index 00000000000..82796abdb21 --- /dev/null +++ b/packages/plugin-sdk/src/plugin-entry.ts @@ -0,0 +1 @@ +export * from "../../../src/plugin-sdk/plugin-entry.js"; diff --git a/packages/plugin-sdk/src/provider-auth-runtime.ts b/packages/plugin-sdk/src/provider-auth-runtime.ts new file mode 100644 index 00000000000..aa2127a46c8 --- /dev/null +++ b/packages/plugin-sdk/src/provider-auth-runtime.ts @@ -0,0 +1 @@ +export * from "../../../src/plugin-sdk/provider-auth-runtime.js"; diff --git a/packages/plugin-sdk/src/provider-auth.ts b/packages/plugin-sdk/src/provider-auth.ts new file mode 100644 index 00000000000..0900e0092eb --- /dev/null +++ b/packages/plugin-sdk/src/provider-auth.ts @@ -0,0 +1 @@ +export * from "../../../src/plugin-sdk/provider-auth.js"; diff --git a/packages/plugin-sdk/src/provider-entry.ts b/packages/plugin-sdk/src/provider-entry.ts new file mode 100644 index 00000000000..483b35a04a6 --- /dev/null +++ b/packages/plugin-sdk/src/provider-entry.ts @@ -0,0 +1 @@ +export * from "../../../src/plugin-sdk/provider-entry.js"; diff --git a/packages/plugin-sdk/src/provider-http.ts b/packages/plugin-sdk/src/provider-http.ts new file mode 100644 index 00000000000..40363e19098 --- /dev/null +++ b/packages/plugin-sdk/src/provider-http.ts @@ -0,0 +1 @@ +export * from "../../../src/plugin-sdk/provider-http.js"; diff --git a/packages/plugin-sdk/src/provider-model-shared.ts b/packages/plugin-sdk/src/provider-model-shared.ts new file mode 100644 index 00000000000..ec731f794b2 --- /dev/null +++ b/packages/plugin-sdk/src/provider-model-shared.ts @@ -0,0 +1 @@ +export * from "../../../src/plugin-sdk/provider-model-shared.js"; diff --git a/packages/plugin-sdk/src/provider-onboard.ts b/packages/plugin-sdk/src/provider-onboard.ts new file mode 100644 index 00000000000..06835fac821 --- /dev/null +++ b/packages/plugin-sdk/src/provider-onboard.ts @@ -0,0 +1 @@ +export * from "../../../src/plugin-sdk/provider-onboard.js"; diff --git a/packages/plugin-sdk/src/provider-stream-shared.ts b/packages/plugin-sdk/src/provider-stream-shared.ts new file mode 100644 index 00000000000..e2aa286a97e --- /dev/null +++ b/packages/plugin-sdk/src/provider-stream-shared.ts @@ -0,0 +1 @@ +export * from "../../../src/plugin-sdk/provider-stream-shared.js"; diff --git a/packages/plugin-sdk/src/provider-tools.ts b/packages/plugin-sdk/src/provider-tools.ts new file mode 100644 index 00000000000..a962aeef2d2 --- /dev/null +++ b/packages/plugin-sdk/src/provider-tools.ts @@ -0,0 +1 @@ +export * from "../../../src/plugin-sdk/provider-tools.js"; diff --git a/packages/plugin-sdk/src/provider-web-search.ts b/packages/plugin-sdk/src/provider-web-search.ts new file mode 100644 index 00000000000..0748e44d53e --- /dev/null +++ b/packages/plugin-sdk/src/provider-web-search.ts @@ -0,0 +1 @@ +export * from "../../../src/plugin-sdk/provider-web-search.js"; diff --git a/packages/plugin-sdk/src/runtime-env.ts b/packages/plugin-sdk/src/runtime-env.ts new file mode 100644 index 00000000000..94a7ef64112 --- /dev/null +++ b/packages/plugin-sdk/src/runtime-env.ts @@ -0,0 +1 @@ +export * from "../../../src/plugin-sdk/runtime-env.js"; diff --git a/packages/plugin-sdk/src/secret-input.ts b/packages/plugin-sdk/src/secret-input.ts new file mode 100644 index 00000000000..a57ad62fa31 --- /dev/null +++ b/packages/plugin-sdk/src/secret-input.ts @@ -0,0 +1 @@ +export * from "../../../src/plugin-sdk/secret-input.js"; diff --git a/packages/plugin-sdk/src/testing.ts b/packages/plugin-sdk/src/testing.ts new file mode 100644 index 00000000000..d6206c242e4 --- /dev/null +++ b/packages/plugin-sdk/src/testing.ts @@ -0,0 +1 @@ +export * from "../../../src/plugin-sdk/testing.js"; diff --git a/packages/plugin-sdk/src/video-generation.ts b/packages/plugin-sdk/src/video-generation.ts new file mode 100644 index 00000000000..da68e6c43eb --- /dev/null +++ b/packages/plugin-sdk/src/video-generation.ts @@ -0,0 +1 @@ +export * from "../../../src/plugin-sdk/video-generation.js"; diff --git a/packages/plugin-sdk/tsconfig.json b/packages/plugin-sdk/tsconfig.json new file mode 100644 index 00000000000..fc1734f569e --- /dev/null +++ b/packages/plugin-sdk/tsconfig.json @@ -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"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b8c7dcd2537..52e443b419e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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': diff --git a/src/plugin-sdk/image-generation-core.ts b/src/plugin-sdk/image-generation-core.ts index 65670f92408..ae2f094bd38 100644 --- a/src/plugin-sdk/image-generation-core.ts +++ b/src/plugin-sdk/image-generation-core.ts @@ -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, diff --git a/src/plugin-sdk/video-generation-core.ts b/src/plugin-sdk/video-generation-core.ts index dcbd007463a..972cf08c907 100644 --- a/src/plugin-sdk/video-generation-core.ts +++ b/src/plugin-sdk/video-generation-core.ts @@ -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, diff --git a/src/plugins/bundled-capability-runtime.ts b/src/plugins/bundled-capability-runtime.ts index 09eb2fb2b16..199068d55ea 100644 --- a/src/plugins/bundled-capability-runtime.ts +++ b/src/plugins/bundled-capability-runtime.ts @@ -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), + ), }; } diff --git a/src/plugins/contracts/extension-package-project-boundaries.test.ts b/src/plugins/contracts/extension-package-project-boundaries.test.ts new file mode 100644 index 00000000000..70145c40e64 --- /dev/null +++ b/src/plugins/contracts/extension-package-project-boundaries.test.ts @@ -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; + devDependencies?: Record; +}; + +function readJsonFile(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("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("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("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("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("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, + ); + }); +}); diff --git a/src/plugins/runtime/runtime-plugin-boundary.ts b/src/plugins/runtime/runtime-plugin-boundary.ts index 3540c1e6a93..03298c833d6 100644 --- a/src/plugins/runtime/runtime-plugin-boundary.ts +++ b/src/plugins/runtime/runtime-plugin-boundary.ts @@ -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, { diff --git a/src/plugins/sdk-alias.test.ts b/src/plugins/sdk-alias.test.ts index d68de4fc163..df236db100d 100644 --- a/src/plugins/sdk-alias.test.ts +++ b/src/plugins/sdk-alias.test.ts @@ -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, }); diff --git a/src/plugins/sdk-alias.ts b/src/plugins/sdk-alias.ts index 239a4bc55c6..1a913ecc4c3 100644 --- a/src/plugins/sdk-alias.ts +++ b/src/plugins/sdk-alias.ts @@ -250,6 +250,7 @@ export function resolvePluginSdkAliasFile(params: { const cachedPluginSdkExportedSubpaths = new Map(); const cachedPluginSdkScopedAliasMaps = new Map>(); +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( diff --git a/test/extension-package-tsc-boundary.test.ts b/test/extension-package-tsc-boundary.test.ts new file mode 100644 index 00000000000..15a7eb3b8fc --- /dev/null +++ b/test/extension-package-tsc-boundary.test.ts @@ -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 }); + } + }); +});