diff --git a/docs/channels/tlon.md b/docs/channels/tlon.md index f3e70c7152a..c404ef9b14b 100644 --- a/docs/channels/tlon.md +++ b/docs/channels/tlon.md @@ -21,7 +21,7 @@ Tlon ships as a plugin and is not bundled with the core install. Install via CLI (npm registry): ```bash -openclaw plugins install @openclaw/tlon +openclaw plugins install @tloncorp/openclaw ``` Local checkout (when running from a git repo): @@ -32,6 +32,9 @@ openclaw plugins install ./extensions/tlon Details: [Plugins](/tools/plugin) +Existing installs from `@openclaw/tlon` are migrated to the new package name on config load so +future `openclaw plugins update` runs resolve from `@tloncorp/openclaw`. + ## Setup 1. Install the Tlon plugin. diff --git a/docs/refactor/tlon-package-migration.md b/docs/refactor/tlon-package-migration.md new file mode 100644 index 00000000000..2965ae5cac0 --- /dev/null +++ b/docs/refactor/tlon-package-migration.md @@ -0,0 +1,73 @@ +--- +title: "Tlon Package Migration" +summary: "Plan for migrating the Tlon plugin package from @openclaw/tlon to @tloncorp/openclaw" +read_when: + - Updating Tlon onboarding/install metadata + - Migrating existing Tlon plugin installs +--- + +# Tlon Package Migration + +This document captures the OpenClaw-side plan for migrating the Tlon plugin package from +`@openclaw/tlon` to `@tloncorp/openclaw` without breaking existing installs. + +## Goals + +- Keep the plugin id and channel id as `tlon`. +- Switch new onboarding and docs to install `@tloncorp/openclaw`. +- Auto-migrate existing `plugins.installs.tlon` records that still point at `@openclaw/tlon`. +- Preserve version continuity on the Tlon side so pinned or version-qualified specs can move cleanly. +- Keep local checkout installs (`./extensions/tlon`) working as before. + +## Required invariants + +The published `@tloncorp/openclaw` package must remain a drop-in replacement for the Tlon plugin. + +- `package.json.name` should be `@tloncorp/openclaw`. +- `package.json.version` should continue the existing `2026.x.y` line instead of resetting to `0.x`. +- `openclaw.channel.id` must stay `tlon`. +- `openclaw.install.npmSpec` must be `@tloncorp/openclaw`. +- `openclaw.extensions` must point at the real published entrypoint. +- `openclaw.plugin.json.id` must stay `tlon`. +- `openclaw.plugin.json.channels` must contain `tlon`. + +## OpenClaw migration behavior + +On config load, OpenClaw should detect old Tlon npm install specs and rewrite them in place. + +- Rewrite `plugins.installs.tlon.spec` from `@openclaw/tlon` to `@tloncorp/openclaw`. +- Rewrite version-qualified specs the same way, preserving the suffix after the package name. +- Apply this to any `plugins.installs.tlon` record that still references the old package name. +- Clear stale npm resolution metadata so the next `openclaw plugins update tlon` resolves against the + new package without comparing the old package integrity hash to the new package artifact. + +Fields to clear when the spec is rewritten: + +- `resolvedName` +- `resolvedVersion` +- `resolvedSpec` +- `integrity` +- `shasum` +- `resolvedAt` + +Fields to preserve: + +- `source` +- `installPath` +- `sourcePath` +- `version` +- `installedAt` + +## Rollout + +1. Publish `@tloncorp/openclaw` with the correct manifest and version continuity. +2. Update OpenClaw onboarding metadata and docs to point new installs at `@tloncorp/openclaw`. +3. Ship the config migration so existing installs start updating from the new package name. +4. Keep `@openclaw/tlon` available during the transition window. +5. Deprecate `@openclaw/tlon` after OpenClaw releases with the migration have had time to land. + +## External follow-up in tloncorp/openclaw-tlon + +- Publish the next `2026.x.y` version under `@tloncorp/openclaw`. +- Update the README to say the plugin is installed separately, not “included with OpenClaw”. +- Verify the published tarball contains the correct `openclaw.plugin.json`. diff --git a/docs/tools/plugin.md b/docs/tools/plugin.md index a257d8b7a45..1d2cdedb5ff 100644 --- a/docs/tools/plugin.md +++ b/docs/tools/plugin.md @@ -923,7 +923,7 @@ it’s present in your workspace/managed skills locations. Recommended packaging: - Main package: `openclaw` (this repo) -- Plugins: separate npm packages under `@openclaw/*` (example: `@openclaw/voice-call`) +- Plugins: separate npm packages, usually under `@openclaw/*` (examples: `@openclaw/voice-call`, `@tloncorp/openclaw`) Publishing contract: diff --git a/extensions/tlon/README.md b/extensions/tlon/README.md index bb28ef691eb..05ef14ac8da 100644 --- a/extensions/tlon/README.md +++ b/extensions/tlon/README.md @@ -2,4 +2,6 @@ Tlon/Urbit channel plugin for OpenClaw. Supports DMs, group mentions, and thread replies. +Install from npm: `openclaw plugins install @tloncorp/openclaw` + Docs: https://docs.openclaw.ai/channels/tlon diff --git a/extensions/tlon/package.json b/extensions/tlon/package.json index 0cb79328d89..036fd7de6db 100644 --- a/extensions/tlon/package.json +++ b/extensions/tlon/package.json @@ -24,7 +24,7 @@ "quickstartAllowFrom": true }, "install": { - "npmSpec": "@openclaw/tlon", + "npmSpec": "@tloncorp/openclaw", "localPath": "extensions/tlon", "defaultChoice": "npm" }, diff --git a/src/config/legacy-migrate.test.ts b/src/config/legacy-migrate.test.ts index 4910c7f9488..5da792e5b7f 100644 --- a/src/config/legacy-migrate.test.ts +++ b/src/config/legacy-migrate.test.ts @@ -1,6 +1,62 @@ import { describe, expect, it } from "vitest"; import { migrateLegacyConfig } from "./legacy-migrate.js"; import { WHISPER_BASE_AUDIO_MODEL } from "./legacy-migrate.test-helpers.js"; +import { findLegacyConfigIssues } from "./legacy.js"; + +describe("legacy migrate tlon plugin install spec", () => { + it("flags legacy @openclaw/tlon install specs for auto-migration", () => { + const issues = findLegacyConfigIssues({ + plugins: { + installs: { + tlon: { + source: "npm", + spec: "@openclaw/tlon", + }, + }, + }, + }); + + expect(issues).toContainEqual({ + path: "plugins.installs.tlon.spec", + message: + "plugins.installs.tlon.spec moved from @openclaw/tlon to @tloncorp/openclaw (auto-migrated on load).", + }); + }); + + it("rewrites old tlon install specs and clears stale npm metadata", () => { + const res = migrateLegacyConfig({ + plugins: { + installs: { + tlon: { + source: "npm", + spec: "@openclaw/tlon@2026.2.21", + resolvedName: "@openclaw/tlon", + resolvedVersion: "2026.2.21", + resolvedSpec: "@openclaw/tlon@2026.2.21", + integrity: "sha512-old", + shasum: "old", + resolvedAt: "2026-03-01T00:00:00.000Z", + installPath: "/tmp/tlon", + version: "2026.2.21", + }, + }, + }, + }); + + expect(res.changes).toContain( + "Moved plugins.installs.tlon.spec → @tloncorp/openclaw@2026.2.21 and cleared stale npm resolution metadata.", + ); + expect(res.config?.plugins?.installs?.tlon?.spec).toBe("@tloncorp/openclaw@2026.2.21"); + expect(res.config?.plugins?.installs?.tlon?.version).toBe("2026.2.21"); + expect(res.config?.plugins?.installs?.tlon?.installPath).toBe("/tmp/tlon"); + expect(res.config?.plugins?.installs?.tlon?.resolvedName).toBeUndefined(); + expect(res.config?.plugins?.installs?.tlon?.resolvedVersion).toBeUndefined(); + expect(res.config?.plugins?.installs?.tlon?.resolvedSpec).toBeUndefined(); + expect(res.config?.plugins?.installs?.tlon?.integrity).toBeUndefined(); + expect(res.config?.plugins?.installs?.tlon?.shasum).toBeUndefined(); + expect(res.config?.plugins?.installs?.tlon?.resolvedAt).toBeUndefined(); + }); +}); describe("legacy migrate audio transcription", () => { it("moves routing.transcribeAudio into tools.media.audio.models", () => { diff --git a/src/config/legacy.migrations.part-3.ts b/src/config/legacy.migrations.part-3.ts index ccc07b4b99f..324c5fa4414 100644 --- a/src/config/legacy.migrations.part-3.ts +++ b/src/config/legacy.migrations.part-3.ts @@ -35,6 +35,16 @@ const AGENT_HEARTBEAT_KEYS = new Set([ const CHANNEL_HEARTBEAT_KEYS = new Set(["showOk", "showAlerts", "useIndicator"]); +function migrateLegacyTlonInstallSpec(spec: string): string | null { + if (spec === "@openclaw/tlon") { + return "@tloncorp/openclaw"; + } + if (spec.startsWith("@openclaw/tlon@")) { + return `@tloncorp/openclaw${spec.slice("@openclaw/tlon".length)}`; + } + return null; +} + function splitLegacyHeartbeat(legacyHeartbeat: Record): { agentHeartbeat: Record | null; channelHeartbeat: Record | null; @@ -97,6 +107,35 @@ function mergeLegacyIntoDefaults(params: { // tools.alsoAllow legacy migration intentionally omitted (field not shipped in prod). export const LEGACY_CONFIG_MIGRATIONS_PART_3: LegacyConfigMigration[] = [ + { + id: "plugins.installs.tlon.spec->tloncorp", + describe: "Move Tlon plugin install records to @tloncorp/openclaw", + apply: (raw, changes) => { + const plugins = getRecord(raw.plugins); + const installs = getRecord(plugins?.installs); + const tlon = getRecord(installs?.tlon); + const spec = typeof tlon?.spec === "string" ? tlon.spec : null; + if (!tlon || !spec) { + return; + } + + const nextSpec = migrateLegacyTlonInstallSpec(spec); + if (!nextSpec) { + return; + } + + tlon.spec = nextSpec; + delete tlon.resolvedName; + delete tlon.resolvedVersion; + delete tlon.resolvedSpec; + delete tlon.integrity; + delete tlon.shasum; + delete tlon.resolvedAt; + changes.push( + `Moved plugins.installs.tlon.spec → ${nextSpec} and cleared stale npm resolution metadata.`, + ); + }, + }, { // v2026.2.26 added a startup guard requiring gateway.controlUi.allowedOrigins (or the // host-header fallback flag) for any non-loopback bind. The onboarding wizard was updated diff --git a/src/config/legacy.rules.ts b/src/config/legacy.rules.ts index 420f6a4685d..c418e907740 100644 --- a/src/config/legacy.rules.ts +++ b/src/config/legacy.rules.ts @@ -46,6 +46,13 @@ function isLegacyGatewayBindHostAlias(value: unknown): boolean { ); } +function isLegacyTlonInstallSpec(value: unknown): boolean { + if (typeof value !== "string") { + return false; + } + return value === "@openclaw/tlon" || value.startsWith("@openclaw/tlon@"); +} + export const LEGACY_CONFIG_RULES: LegacyConfigRule[] = [ { path: ["whatsapp"], @@ -209,4 +216,10 @@ export const LEGACY_CONFIG_RULES: LegacyConfigRule[] = [ message: "top-level heartbeat is not a valid config path; use agents.defaults.heartbeat (cadence/target/model settings) or channels.defaults.heartbeat (showOk/showAlerts/useIndicator).", }, + { + path: ["plugins", "installs", "tlon", "spec"], + message: + "plugins.installs.tlon.spec moved from @openclaw/tlon to @tloncorp/openclaw (auto-migrated on load).", + match: (value) => isLegacyTlonInstallSpec(value), + }, ];