Compare commits
3 Commits
main
...
tlon-packa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
89c59c50d5 | ||
|
|
e244bf893b | ||
|
|
584f6bea5b |
@ -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.
|
||||
|
||||
73
docs/refactor/tlon-package-migration.md
Normal file
73
docs/refactor/tlon-package-migration.md
Normal file
@ -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`.
|
||||
@ -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:
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -24,7 +24,7 @@
|
||||
"quickstartAllowFrom": true
|
||||
},
|
||||
"install": {
|
||||
"npmSpec": "@openclaw/tlon",
|
||||
"npmSpec": "@tloncorp/openclaw",
|
||||
"localPath": "extensions/tlon",
|
||||
"defaultChoice": "npm"
|
||||
},
|
||||
|
||||
@ -360,7 +360,7 @@ describe("config strict validation", () => {
|
||||
expect(res.ok).toBe(false);
|
||||
});
|
||||
|
||||
it("flags legacy config entries without auto-migrating", async () => {
|
||||
it("keeps auto-migrated legacy config entries valid in snapshots", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
await writeOpenClawConfig(home, {
|
||||
agents: { list: [{ id: "pi" }] },
|
||||
@ -368,9 +368,12 @@ describe("config strict validation", () => {
|
||||
});
|
||||
|
||||
const snap = await readConfigFileSnapshot();
|
||||
const snapshotConfig = snap.config as { routing?: unknown };
|
||||
|
||||
expect(snap.valid).toBe(false);
|
||||
expect(snap.valid).toBe(true);
|
||||
expect(snap.legacyIssues).not.toHaveLength(0);
|
||||
expect(snapshotConfig.routing).toBeUndefined();
|
||||
expect(snap.config.channels?.whatsapp?.allowFrom).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@ -404,8 +407,9 @@ describe("config strict validation", () => {
|
||||
});
|
||||
|
||||
const snap = await readConfigFileSnapshot();
|
||||
expect(snap.valid).toBe(false);
|
||||
expect(snap.valid).toBe(true);
|
||||
expect(snap.legacyIssues.some((issue) => issue.path === "gateway.bind")).toBe(true);
|
||||
expect(snap.config.gateway?.bind).toBe("lan");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -68,8 +68,11 @@ function expectRoutingAllowFromLegacySnapshot(
|
||||
ctx: { snapshot: ConfigSnapshot; parsed: unknown },
|
||||
expectedAllowFrom: string[],
|
||||
) {
|
||||
expect(ctx.snapshot.valid).toBe(false);
|
||||
const snapshotConfig = ctx.snapshot.config as { routing?: unknown };
|
||||
expect(ctx.snapshot.valid).toBe(true);
|
||||
expect(ctx.snapshot.legacyIssues.some((issue) => issue.path === "routing.allowFrom")).toBe(true);
|
||||
expect(snapshotConfig.routing).toBeUndefined();
|
||||
expect(ctx.snapshot.config.channels?.whatsapp?.allowFrom).toBeUndefined();
|
||||
const parsed = ctx.parsed as {
|
||||
routing?: { allowFrom?: string[] };
|
||||
channels?: unknown;
|
||||
@ -269,8 +272,12 @@ describe("legacy config detection", () => {
|
||||
await withSnapshotForConfig(
|
||||
{ memorySearch: { provider: "local", fallback: "none" } },
|
||||
async (ctx) => {
|
||||
expect(ctx.snapshot.valid).toBe(false);
|
||||
expect(ctx.snapshot.valid).toBe(true);
|
||||
expect(ctx.snapshot.legacyIssues.some((issue) => issue.path === "memorySearch")).toBe(true);
|
||||
expect(ctx.snapshot.config.agents?.defaults?.memorySearch).toEqual({
|
||||
provider: "local",
|
||||
fallback: "none",
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
@ -285,8 +292,9 @@ describe("legacy config detection", () => {
|
||||
});
|
||||
it("flags legacy provider sections in snapshot", async () => {
|
||||
await withSnapshotForConfig({ whatsapp: { allowFrom: ["+1555"] } }, async (ctx) => {
|
||||
expect(ctx.snapshot.valid).toBe(false);
|
||||
expect(ctx.snapshot.valid).toBe(true);
|
||||
expect(ctx.snapshot.legacyIssues.some((issue) => issue.path === "whatsapp")).toBe(true);
|
||||
expect(ctx.snapshot.config.channels?.whatsapp?.allowFrom).toEqual(["+1555"]);
|
||||
|
||||
const parsed = ctx.parsed as {
|
||||
channels?: unknown;
|
||||
|
||||
@ -3,6 +3,7 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { createConfigIO } from "./io.js";
|
||||
import { migrateLegacyConfig } from "./legacy-migrate.js";
|
||||
|
||||
async function withTempHome(run: (home: string) => Promise<void>): Promise<void> {
|
||||
const home = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-config-"));
|
||||
@ -166,4 +167,107 @@ describe("config io paths", () => {
|
||||
expect(logger.error).toHaveBeenCalledWith(expect.stringContaining("- gateway.port:"));
|
||||
});
|
||||
});
|
||||
|
||||
it("auto-migrates legacy tlon install specs during load and snapshot reads", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const configDir = path.join(home, ".openclaw");
|
||||
await fs.mkdir(configDir, { recursive: true });
|
||||
const configPath = path.join(configDir, "openclaw.json");
|
||||
await fs.writeFile(
|
||||
configPath,
|
||||
JSON.stringify(
|
||||
{
|
||||
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",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const io = createIoForHome(home);
|
||||
expect(io.loadConfig().plugins?.installs?.tlon?.spec).toBe("@tloncorp/openclaw@2026.2.21");
|
||||
expect(io.loadConfig().plugins?.installs?.tlon?.resolvedSpec).toBeUndefined();
|
||||
|
||||
const snapshot = await io.readConfigFileSnapshot();
|
||||
expect(snapshot.valid).toBe(true);
|
||||
expect(
|
||||
snapshot.legacyIssues.some((issue) => issue.path === "plugins.installs.tlon.spec"),
|
||||
).toBe(true);
|
||||
expect(snapshot.config.plugins?.installs?.tlon?.spec).toBe("@tloncorp/openclaw@2026.2.21");
|
||||
expect(snapshot.config.plugins?.installs?.tlon?.resolvedSpec).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("persists migrated tlon install specs back to disk", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const configDir = path.join(home, ".openclaw");
|
||||
await fs.mkdir(configDir, { recursive: true });
|
||||
const configPath = path.join(configDir, "openclaw.json");
|
||||
await fs.writeFile(
|
||||
configPath,
|
||||
JSON.stringify(
|
||||
{
|
||||
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",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const io = createIoForHome(home);
|
||||
const parsed = JSON.parse(await fs.readFile(configPath, "utf-8")) as unknown;
|
||||
const migrated = migrateLegacyConfig(parsed);
|
||||
|
||||
expect(migrated.config).toBeTruthy();
|
||||
await io.writeConfigFile(migrated.config!);
|
||||
|
||||
const written = JSON.parse(await fs.readFile(configPath, "utf-8")) as {
|
||||
plugins?: {
|
||||
installs?: {
|
||||
tlon?: {
|
||||
spec?: string;
|
||||
resolvedSpec?: string;
|
||||
resolvedName?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
expect(written.plugins?.installs?.tlon?.spec).toBe("@tloncorp/openclaw@2026.2.21");
|
||||
expect(written.plugins?.installs?.tlon?.resolvedSpec).toBeUndefined();
|
||||
expect(written.plugins?.installs?.tlon?.resolvedName).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -41,7 +41,7 @@ import {
|
||||
readConfigIncludeFileWithGuards,
|
||||
resolveConfigIncludes,
|
||||
} from "./includes.js";
|
||||
import { findLegacyConfigIssues } from "./legacy.js";
|
||||
import { applyLegacyMigrations, findLegacyConfigIssues } from "./legacy.js";
|
||||
import { applyMergePatch } from "./merge-patch.js";
|
||||
import { normalizeExecSafeBinProfilesInConfig } from "./normalize-exec-safe-bin.js";
|
||||
import { normalizeConfigPaths } from "./normalize-paths.js";
|
||||
@ -197,6 +197,13 @@ function hasOwnObjectKey(value: Record<string, unknown>, key: string): boolean {
|
||||
return Object.prototype.hasOwnProperty.call(value, key);
|
||||
}
|
||||
|
||||
function shouldAutoMigrateLegacyIssues(legacyIssues: LegacyConfigIssue[]): boolean {
|
||||
return (
|
||||
legacyIssues.length > 0 &&
|
||||
legacyIssues.every((issue) => issue.message.includes("(auto-migrated on load)."))
|
||||
);
|
||||
}
|
||||
|
||||
const WRITE_PRUNED_OBJECT = Symbol("write-pruned-object");
|
||||
|
||||
type UnsetPathWriteResult = {
|
||||
@ -705,6 +712,58 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
const configPath =
|
||||
candidatePaths.find((candidate) => deps.fs.existsSync(candidate)) ?? requestedConfigPath;
|
||||
|
||||
function validateResolvedConfigWithAutoMigration(
|
||||
resolvedConfigRaw: unknown,
|
||||
sourceRaw?: unknown,
|
||||
) {
|
||||
const legacyIssues = findLegacyConfigIssues(resolvedConfigRaw, sourceRaw);
|
||||
if (shouldAutoMigrateLegacyIssues(legacyIssues)) {
|
||||
const migrated = applyLegacyMigrations(resolvedConfigRaw);
|
||||
if (migrated.next) {
|
||||
const validatedMigrated = validateConfigObjectWithPlugins(migrated.next);
|
||||
if (validatedMigrated.ok) {
|
||||
return {
|
||||
legacyIssues,
|
||||
validated: validatedMigrated,
|
||||
effectiveResolvedConfigRaw: migrated.next,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
legacyIssues,
|
||||
validated: validateConfigObjectWithPlugins(resolvedConfigRaw),
|
||||
effectiveResolvedConfigRaw: resolvedConfigRaw,
|
||||
};
|
||||
}
|
||||
|
||||
function validateResolvedRawConfigWithAutoMigration(
|
||||
resolvedConfigRaw: unknown,
|
||||
sourceRaw?: unknown,
|
||||
) {
|
||||
const legacyIssues = findLegacyConfigIssues(resolvedConfigRaw, sourceRaw);
|
||||
if (shouldAutoMigrateLegacyIssues(legacyIssues)) {
|
||||
const migrated = applyLegacyMigrations(resolvedConfigRaw);
|
||||
if (migrated.next) {
|
||||
const validatedMigrated = validateConfigObjectRawWithPlugins(migrated.next);
|
||||
if (validatedMigrated.ok) {
|
||||
return {
|
||||
legacyIssues,
|
||||
validated: validatedMigrated,
|
||||
effectiveResolvedConfigRaw: migrated.next,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
legacyIssues,
|
||||
validated: validateConfigObjectRawWithPlugins(resolvedConfigRaw),
|
||||
effectiveResolvedConfigRaw: resolvedConfigRaw,
|
||||
};
|
||||
}
|
||||
|
||||
function loadConfig(): OpenClawConfig {
|
||||
try {
|
||||
maybeLoadDotEnvForConfig(deps.env);
|
||||
@ -743,7 +802,7 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
if (preValidationDuplicates.length > 0) {
|
||||
throw new DuplicateAgentDirError(preValidationDuplicates);
|
||||
}
|
||||
const validated = validateConfigObjectWithPlugins(resolvedConfig);
|
||||
const { validated } = validateResolvedConfigWithAutoMigration(resolvedConfig, parsed);
|
||||
if (!validated.ok) {
|
||||
const details = validated.issues
|
||||
.map(
|
||||
@ -949,11 +1008,8 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
}));
|
||||
|
||||
const resolvedConfigRaw = readResolution.resolvedConfigRaw;
|
||||
// Detect legacy keys on resolved config, but only mark source-literal legacy
|
||||
// entries (for auto-migration) when they are present in the parsed source.
|
||||
const legacyIssues = findLegacyConfigIssues(resolvedConfigRaw, parsedRes.parsed);
|
||||
|
||||
const validated = validateConfigObjectWithPlugins(resolvedConfigRaw);
|
||||
const { legacyIssues, validated, effectiveResolvedConfigRaw } =
|
||||
validateResolvedConfigWithAutoMigration(resolvedConfigRaw, parsedRes.parsed);
|
||||
if (!validated.ok) {
|
||||
return {
|
||||
snapshot: {
|
||||
@ -961,7 +1017,7 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
exists: true,
|
||||
raw,
|
||||
parsed: parsedRes.parsed,
|
||||
resolved: coerceConfig(resolvedConfigRaw),
|
||||
resolved: coerceConfig(effectiveResolvedConfigRaw),
|
||||
valid: false,
|
||||
config: coerceConfig(resolvedConfigRaw),
|
||||
hash,
|
||||
@ -993,7 +1049,7 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
parsed: parsedRes.parsed,
|
||||
// Use resolvedConfigRaw (after $include and ${ENV} substitution but BEFORE runtime defaults)
|
||||
// for config set/unset operations (issue #6070)
|
||||
resolved: coerceConfig(resolvedConfigRaw),
|
||||
resolved: coerceConfig(effectiveResolvedConfigRaw),
|
||||
valid: true,
|
||||
config: snapshotConfig,
|
||||
hash,
|
||||
@ -1090,7 +1146,7 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
const validated = validateConfigObjectRawWithPlugins(persistCandidate);
|
||||
const { validated } = validateResolvedRawConfigWithAutoMigration(persistCandidate);
|
||||
if (!validated.ok) {
|
||||
const issue = validated.issues[0];
|
||||
const pathLabel = issue?.path ? issue.path : "<root>";
|
||||
|
||||
@ -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", () => {
|
||||
|
||||
@ -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<string, unknown>): {
|
||||
agentHeartbeat: Record<string, unknown> | null;
|
||||
channelHeartbeat: Record<string, unknown> | 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
|
||||
|
||||
@ -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),
|
||||
},
|
||||
];
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user