decocms
Version:
CLI for managing deco.chat apps & projects
341 lines (336 loc) • 11.1 kB
JavaScript
// deno-lint-ignore-file no-explicit-any
import { compile } from "json-schema-to-typescript";
import { generateName } from "json-schema-to-typescript/dist/src/utils.js";
import { MD5 } from "object-hash";
import prettier from "prettier";
import { readWranglerConfig } from "../../lib/config.js";
import { createWorkspaceClient } from "../../lib/mcp.js";
import { parser as scopeParser } from "../../lib/parse-binding-tool.js";
const toValidProperty = (property) => {
return isValidJavaScriptPropertyName(property) ? property : `["${property}"]`;
};
// Sanitize description for safe use in JSDoc block comments
const formatDescription = (desc) => {
if (!desc)
return "";
return (desc
// Escape */ sequences that would break the comment block
.replace(/\*\//g, "*\\/")
// Normalize line endings
.replace(/\r\n/g, "\n")
.replace(/\r/g, "\n")
// Split into lines and format each line
.split("\n")
.map((line) => line.trim())
.filter((line) => line.length > 0)
.map((line) => ` * ${line}`)
.join("\n"));
};
function slugify(name) {
return name.replace(/[^a-zA-Z0-9]/g, "").toLowerCase();
}
export function format(content) {
try {
return prettier.format(content, {
parser: "babel-ts",
plugins: [],
});
}
catch {
// Fallback to unformatted content
return Promise.resolve(content);
}
}
// Shared list of reserved JavaScript keywords
const RESERVED_KEYWORDS = [
"break",
"case",
"catch",
"class",
"const",
"continue",
"debugger",
"default",
"delete",
"do",
"else",
"export",
"extends",
"finally",
"for",
"function",
"if",
"import",
"in",
"instanceof",
"let",
"new",
"return",
"super",
"switch",
"this",
"throw",
"try",
"typeof",
"var",
"void",
"while",
"with",
"yield",
"enum",
"await",
"implements",
"interface",
"package",
"private",
"protected",
"public",
"static",
"abstract",
"boolean",
"byte",
"char",
"double",
"final",
"float",
"goto",
"int",
"long",
"native",
"short",
"synchronized",
"throws",
"transient",
"volatile",
"null",
"true",
"false",
"undefined",
"NaN",
"Infinity",
];
function isValidJavaScriptPropertyName(name) {
// Check if it's a valid JavaScript identifier
const validIdentifierRegex = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
if (!validIdentifierRegex.test(name)) {
return false;
}
// Check for reserved keywords
return !RESERVED_KEYWORDS.includes(name);
}
const CONTRACTS_BINDING = "@deco/contracts";
const DEFAULT_BINDINGS = [
{
name: "DECO_CHAT_WORKSPACE_API",
integration_id: "i:workspace-management",
type: "mcp",
},
{
name: "DECO_CHAT_API",
integration_id: "i:user-management",
type: "mcp",
},
];
const unwrapMcpResult = (result, opts) => {
if ("isError" in result && result.isError) {
const message = (Array.isArray(result.content) ? result.content[0]?.text : undefined) ??
JSON.stringify(result);
throw new Error(opts?.errorMessage?.(message) ?? message);
}
return result;
};
const workspaceSlug = (workspace) => {
if (workspace.startsWith("/")) {
// /shared/$slug or /users/$slug
return workspace.slice(1).split("/")[1];
}
return workspace;
};
export const genEnv = async ({ workspace, local, bindings, selfUrl, }) => {
const wrangler = await readWranglerConfig();
const appName = `@${wrangler.scope ?? workspaceSlug(workspace)}/${wrangler.name}`;
const client = await createWorkspaceClient({ workspace, local });
const apiClient = await createWorkspaceClient({ local });
try {
const types = new Map();
types.set("Env", 1); // set the default env type
let tsTypes = "";
const mapBindingTools = {};
const props = await Promise.all([
...bindings,
...DEFAULT_BINDINGS,
...(selfUrl
? [
{
name: "SELF",
type: "mcp",
integration_url: selfUrl,
ignoreCache: true,
},
]
: []),
].map(async (binding) => {
let connection;
let stateKey;
if ("integration_id" in binding) {
const integrationResult = (await client.callTool({
name: "INTEGRATIONS_GET",
arguments: {
id: binding.integration_id,
},
}));
const integration = unwrapMcpResult(integrationResult, {
errorMessage: (error) => `Error getting integration ${binding.integration_id}: ${error}`,
});
connection = integration.structuredContent.connection;
}
else if ("integration_name" in binding ||
binding.type === "contract") {
const [integrationName, type] = "integration_name" in binding
? [binding.integration_name, binding.integration_name]
: [CONTRACTS_BINDING, `${appName}-${MD5(binding.contract)}`];
stateKey = { type, key: binding.name };
const appResult = (await client.callTool({
name: "REGISTRY_GET_APP",
arguments: {
name: integrationName,
},
}));
const app = unwrapMcpResult(appResult, {
errorMessage: (error) => `Error getting app ${integrationName}: ${error}`,
});
connection = app.structuredContent.connection;
}
else if ("integration_url" in binding) {
connection = {
type: "HTTP",
url: binding.integration_url,
};
}
else {
throw new Error(`Unknown binding type: ${binding}`);
}
const tools = (await apiClient.callTool({
name: "INTEGRATIONS_LIST_TOOLS",
arguments: {
connection,
ignoreCache: "ignoreCache" in binding ? binding.ignoreCache : undefined,
},
}));
if (!Array.isArray(tools.structuredContent?.tools)) {
console.warn(`⚠️ No tools found for integration ${binding.name}. Skipping...`);
return null;
}
if ("integration_name" in binding || binding.type === "contract") {
mapBindingTools[binding.name] = tools.structuredContent.tools.map((t) => t.name);
}
const compiledTools = await Promise.all(tools.structuredContent.tools.map(async (t) => {
const jsName = generateName(t.name, new Set());
const inputName = `${jsName}Input`;
const outputName = `${jsName}Output`;
const customName = (schema) => {
let typeName = schema.title ?? schema.type;
if (Array.isArray(typeName)) {
typeName = typeName.join(",");
}
if (typeof typeName !== "string") {
return undefined;
}
const key = slugify(typeName);
const count = types.get(key) ?? 0;
types.set(key, count + 1);
return count ? `${typeName}_${count}` : typeName;
};
const [inputTs, outputTs] = await Promise.all([
compile({ ...t.inputSchema, title: inputName }, inputName, {
additionalProperties: false,
customName,
format: false,
}),
t.outputSchema
? await compile({ ...t.outputSchema, title: outputName }, outputName, {
customName,
additionalProperties: false,
format: false,
})
: undefined,
]);
tsTypes += `
${inputTs}
${outputTs ?? ""}
`;
return [
t.name,
inputName,
outputTs ? outputName : undefined,
t.description,
];
}));
return [binding.name, compiledTools, stateKey];
}));
return await format(`
// Generated types - do not edit manually
${tsTypes}
import { z } from "zod";
export type Mcp<T extends Record<string, (input: any) => Promise<any>>> = {
[K in keyof T]: ((input: Parameters<T[K]>[0]) => Promise<ReturnType<T[K]>>) & {
asTool: () => Promise<{
inputSchema: z.ZodType<Parameters<T[K]>[0]>
outputSchema?: z.ZodType<ReturnType<T[K]>>
description: string
id: string
execute: ({ context }: { context: Parameters<T[K]>[0] }) => Promise<ReturnType<T[K]>>
}>
}
}
export const StateSchema = z.object({
${props
.filter((p) => p !== null && p[2] !== undefined)
.map((prop) => {
const [_, __, stateKey] = prop;
return `${stateKey.key}: z.object({
value: z.string(),
__type: z.literal("${stateKey.type}").default("${stateKey.type}"),
})`;
})
.join(",\n")}
})
export interface Env {
DECO_CHAT_WORKSPACE: string;
DECO_CHAT_API_JWT_PUBLIC_KEY: string;
${props
.filter((p) => p !== null)
.map(([propName, tools]) => {
return `${propName}: Mcp<{
${tools
.map(([toolName, inputName, outputName, description]) => {
const docComment = description
? `/**\n${formatDescription(description)}\n */`
: "";
return `${docComment}
${toValidProperty(toolName)}: (input: ${inputName}) => Promise<${outputName ?? "any"}>;
`;
})
.join("")}
}>;`;
})
.join("")}
}
export const Scopes = {
${Object.entries(mapBindingTools)
.map(([bindingName, tools]) => `${toValidProperty(bindingName)}: {
${tools
.map((toolName) => `${toValidProperty(toolName)}: "${scopeParser.fromBindingToolToScope({ bindingName, toolName })}"`)
.join(",\n")}
}`)
.join(",\n")}
}
`);
}
finally {
// Clean up the client connections
await client.close();
await apiClient.close();
}
};
//# sourceMappingURL=gen.js.map