feat: add tlon channel plugin
This commit is contained in:
@@ -1,3 +1,6 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { getChannelPluginCatalogEntry, listChannelPluginCatalogEntries } from "./catalog.js";
|
||||
@@ -13,4 +16,37 @@ describe("channel plugin catalog", () => {
|
||||
const ids = listChannelPluginCatalogEntries().map((entry) => entry.id);
|
||||
expect(ids).toContain("msteams");
|
||||
});
|
||||
|
||||
it("includes external catalog entries", () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-catalog-"));
|
||||
const catalogPath = path.join(dir, "catalog.json");
|
||||
fs.writeFileSync(
|
||||
catalogPath,
|
||||
JSON.stringify({
|
||||
entries: [
|
||||
{
|
||||
name: "@clawdbot/demo-channel",
|
||||
clawdbot: {
|
||||
channel: {
|
||||
id: "demo-channel",
|
||||
label: "Demo Channel",
|
||||
selectionLabel: "Demo Channel",
|
||||
docsPath: "/channels/demo-channel",
|
||||
blurb: "Demo entry",
|
||||
order: 999,
|
||||
},
|
||||
install: {
|
||||
npmSpec: "@clawdbot/demo-channel",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
const ids = listChannelPluginCatalogEntries({ catalogPaths: [catalogPath] }).map(
|
||||
(entry) => entry.id,
|
||||
);
|
||||
expect(ids).toContain("demo-channel");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import { discoverClawdbotPlugins } from "../../plugins/discovery.js";
|
||||
import type { PluginOrigin } from "../../plugins/types.js";
|
||||
import type { ClawdbotPackageManifest } from "../../plugins/manifest.js";
|
||||
import { CONFIG_DIR, resolveUserPath } from "../../utils.js";
|
||||
import type { ChannelMeta } from "./types.js";
|
||||
|
||||
export type ChannelUiMetaEntry = {
|
||||
@@ -33,6 +35,7 @@ export type ChannelPluginCatalogEntry = {
|
||||
|
||||
type CatalogOptions = {
|
||||
workspaceDir?: string;
|
||||
catalogPaths?: string[];
|
||||
};
|
||||
|
||||
const ORIGIN_PRIORITY: Record<PluginOrigin, number> = {
|
||||
@@ -42,6 +45,74 @@ const ORIGIN_PRIORITY: Record<PluginOrigin, number> = {
|
||||
bundled: 3,
|
||||
};
|
||||
|
||||
type ExternalCatalogEntry = {
|
||||
name?: string;
|
||||
version?: string;
|
||||
description?: string;
|
||||
clawdbot?: ClawdbotPackageManifest;
|
||||
};
|
||||
|
||||
const DEFAULT_CATALOG_PATHS = [
|
||||
path.join(CONFIG_DIR, "mpm", "plugins.json"),
|
||||
path.join(CONFIG_DIR, "mpm", "catalog.json"),
|
||||
path.join(CONFIG_DIR, "plugins", "catalog.json"),
|
||||
];
|
||||
|
||||
const ENV_CATALOG_PATHS = ["CLAWDBOT_PLUGIN_CATALOG_PATHS", "CLAWDBOT_MPM_CATALOG_PATHS"];
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
||||
}
|
||||
|
||||
function parseCatalogEntries(raw: unknown): ExternalCatalogEntry[] {
|
||||
if (Array.isArray(raw)) {
|
||||
return raw.filter((entry): entry is ExternalCatalogEntry => isRecord(entry));
|
||||
}
|
||||
if (!isRecord(raw)) return [];
|
||||
const list = raw.entries ?? raw.packages ?? raw.plugins;
|
||||
if (!Array.isArray(list)) return [];
|
||||
return list.filter((entry): entry is ExternalCatalogEntry => isRecord(entry));
|
||||
}
|
||||
|
||||
function splitEnvPaths(value: string): string[] {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return [];
|
||||
return trimmed
|
||||
.split(/[;,]/g)
|
||||
.flatMap((chunk) => chunk.split(path.delimiter))
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function resolveExternalCatalogPaths(options: CatalogOptions): string[] {
|
||||
if (options.catalogPaths && options.catalogPaths.length > 0) {
|
||||
return options.catalogPaths.map((entry) => entry.trim()).filter(Boolean);
|
||||
}
|
||||
for (const key of ENV_CATALOG_PATHS) {
|
||||
const raw = process.env[key];
|
||||
if (raw && raw.trim()) {
|
||||
return splitEnvPaths(raw);
|
||||
}
|
||||
}
|
||||
return DEFAULT_CATALOG_PATHS;
|
||||
}
|
||||
|
||||
function loadExternalCatalogEntries(options: CatalogOptions): ExternalCatalogEntry[] {
|
||||
const paths = resolveExternalCatalogPaths(options);
|
||||
const entries: ExternalCatalogEntry[] = [];
|
||||
for (const rawPath of paths) {
|
||||
const resolved = resolveUserPath(rawPath);
|
||||
if (!fs.existsSync(resolved)) continue;
|
||||
try {
|
||||
const payload = JSON.parse(fs.readFileSync(resolved, "utf-8")) as unknown;
|
||||
entries.push(...parseCatalogEntries(payload));
|
||||
} catch {
|
||||
// Ignore invalid catalog files.
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
function toChannelMeta(params: {
|
||||
channel: NonNullable<ClawdbotPackageManifest["channel"]>;
|
||||
id: string;
|
||||
@@ -132,6 +203,13 @@ function buildCatalogEntry(candidate: {
|
||||
return { id, meta, install };
|
||||
}
|
||||
|
||||
function buildExternalCatalogEntry(entry: ExternalCatalogEntry): ChannelPluginCatalogEntry | null {
|
||||
return buildCatalogEntry({
|
||||
packageName: entry.name,
|
||||
packageClawdbot: entry.clawdbot,
|
||||
});
|
||||
}
|
||||
|
||||
export function buildChannelUiCatalog(
|
||||
plugins: Array<{ id: string; meta: ChannelMeta }>,
|
||||
): ChannelUiCatalog {
|
||||
@@ -176,6 +254,15 @@ export function listChannelPluginCatalogEntries(
|
||||
}
|
||||
}
|
||||
|
||||
const externalEntries = loadExternalCatalogEntries(options)
|
||||
.map((entry) => buildExternalCatalogEntry(entry))
|
||||
.filter((entry): entry is ChannelPluginCatalogEntry => Boolean(entry));
|
||||
for (const entry of externalEntries) {
|
||||
if (!resolved.has(entry.id)) {
|
||||
resolved.set(entry.id, { entry, priority: 99 });
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(resolved.values())
|
||||
.map(({ entry }) => entry)
|
||||
.sort((a, b) => {
|
||||
|
||||
@@ -39,6 +39,12 @@ export type ChannelSetupInput = {
|
||||
password?: string;
|
||||
deviceName?: string;
|
||||
initialSyncLimit?: number;
|
||||
ship?: string;
|
||||
url?: string;
|
||||
code?: string;
|
||||
groupChannels?: string[];
|
||||
dmAllowlist?: string[];
|
||||
autoDiscoverChannels?: boolean;
|
||||
};
|
||||
|
||||
export type ChannelStatusIssue = {
|
||||
|
||||
Reference in New Issue
Block a user