carapace-plugin-sdk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jeff Steinbok
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,161 @@
1
+ # 🦞🐚 carapace-plugin-sdk
2
+
3
+ SDK for building [OpenClaw](https://github.com/JeffSteinbok/openclaw) plugins.
4
+
5
+ Define your tools and config. The SDK generates a fully typed OpenClaw plugin, a standalone CLI, and a plugin manifest — automatically.
6
+
7
+ ## Install
8
+
9
+ > Pre-release — not yet on npm. Install directly from GitHub:
10
+
11
+ ```json
12
+ "dependencies": {
13
+ "carapace-plugin-sdk": "github:JeffSteinbok/carapace-plugin-sdk#main"
14
+ }
15
+ ```
16
+
17
+ The SDK has a `prepare` script, so `npm install` builds it automatically.
18
+
19
+ ## Quick start
20
+
21
+ > **New plugin?** Use [carapace-plugin-template](https://github.com/JeffSteinbok/carapace-plugin-template) — it scaffolds the full project structure, CI, and tests in one click.
22
+
23
+ Here's what you write in `src/plugin.ts`:
24
+
25
+ ```ts
26
+ import { definePlugin } from "carapace-plugin-sdk";
27
+ import { Type } from "@sinclair/typebox";
28
+
29
+ // The export must be named `createEntry` — the SDK's build tools look for it by name.
30
+ export const createEntry = definePlugin({
31
+ id: "my-plugin",
32
+ name: "My Plugin",
33
+ description: "Does something useful.",
34
+
35
+ configSchema: Type.Object({
36
+ apiKey: Type.Optional(Type.String({ description: "API key for the service." })),
37
+ }),
38
+
39
+ tools: (tool) => [
40
+ tool({
41
+ name: "do_thing",
42
+ description: "Does the thing.",
43
+ parameters: Type.Object({
44
+ input: Type.String({ description: "Input value." }),
45
+ }),
46
+ execute: async ({ input }, config) => {
47
+ // input: string ✓ config.apiKey: string | undefined ✓
48
+ return { result: input, usingKey: !!config.apiKey };
49
+ },
50
+ }),
51
+ ],
52
+ });
53
+ ```
54
+
55
+ Run `npm run build` and you get:
56
+
57
+ | Generated file | What it is |
58
+ |----------------|-----------|
59
+ | `dist/adapter.js` | OpenClaw plugin adapter |
60
+ | `dist/bin/my-plugin.js` | Standalone CLI — each tool is a subcommand |
61
+ | `openclaw.plugin.json` | Plugin manifest read by OpenClaw at install time |
62
+
63
+ Nothing else to write. No registration boilerplate, no result wrapping, no manifest to maintain.
64
+
65
+ ## What the SDK handles for you
66
+
67
+ | You write | SDK handles |
68
+ |-----------|-------------|
69
+ | `execute()` returning a plain object | Wrapping in the OpenClaw result format |
70
+ | `configSchema` TypeBox schema | JSON Schema for the manifest + OpenClaw settings UI |
71
+ | Tool names | `contracts.tools` list in the manifest |
72
+ | `src/plugin.ts` | `dist/adapter.js`, `dist/bin/*.js`, `openclaw.plugin.json` |
73
+
74
+ ## Build setup
75
+
76
+ Add to `package.json`:
77
+
78
+ ```json
79
+ {
80
+ "bin": { "my-plugin": "./dist/bin/my-plugin.js" },
81
+ "scripts": {
82
+ "build": "tsup && carapace-generate-cli --entry ./dist/plugin.js --out ./dist/bin"
83
+ }
84
+ }
85
+ ```
86
+
87
+ The SDK ships shared configs so your project files stay minimal:
88
+
89
+ **`tsconfig.json`** — one line:
90
+ ```json
91
+ { "extends": "carapace-plugin-sdk/tsconfig.base.json" }
92
+ ```
93
+
94
+ **`tsup.config.ts`** — three lines:
95
+ ```ts
96
+ import { defineConfig } from "tsup";
97
+ import { definePluginConfig } from "carapace-plugin-sdk/tsup";
98
+
99
+ export default defineConfig(definePluginConfig());
100
+ ```
101
+
102
+ **`vitest.config.ts`** — not needed. Vitest finds `tests/**/*.test.ts` without configuration.
103
+
104
+ ## CLI — for free
105
+
106
+ Every plugin is automatically a standalone CLI. After `npm run build`:
107
+
108
+ ```bash
109
+ my-plugin --help
110
+ my-plugin do-thing "hello"
111
+ my-plugin do-thing "hello" --json
112
+ MY_PLUGIN_API_KEY=sk-... my-plugin do-thing "hello"
113
+ ```
114
+
115
+ Config fields map to environment variables:
116
+ `<PLUGIN_ID_SCREAMING_SNAKE>_<FIELD_SCREAMING_SNAKE>`
117
+
118
+ ## Reusable CI/CD workflows
119
+
120
+ Call the shared GitHub Actions workflows from your plugin repo — no workflow logic to copy:
121
+
122
+ ```yaml
123
+ # .github/workflows/ci.yml
124
+ jobs:
125
+ ci:
126
+ uses: JeffSteinbok/carapace-plugin-sdk/.github/workflows/plugin-ci.yml@main
127
+ ```
128
+
129
+ ```yaml
130
+ # .github/workflows/release.yml
131
+ on:
132
+ workflow_dispatch:
133
+ inputs:
134
+ version-bump:
135
+ type: choice
136
+ options: [patch, minor, major]
137
+ prerelease:
138
+ type: choice
139
+ options: ['', alpha, beta, rc]
140
+ jobs:
141
+ release:
142
+ uses: JeffSteinbok/carapace-plugin-sdk/.github/workflows/plugin-release.yml@main
143
+ with:
144
+ version-bump: ${{ inputs.version-bump }}
145
+ prerelease: ${{ inputs.prerelease }}
146
+ secrets:
147
+ npm-token: ${{ secrets.NPM_TOKEN }}
148
+ ```
149
+
150
+ ## Examples
151
+
152
+ - [carapace-plugin-template](https://github.com/JeffSteinbok/carapace-plugin-template) — starter template with CI, tests, and build pre-configured
153
+ - [carapace-stock-quotes](https://github.com/JeffSteinbok/carapace-stock-quotes) — real plugin with multiple data sources (Yahoo Finance + Finnhub)
154
+
155
+ ## Internals
156
+
157
+ See [ARCHITECTURE.md](./ARCHITECTURE.md) for how the SDK works under the hood — the type machinery behind `definePlugin`, how `carapace-generate-cli` generates artifacts, the CLI runtime, and the adapter pattern.
158
+
159
+ ## License
160
+
161
+ MIT
package/dist/cli.d.ts ADDED
@@ -0,0 +1,63 @@
1
+ import { PluginEntry } from './index.js';
2
+ import '@sinclair/typebox';
3
+
4
+ /**
5
+ * carapace-plugin-sdk/cli — CLI runtime.
6
+ *
7
+ * Turns any OpenClaw plugin into a standalone command-line tool — for free.
8
+ * You never import this directly. It is called by the generated
9
+ * `dist/bin/<plugin-id>.js` that `carapace-generate-cli` emits at build time.
10
+ *
11
+ * How it works end-to-end:
12
+ * 1. Build time: `carapace-generate-cli` imports your `createEntry()`,
13
+ * reads the plugin metadata, and writes `dist/bin/<plugin>.js`.
14
+ * 2. Run time: the generated file calls `run(createEntry(), options)`.
15
+ * 3. `run()` calls `register()` with a mock API to collect tool definitions,
16
+ * maps environment variables → pluginConfig, parses argv, finds the right
17
+ * tool, builds its params, and calls execute().
18
+ *
19
+ * Config via environment variables:
20
+ * Each field in configSchema.properties maps to an env var:
21
+ * <PLUGIN_ID_SCREAMING_SNAKE>_<FIELD_SCREAMING_SNAKE>
22
+ * Example: plugin `stock-quotes`, field `finnhubApiKey`
23
+ * → env var `STOCK_QUOTES_FINNHUB_API_KEY`
24
+ *
25
+ * @module carapace-plugin-sdk/cli
26
+ */
27
+
28
+ /**
29
+ * Options for the CLI runner.
30
+ * These are set by the generated bin file — plugin authors never pass them manually.
31
+ */
32
+ interface RunOptions {
33
+ /**
34
+ * Prefix used when mapping environment variables to plugin config fields.
35
+ * Generated from the plugin id: "stock-quotes" → "STOCK_QUOTES".
36
+ * Override with `--name` when generating the CLI to use a custom prefix.
37
+ */
38
+ envPrefix?: string;
39
+ /**
40
+ * The binary name shown in --help usage lines and error messages.
41
+ * Defaults to the plugin's `id`.
42
+ */
43
+ binName?: string;
44
+ }
45
+ /**
46
+ * Entry point for the generated CLI binary.
47
+ *
48
+ * Call this from the auto-generated `dist/bin/<plugin-id>.js`:
49
+ *
50
+ * ```js
51
+ * import { run } from "carapace-plugin-sdk/cli";
52
+ * import { createEntry } from "../index.js";
53
+ * run(createEntry(), { binName: "my-plugin", envPrefix: "MY_PLUGIN" });
54
+ * ```
55
+ *
56
+ * You never write that file manually — `carapace-generate-cli` emits it.
57
+ *
58
+ * @param entry - The object returned by your plugin's `createEntry()`.
59
+ * @param options - Binary name and env prefix (set by the generated file).
60
+ */
61
+ declare function run(entry: PluginEntry, options?: RunOptions): Promise<void>;
62
+
63
+ export { type RunOptions, run };
package/dist/cli.js ADDED
@@ -0,0 +1,194 @@
1
+ // src/cli.ts
2
+ function asConfigSchema(schema) {
3
+ if (schema && typeof schema === "object" && "properties" in schema) {
4
+ return schema;
5
+ }
6
+ return null;
7
+ }
8
+ function envVarName(prefix, field) {
9
+ const snake = field.replace(/([A-Z])/g, "_$1").toUpperCase();
10
+ return `${prefix}_${snake}`;
11
+ }
12
+ function buildConfigFromEnv(prefix, schema) {
13
+ const config = {};
14
+ const cs = asConfigSchema(schema);
15
+ if (!cs?.properties) return config;
16
+ for (const [field, def] of Object.entries(cs.properties)) {
17
+ const envName = envVarName(prefix, field);
18
+ const val = process.env[envName]?.trim();
19
+ if (val) {
20
+ config[field] = def.type === "number" ? Number(val) : val;
21
+ }
22
+ }
23
+ return config;
24
+ }
25
+ function parseArgs(argv) {
26
+ const flags = {};
27
+ const positional = [];
28
+ let command = null;
29
+ for (let i = 0; i < argv.length; i++) {
30
+ const arg = argv[i];
31
+ if (arg.startsWith("--")) {
32
+ const eqIdx = arg.indexOf("=");
33
+ if (eqIdx !== -1) {
34
+ flags[arg.slice(2, eqIdx)] = arg.slice(eqIdx + 1);
35
+ } else {
36
+ const next = argv[i + 1];
37
+ if (next && !next.startsWith("--")) {
38
+ flags[arg.slice(2)] = next;
39
+ i++;
40
+ } else {
41
+ flags[arg.slice(2)] = true;
42
+ }
43
+ }
44
+ } else if (!command) {
45
+ command = arg;
46
+ } else {
47
+ positional.push(arg);
48
+ }
49
+ }
50
+ return { command, positional, flags };
51
+ }
52
+ function printHelp(entry, tools, binName, envPrefix) {
53
+ console.log(`${binName} \u2014 ${entry.description ?? entry.name}
54
+ `);
55
+ console.log("Usage:");
56
+ console.log(` ${binName} <command> [args...] [--json]
57
+ `);
58
+ console.log("Commands:");
59
+ for (const tool of tools) {
60
+ const params = tool.parameters?.properties ? Object.keys(tool.parameters.properties).map((p) => {
61
+ const schema = tool.parameters.properties[p];
62
+ return schema.type === "array" ? `<${p}...>` : `<${p}>`;
63
+ }).join(" ") : "";
64
+ const cmdName = tool.name.replace(/_/g, "-");
65
+ console.log(` ${cmdName.padEnd(20)} ${params.padEnd(20)} ${tool.description ?? ""}`);
66
+ }
67
+ console.log("\nOptions:");
68
+ console.log(" --json Output raw JSON instead of pretty-printed text");
69
+ console.log(" --help, -h Show this help");
70
+ const cs = asConfigSchema(entry.configSchema);
71
+ if (envPrefix && cs?.properties) {
72
+ console.log("\nEnvironment:");
73
+ for (const [field, def] of Object.entries(cs.properties)) {
74
+ const envName = envVarName(envPrefix, field);
75
+ console.log(` ${envName.padEnd(30)} ${def.description ?? ""}`);
76
+ }
77
+ }
78
+ }
79
+ function formatPretty(result) {
80
+ if (result == null) return "";
81
+ const content = result?.content;
82
+ if (Array.isArray(content) && content[0]?.text) {
83
+ try {
84
+ const parsed = JSON.parse(content[0].text);
85
+ return formatPrettyValue(parsed);
86
+ } catch {
87
+ return content[0].text;
88
+ }
89
+ }
90
+ return formatPrettyValue(result);
91
+ }
92
+ function formatPrettyValue(val) {
93
+ if (typeof val === "string") return val;
94
+ if (typeof val !== "object" || val === null) return String(val);
95
+ if ("error" in val) return `Error: ${val.error}`;
96
+ if (Array.isArray(val)) return val.map((item) => formatPrettyValue(item)).join("\n");
97
+ return JSON.stringify(val, null, 2);
98
+ }
99
+ function matchTool(tools, command) {
100
+ const exact = tools.find((t) => t.name === command);
101
+ if (exact) return exact;
102
+ const normalized = command.replace(/-/g, "_");
103
+ const norm = tools.find((t) => t.name === normalized);
104
+ if (norm) return norm;
105
+ const suffix = tools.find((t) => t.name.endsWith(`_${normalized}`));
106
+ if (suffix) return suffix;
107
+ if (tools.length === 1) return tools[0];
108
+ return void 0;
109
+ }
110
+ function buildParams(tool, positional, flags) {
111
+ const params = {};
112
+ const schema = tool.parameters?.properties;
113
+ if (!schema) return params;
114
+ const paramNames = Object.keys(schema);
115
+ let posIdx = 0;
116
+ for (const name of paramNames) {
117
+ const def = schema[name];
118
+ const flagName = name.replace(/_/g, "-");
119
+ if (flagName in flags) {
120
+ params[name] = flags[flagName];
121
+ continue;
122
+ }
123
+ if (name in flags) {
124
+ params[name] = flags[name];
125
+ continue;
126
+ }
127
+ if (def.type === "array") {
128
+ params[name] = positional.slice(posIdx);
129
+ posIdx = positional.length;
130
+ continue;
131
+ }
132
+ if (posIdx < positional.length) {
133
+ params[name] = positional[posIdx++];
134
+ }
135
+ }
136
+ return params;
137
+ }
138
+ async function run(entry, options = {}) {
139
+ const binName = options.binName ?? entry.id;
140
+ const envPrefix = options.envPrefix ?? entry.id.replace(/-/g, "_").toUpperCase();
141
+ const tools = [];
142
+ const config = buildConfigFromEnv(envPrefix, entry.configSchema);
143
+ entry.register({
144
+ registerTool: (tool2) => tools.push(tool2),
145
+ pluginConfig: config
146
+ });
147
+ const argv = process.argv.slice(2);
148
+ const { command, positional, flags } = parseArgs(argv);
149
+ if (flags.help || flags.h || !command && positional.length === 0) {
150
+ printHelp(entry, tools, binName, envPrefix);
151
+ process.exit(0);
152
+ }
153
+ const json = !!flags.json;
154
+ delete flags.json;
155
+ delete flags.help;
156
+ delete flags.h;
157
+ const tool = matchTool(tools, command ?? "");
158
+ if (!tool) {
159
+ console.error(`Unknown command: ${command}`);
160
+ console.error(`Run \`${binName} --help\` for available commands.`);
161
+ process.exit(1);
162
+ }
163
+ let effectivePositional = positional;
164
+ if (tools.length === 1 && command && !tools.find((t) => t.name === command.replace(/-/g, "_"))) {
165
+ effectivePositional = [command, ...positional];
166
+ }
167
+ const params = buildParams(tool, effectivePositional, flags);
168
+ try {
169
+ const result = await tool.execute("cli", params);
170
+ if (json) {
171
+ const content = result?.content;
172
+ if (Array.isArray(content) && content[0]?.text) {
173
+ try {
174
+ console.log(JSON.stringify(JSON.parse(content[0].text), null, 2));
175
+ } catch {
176
+ console.log(content[0].text);
177
+ }
178
+ } else {
179
+ console.log(JSON.stringify(result, null, 2));
180
+ }
181
+ } else {
182
+ const output = formatPretty(result);
183
+ if (output) console.log(output);
184
+ }
185
+ } catch (e) {
186
+ const msg = e instanceof Error ? e.message : String(e);
187
+ console.error(`Error: ${msg}`);
188
+ process.exit(1);
189
+ }
190
+ }
191
+ export {
192
+ run
193
+ };
194
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli.ts"],"sourcesContent":["/**\n * carapace-plugin-sdk/cli — CLI runtime.\n *\n * Turns any OpenClaw plugin into a standalone command-line tool — for free.\n * You never import this directly. It is called by the generated\n * `dist/bin/<plugin-id>.js` that `carapace-generate-cli` emits at build time.\n *\n * How it works end-to-end:\n * 1. Build time: `carapace-generate-cli` imports your `createEntry()`,\n * reads the plugin metadata, and writes `dist/bin/<plugin>.js`.\n * 2. Run time: the generated file calls `run(createEntry(), options)`.\n * 3. `run()` calls `register()` with a mock API to collect tool definitions,\n * maps environment variables → pluginConfig, parses argv, finds the right\n * tool, builds its params, and calls execute().\n *\n * Config via environment variables:\n * Each field in configSchema.properties maps to an env var:\n * <PLUGIN_ID_SCREAMING_SNAKE>_<FIELD_SCREAMING_SNAKE>\n * Example: plugin `stock-quotes`, field `finnhubApiKey`\n * → env var `STOCK_QUOTES_FINNHUB_API_KEY`\n *\n * @module carapace-plugin-sdk/cli\n */\n\nimport type { PluginEntry } from \"./index.js\";\n\n// ---------------------------------------------------------------------------\n// Internal types\n//\n// These mirror the public PluginTool/PluginToolSchema types but are kept\n// private here so the cli module doesn't re-export them. The runtime only\n// needs to read these shapes; it never constructs them.\n// ---------------------------------------------------------------------------\n\n/** A single tool parameter schema, as returned by TypeBox or hand-written JSON Schema. */\ninterface ToolParam {\n type: string;\n description?: string;\n items?: { type: string };\n minItems?: number;\n enum?: string[];\n}\n\n/** The full parameter schema for a tool (always an object at the top level). */\ninterface ToolSchema {\n type: \"object\";\n properties: Record<string, ToolParam>;\n required?: string[];\n}\n\n/** Shape of configSchema.properties entries — used when reading env vars and printing help. */\ninterface ConfigField {\n type: string;\n description?: string;\n}\n\n/** The configSchema shape the CLI reads from PluginEntry. */\ninterface ConfigSchema {\n properties?: Record<string, ConfigField>;\n}\n\n/** Cast an unknown configSchema to the readable shape. Returns null if not usable. */\nfunction asConfigSchema(schema: unknown): ConfigSchema | null {\n if (schema && typeof schema === \"object\" && \"properties\" in schema) {\n return schema as ConfigSchema;\n }\n return null;\n}\n\n/** A registered tool as captured from register(). */\ninterface Tool {\n name: string;\n label?: string;\n description?: string;\n parameters?: ToolSchema;\n execute(toolCallId: string, params: Record<string, unknown>): Promise<unknown>;\n}\n\n/**\n * Options for the CLI runner.\n * These are set by the generated bin file — plugin authors never pass them manually.\n */\nexport interface RunOptions {\n /**\n * Prefix used when mapping environment variables to plugin config fields.\n * Generated from the plugin id: \"stock-quotes\" → \"STOCK_QUOTES\".\n * Override with `--name` when generating the CLI to use a custom prefix.\n */\n envPrefix?: string;\n /**\n * The binary name shown in --help usage lines and error messages.\n * Defaults to the plugin's `id`.\n */\n binName?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Env → config mapping\n//\n// When running as a CLI (outside OpenClaw), config comes from environment\n// variables instead of the host's config store. This section converts them.\n// ---------------------------------------------------------------------------\n\n/**\n * Build the environment variable name for a given plugin prefix + config field.\n *\n * Converts camelCase field names to SCREAMING_SNAKE_CASE and prepends the prefix.\n *\n * @example\n * envVarName(\"STOCK_QUOTES\", \"finnhubApiKey\") // → \"STOCK_QUOTES_FINNHUB_API_KEY\"\n * envVarName(\"MY_PLUGIN\", \"defaultName\") // → \"MY_PLUGIN_DEFAULT_NAME\"\n */\nfunction envVarName(prefix: string, field: string): string {\n const snake = field.replace(/([A-Z])/g, \"_$1\").toUpperCase();\n return `${prefix}_${snake}`;\n}\n\n/**\n * Build a pluginConfig object from environment variables.\n *\n * Reads each field declared in `configSchema.properties` from a corresponding\n * env var. Missing env vars are silently skipped (the plugin's `buildConfig()`\n * is responsible for applying defaults).\n *\n * @param prefix - The SCREAMING_SNAKE_CASE plugin prefix (e.g. \"STOCK_QUOTES\").\n * @param schema - The plugin's configSchema, used to know which fields to look for.\n */\nfunction buildConfigFromEnv(\n prefix: string,\n schema?: unknown,\n): Record<string, unknown> {\n const config: Record<string, unknown> = {};\n const cs = asConfigSchema(schema);\n if (!cs?.properties) return config;\n\n for (const [field, def] of Object.entries(cs.properties)) {\n const envName = envVarName(prefix, field);\n const val = process.env[envName]?.trim();\n if (val) {\n // Coerce numeric fields so the plugin receives the right type.\n config[field] = def.type === \"number\" ? Number(val) : val;\n }\n }\n return config;\n}\n\n// ---------------------------------------------------------------------------\n// Arg parsing\n// ---------------------------------------------------------------------------\n\n/** Output of parseArgs(). */\ninterface ParsedArgs {\n /** The first non-flag argument (the subcommand / tool name). */\n command: string | null;\n /** Remaining non-flag arguments after the command. */\n positional: string[];\n /** --flag and --flag=value pairs. Boolean flags are stored as `true`. */\n flags: Record<string, string | boolean>;\n}\n\n/**\n * Minimal argv parser — no external dependencies.\n *\n * Rules:\n * - `--flag value` → flags[\"flag\"] = \"value\"\n * - `--flag=value` → flags[\"flag\"] = \"value\"\n * - `--flag` → flags[\"flag\"] = true (when next arg is another flag or absent)\n * - First non-flag → command\n * - Subsequent non-flags → positional[]\n *\n * @param argv - Typically `process.argv.slice(2)`.\n */\nfunction parseArgs(argv: string[]): ParsedArgs {\n const flags: Record<string, string | boolean> = {};\n const positional: string[] = [];\n let command: string | null = null;\n\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i];\n if (arg.startsWith(\"--\")) {\n const eqIdx = arg.indexOf(\"=\");\n if (eqIdx !== -1) {\n // --flag=value form\n flags[arg.slice(2, eqIdx)] = arg.slice(eqIdx + 1);\n } else {\n const next = argv[i + 1];\n if (next && !next.startsWith(\"--\")) {\n // --flag value form\n flags[arg.slice(2)] = next;\n i++; // consume the value token\n } else {\n // Boolean flag: --help, --json, etc.\n flags[arg.slice(2)] = true;\n }\n }\n } else if (!command) {\n command = arg;\n } else {\n positional.push(arg);\n }\n }\n\n return { command, positional, flags };\n}\n\n// ---------------------------------------------------------------------------\n// Help generation\n// ---------------------------------------------------------------------------\n\n/**\n * Print the --help screen to stdout.\n *\n * Shows:\n * - Plugin name and description\n * - Each registered tool as a subcommand with its parameters\n * - Global options (--json, --help)\n * - Environment variables (if the plugin has configSchema fields)\n */\nfunction printHelp(entry: PluginEntry, tools: Tool[], binName: string, envPrefix?: string) {\n console.log(`${binName} — ${entry.description ?? entry.name}\\n`);\n console.log(\"Usage:\");\n console.log(` ${binName} <command> [args...] [--json]\\n`);\n console.log(\"Commands:\");\n\n for (const tool of tools) {\n // Build a short param signature like \"<city> <units>\" or \"<symbols...>\"\n const params = tool.parameters?.properties\n ? Object.keys(tool.parameters.properties)\n .map((p) => {\n const schema = tool.parameters!.properties[p];\n // Array params consume all remaining positional args → show with \"...\"\n return schema.type === \"array\" ? `<${p}...>` : `<${p}>`;\n })\n .join(\" \")\n : \"\";\n // Normalise underscores to hyphens for display (tool names are stored with underscores)\n const cmdName = tool.name.replace(/_/g, \"-\");\n console.log(` ${cmdName.padEnd(20)} ${params.padEnd(20)} ${tool.description ?? \"\"}`);\n }\n\n console.log(\"\\nOptions:\");\n console.log(\" --json Output raw JSON instead of pretty-printed text\");\n console.log(\" --help, -h Show this help\");\n\n // Show the env var section only if the plugin actually has config fields.\n const cs = asConfigSchema(entry.configSchema);\n if (envPrefix && cs?.properties) {\n console.log(\"\\nEnvironment:\");\n for (const [field, def] of Object.entries(cs.properties)) {\n const envName = envVarName(envPrefix, field);\n console.log(` ${envName.padEnd(30)} ${def.description ?? \"\"}`);\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Output formatting\n// ---------------------------------------------------------------------------\n\n/**\n * Unwrap a formatResult() envelope and pretty-print its contents.\n *\n * OpenClaw tool results are wrapped as `{ content: [{ type: \"text\", text: \"...\" }] }`.\n * This function peels that off and delegates to formatPrettyValue() for human output.\n * When `--json` is used, the caller handles output directly (no pretty printing).\n */\nfunction formatPretty(result: unknown): string {\n if (result == null) return \"\";\n\n // Unwrap the standard formatResult() envelope.\n const content = (result as { content?: { text?: string }[] })?.content;\n if (Array.isArray(content) && content[0]?.text) {\n try {\n // The text is usually JSON — parse it for pretty formatting.\n const parsed = JSON.parse(content[0].text);\n return formatPrettyValue(parsed);\n } catch {\n // Not valid JSON — print the raw text.\n return content[0].text;\n }\n }\n\n // Result wasn't wrapped with formatResult() — format it directly.\n return formatPrettyValue(result);\n}\n\n/**\n * Recursively pretty-print a value.\n *\n * Strings are returned as-is. Arrays are printed one item per line.\n * Objects with an `error` key are shown as \"Error: <message>\".\n * Everything else falls back to JSON.stringify with indentation.\n */\nfunction formatPrettyValue(val: unknown): string {\n if (typeof val === \"string\") return val;\n if (typeof val !== \"object\" || val === null) return String(val);\n if (\"error\" in val) return `Error: ${(val as { error: string }).error}`;\n if (Array.isArray(val)) return val.map((item) => formatPrettyValue(item)).join(\"\\n\");\n return JSON.stringify(val, null, 2);\n}\n\n// ---------------------------------------------------------------------------\n// Tool matching\n// ---------------------------------------------------------------------------\n\n/**\n * Find the tool that best matches the given command string.\n *\n * Matching order (first match wins):\n * 1. Exact match on `tool.name`\n * 2. Hyphens normalised to underscores (CLI users prefer hyphens; tool names use underscores)\n * 3. Suffix match — \"quote\" matches \"stock_quote\" (convenience shorthand)\n * 4. If there's only one tool, use it regardless of the command name\n * (lets `my-plugin some-value` work without typing the subcommand)\n */\nfunction matchTool(tools: Tool[], command: string): Tool | undefined {\n const exact = tools.find((t) => t.name === command);\n if (exact) return exact;\n\n const normalized = command.replace(/-/g, \"_\");\n const norm = tools.find((t) => t.name === normalized);\n if (norm) return norm;\n\n const suffix = tools.find((t) => t.name.endsWith(`_${normalized}`));\n if (suffix) return suffix;\n\n // Single-tool convenience: the user typed anything, just use the only tool.\n if (tools.length === 1) return tools[0];\n return undefined;\n}\n\n/**\n * Map positional args and flags onto the tool's declared parameter schema.\n *\n * Parameters are filled in schema order:\n * 1. Flags (`--param-name value` or `--param-name=value`) take priority.\n * 2. Array-typed params consume all remaining positional args.\n * 3. Other params consume positional args left-to-right.\n *\n * Flag names are normalised: `--my-param` matches schema key `myParam` or `my_param`.\n */\nfunction buildParams(tool: Tool, positional: string[], flags: Record<string, string | boolean>): Record<string, unknown> {\n const params: Record<string, unknown> = {};\n const schema = tool.parameters?.properties;\n if (!schema) return params;\n\n const paramNames = Object.keys(schema);\n let posIdx = 0; // index into positional[], advanced as we consume args\n\n for (const name of paramNames) {\n const def = schema[name];\n // Flags can be passed with hyphens even if the schema uses camelCase/underscores.\n const flagName = name.replace(/_/g, \"-\");\n if (flagName in flags) { params[name] = flags[flagName]; continue; }\n if (name in flags) { params[name] = flags[name]; continue; }\n\n if (def.type === \"array\") {\n // Array param: greedily consume all remaining positional args.\n params[name] = positional.slice(posIdx);\n posIdx = positional.length;\n continue;\n }\n\n // Scalar param: consume the next positional arg.\n if (posIdx < positional.length) {\n params[name] = positional[posIdx++];\n }\n }\n\n return params;\n}\n\n// ---------------------------------------------------------------------------\n// Main runner\n// ---------------------------------------------------------------------------\n\n/**\n * Entry point for the generated CLI binary.\n *\n * Call this from the auto-generated `dist/bin/<plugin-id>.js`:\n *\n * ```js\n * import { run } from \"carapace-plugin-sdk/cli\";\n * import { createEntry } from \"../index.js\";\n * run(createEntry(), { binName: \"my-plugin\", envPrefix: \"MY_PLUGIN\" });\n * ```\n *\n * You never write that file manually — `carapace-generate-cli` emits it.\n *\n * @param entry - The object returned by your plugin's `createEntry()`.\n * @param options - Binary name and env prefix (set by the generated file).\n */\nexport async function run(entry: PluginEntry, options: RunOptions = {}): Promise<void> {\n const binName = options.binName ?? entry.id;\n const envPrefix = options.envPrefix ?? entry.id.replace(/-/g, \"_\").toUpperCase();\n\n // Call register() with a lightweight mock API to collect tool definitions.\n // This does NOT invoke any tool logic — it just gathers metadata.\n const tools: Tool[] = [];\n const config = buildConfigFromEnv(envPrefix, entry.configSchema);\n entry.register({\n registerTool: (tool: unknown) => tools.push(tool as Tool),\n pluginConfig: config,\n });\n\n const argv = process.argv.slice(2);\n const { command, positional, flags } = parseArgs(argv);\n\n // Show help when explicitly requested or when invoked with no arguments.\n if (flags.help || flags.h || (!command && positional.length === 0)) {\n printHelp(entry, tools, binName, envPrefix);\n process.exit(0);\n }\n\n const json = !!flags.json;\n // Remove meta-flags so they don't leak into buildParams().\n delete flags.json;\n delete flags.help;\n delete flags.h;\n\n // Find the matching tool. Error out clearly if not found.\n const tool = matchTool(tools, command ?? \"\");\n if (!tool) {\n console.error(`Unknown command: ${command}`);\n console.error(`Run \\`${binName} --help\\` for available commands.`);\n process.exit(1);\n }\n\n // Single-tool convenience: if the user typed `my-plugin some-value` and the\n // plugin only has one tool, treat \"some-value\" as the first positional arg\n // rather than the (wrong) command name.\n let effectivePositional = positional;\n if (tools.length === 1 && command && !tools.find((t) => t.name === command.replace(/-/g, \"_\"))) {\n effectivePositional = [command, ...positional];\n }\n\n const params = buildParams(tool, effectivePositional, flags);\n\n try {\n const result = await tool.execute(\"cli\", params);\n\n if (json) {\n // Raw JSON output — unwrap the formatResult envelope if present.\n const content = (result as { content?: { text?: string }[] })?.content;\n if (Array.isArray(content) && content[0]?.text) {\n try {\n // Re-parse and re-stringify to get consistent indentation.\n console.log(JSON.stringify(JSON.parse(content[0].text), null, 2));\n } catch {\n console.log(content[0].text);\n }\n } else {\n console.log(JSON.stringify(result, null, 2));\n }\n } else {\n const output = formatPretty(result);\n if (output) console.log(output);\n }\n } catch (e: unknown) {\n const msg = e instanceof Error ? e.message : String(e);\n console.error(`Error: ${msg}`);\n process.exit(1);\n }\n}\n"],"mappings":"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","names":["tool"]}
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/generate-cli.ts
4
+ import { writeFileSync, mkdirSync, readFileSync } from "fs";
5
+ import { resolve, relative, dirname, sep } from "path";
6
+ import { pathToFileURL } from "url";
7
+ function parseGeneratorArgs() {
8
+ const args = process.argv.slice(2);
9
+ let entry = "";
10
+ let outDir = "";
11
+ let name;
12
+ for (let i = 0; i < args.length; i++) {
13
+ if (args[i] === "--entry" && args[i + 1]) entry = args[++i];
14
+ else if (args[i] === "--out" && args[i + 1]) outDir = args[++i];
15
+ else if (args[i] === "--name" && args[i + 1]) name = args[++i];
16
+ }
17
+ if (!entry || !outDir) {
18
+ console.error("Usage: carapace-generate-cli --entry <path-to-dist/index.js> --out <output-dir> [--name <bin-name>]");
19
+ process.exit(1);
20
+ }
21
+ return { entry: resolve(entry), outDir: resolve(outDir), name };
22
+ }
23
+ function toPosixRelative(from, to) {
24
+ let rel = relative(from, to);
25
+ if (sep === "\\") rel = rel.replaceAll("\\", "/");
26
+ if (!rel.startsWith(".")) rel = `./${rel}`;
27
+ return rel;
28
+ }
29
+ function generateCli(entry, outDir, binName, envPrefix) {
30
+ const relativeEntry = toPosixRelative(outDir, entry);
31
+ const content = `#!/usr/bin/env node
32
+ // Auto-generated by carapace-plugin-sdk \u2014 do not edit
33
+ import { run } from "carapace-plugin-sdk/cli";
34
+ import { createEntry } from "${relativeEntry}";
35
+
36
+ run(createEntry(), { binName: "${binName}", envPrefix: "${envPrefix}" });
37
+ `;
38
+ mkdirSync(outDir, { recursive: true });
39
+ const outPath = resolve(outDir, `${binName}.js`);
40
+ writeFileSync(outPath, content, { mode: 493 });
41
+ console.log(`Generated CLI: ${outPath}`);
42
+ }
43
+ function generateAdapter(entry) {
44
+ const adapterDir = dirname(entry);
45
+ const relativeEntry = toPosixRelative(adapterDir, entry);
46
+ const content = `// Auto-generated by carapace-plugin-sdk \u2014 do not edit
47
+ import { createAdapter } from "carapace-plugin-sdk";
48
+ import { createEntry } from "${relativeEntry}";
49
+
50
+ export default createAdapter(createEntry(), import.meta.url);
51
+ `;
52
+ const outPath = resolve(adapterDir, "adapter.js");
53
+ writeFileSync(outPath, content);
54
+ console.log(`Generated adapter: ${outPath}`);
55
+ }
56
+ function generateManifest(pluginEntry) {
57
+ let version = "0.0.0";
58
+ try {
59
+ const pkg = JSON.parse(readFileSync(resolve(process.cwd(), "package.json"), "utf8"));
60
+ version = pkg.version ?? version;
61
+ } catch {
62
+ console.warn("Warning: could not read package.json for version \u2014 defaulting to 0.0.0");
63
+ }
64
+ const manifest = {
65
+ id: pluginEntry.id,
66
+ name: pluginEntry.name,
67
+ description: pluginEntry.description ?? "",
68
+ version,
69
+ // The adapter is always at this path — generated by generateAdapter() above.
70
+ entry: "./dist/adapter.js"
71
+ };
72
+ if (pluginEntry.configSchema) {
73
+ manifest.configSchema = pluginEntry.configSchema;
74
+ }
75
+ if (pluginEntry.contracts) {
76
+ manifest.contracts = pluginEntry.contracts;
77
+ }
78
+ manifest.activation = pluginEntry.activation ?? { onStartup: true };
79
+ manifest.openclaw = {};
80
+ const outPath = resolve(process.cwd(), "openclaw.plugin.json");
81
+ writeFileSync(outPath, JSON.stringify(manifest, null, 2) + "\n");
82
+ console.log(`Generated manifest: ${outPath}`);
83
+ }
84
+ async function main() {
85
+ const { entry, outDir, name } = parseGeneratorArgs();
86
+ const entryUrl = pathToFileURL(entry).href;
87
+ const mod = await import(entryUrl);
88
+ const createEntry = mod.createEntry ?? mod.default?.createEntry;
89
+ if (typeof createEntry !== "function") {
90
+ console.error(`Error: ${entry} does not export createEntry()`);
91
+ process.exit(1);
92
+ }
93
+ const pluginEntry = createEntry();
94
+ const binName = name ?? pluginEntry.id;
95
+ const envPrefix = binName.replace(/-/g, "_").toUpperCase();
96
+ generateCli(entry, outDir, binName, envPrefix);
97
+ generateAdapter(entry);
98
+ generateManifest(pluginEntry);
99
+ }
100
+ main().catch((e) => {
101
+ console.error(e);
102
+ process.exit(1);
103
+ });
104
+ //# sourceMappingURL=generate-cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/generate-cli.ts"],"sourcesContent":["#!/usr/bin/env node\r\n/**\r\n * carapace-generate-cli — Build-time generator for OpenClaw plugins.\r\n *\r\n * Reads a plugin's createEntry() metadata and emits:\r\n * - dist/bin/<plugin-id>.js — standalone CLI entry point\r\n * - dist/adapter.js — OpenClaw adapter (always identical, no source file needed)\r\n * - openclaw.plugin.json — plugin manifest (derived from createEntry() + package.json)\r\n *\r\n * Usage:\r\n * carapace-generate-cli --entry ./dist/plugin.js --out ./dist/bin\r\n * carapace-generate-cli --entry ./dist/plugin.js --out ./dist/bin --name my-tool\r\n */\r\n\r\nimport { writeFileSync, mkdirSync, readFileSync } from \"node:fs\";\r\nimport { resolve, relative, dirname, sep } from \"node:path\";\r\nimport { pathToFileURL } from \"node:url\";\r\n\r\n// ---------------------------------------------------------------------------\r\n// Arg parsing\r\n// ---------------------------------------------------------------------------\r\n\r\nfunction parseGeneratorArgs(): { entry: string; outDir: string; name?: string } {\r\n const args = process.argv.slice(2);\r\n let entry = \"\";\r\n let outDir = \"\";\r\n let name: string | undefined;\r\n\r\n for (let i = 0; i < args.length; i++) {\r\n if (args[i] === \"--entry\" && args[i + 1]) entry = args[++i];\r\n else if (args[i] === \"--out\" && args[i + 1]) outDir = args[++i];\r\n else if (args[i] === \"--name\" && args[i + 1]) name = args[++i];\r\n }\r\n\r\n if (!entry || !outDir) {\r\n console.error(\"Usage: carapace-generate-cli --entry <path-to-dist/index.js> --out <output-dir> [--name <bin-name>]\");\r\n process.exit(1);\r\n }\r\n\r\n return { entry: resolve(entry), outDir: resolve(outDir), name };\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// Path helper\r\n// ---------------------------------------------------------------------------\r\n\r\nfunction toPosixRelative(from: string, to: string): string {\r\n let rel = relative(from, to);\r\n if (sep === \"\\\\\") rel = rel.replaceAll(\"\\\\\", \"/\");\r\n if (!rel.startsWith(\".\")) rel = `./${rel}`;\r\n return rel;\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// Generators\r\n// ---------------------------------------------------------------------------\r\n\r\nfunction generateCli(entry: string, outDir: string, binName: string, envPrefix: string): void {\r\n const relativeEntry = toPosixRelative(outDir, entry);\r\n\r\n const content = `#!/usr/bin/env node\r\n// Auto-generated by carapace-plugin-sdk — do not edit\r\nimport { run } from \"carapace-plugin-sdk/cli\";\r\nimport { createEntry } from \"${relativeEntry}\";\r\n\r\nrun(createEntry(), { binName: \"${binName}\", envPrefix: \"${envPrefix}\" });\r\n`;\r\n\r\n mkdirSync(outDir, { recursive: true });\r\n const outPath = resolve(outDir, `${binName}.js`);\r\n writeFileSync(outPath, content, { mode: 0o755 });\r\n console.log(`Generated CLI: ${outPath}`);\r\n}\r\n\r\nfunction generateAdapter(entry: string): void {\r\n const adapterDir = dirname(entry);\r\n const relativeEntry = toPosixRelative(adapterDir, entry);\r\n\r\n const content = `// Auto-generated by carapace-plugin-sdk — do not edit\r\nimport { createAdapter } from \"carapace-plugin-sdk\";\r\nimport { createEntry } from \"${relativeEntry}\";\r\n\r\nexport default createAdapter(createEntry(), import.meta.url);\r\n`;\r\n\r\n const outPath = resolve(adapterDir, \"adapter.js\");\r\n writeFileSync(outPath, content);\r\n console.log(`Generated adapter: ${outPath}`);\r\n}\r\n\r\n/**\r\n * Generate openclaw.plugin.json from createEntry() metadata + package.json.\r\n *\r\n * createEntry() is the single source of truth for:\r\n * id, name, description, configSchema, contracts, activation\r\n *\r\n * version comes from package.json (it's the npm package version).\r\n * entry is always ./dist/adapter.js.\r\n *\r\n * The manifest is written to the current working directory (the plugin root),\r\n * not into dist/, because OpenClaw reads it from the installed package root.\r\n */\r\nfunction generateManifest(pluginEntry: {\r\n id: string;\r\n name: string;\r\n description?: string;\r\n configSchema?: unknown;\r\n contracts?: unknown;\r\n activation?: unknown;\r\n}): void {\r\n // Read version from package.json in the plugin's root directory.\r\n // process.cwd() is the plugin root when invoked via `npm run build`.\r\n let version = \"0.0.0\";\r\n try {\r\n const pkg = JSON.parse(readFileSync(resolve(process.cwd(), \"package.json\"), \"utf8\"));\r\n version = pkg.version ?? version;\r\n } catch {\r\n console.warn(\"Warning: could not read package.json for version — defaulting to 0.0.0\");\r\n }\r\n\r\n const manifest: Record<string, unknown> = {\r\n id: pluginEntry.id,\r\n name: pluginEntry.name,\r\n description: pluginEntry.description ?? \"\",\r\n version,\r\n // The adapter is always at this path — generated by generateAdapter() above.\r\n entry: \"./dist/adapter.js\",\r\n };\r\n\r\n if (pluginEntry.configSchema) {\r\n manifest.configSchema = pluginEntry.configSchema;\r\n }\r\n\r\n if (pluginEntry.contracts) {\r\n manifest.contracts = pluginEntry.contracts;\r\n }\r\n\r\n // activation defaults to onStartup: true if not specified.\r\n manifest.activation = pluginEntry.activation ?? { onStartup: true };\r\n\r\n // openclaw is a host-specific extension block. Default to empty.\r\n manifest.openclaw = {};\r\n\r\n const outPath = resolve(process.cwd(), \"openclaw.plugin.json\");\r\n writeFileSync(outPath, JSON.stringify(manifest, null, 2) + \"\\n\");\r\n console.log(`Generated manifest: ${outPath}`);\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// Main\r\n// ---------------------------------------------------------------------------\r\n\r\nasync function main() {\r\n const { entry, outDir, name } = parseGeneratorArgs();\r\n\r\n const entryUrl = pathToFileURL(entry).href;\r\n const mod = await import(entryUrl);\r\n\r\n const createEntry = mod.createEntry ?? mod.default?.createEntry;\r\n if (typeof createEntry !== \"function\") {\r\n console.error(`Error: ${entry} does not export createEntry()`);\r\n process.exit(1);\r\n }\r\n\r\n const pluginEntry = createEntry();\r\n const binName = name ?? pluginEntry.id;\r\n const envPrefix = binName.replace(/-/g, \"_\").toUpperCase();\r\n\r\n generateCli(entry, outDir, binName, envPrefix);\r\n generateAdapter(entry);\r\n generateManifest(pluginEntry);\r\n}\r\n\r\nmain().catch((e) => {\r\n console.error(e);\r\n process.exit(1);\r\n});\r\n"],"mappings":"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","names":[]}
@@ -0,0 +1,200 @@
1
+ import { TObject, Static } from '@sinclair/typebox';
2
+
3
+ /**
4
+ * carapace-plugin-sdk — Core types and helpers for OpenClaw plugins.
5
+ *
6
+ * This is the main entry point. Import `definePlugin` to author a plugin
7
+ * with full TypeScript inference — typed config and typed tool parameters
8
+ * with no boilerplate.
9
+ *
10
+ * Quick-start:
11
+ *
12
+ * src/plugin.ts — the only file you write; export `createEntry` from `definePlugin`
13
+ * dist/adapter.js — auto-generated at build time; do not write by hand
14
+ *
15
+ * @module carapace-plugin-sdk
16
+ */
17
+
18
+ /**
19
+ * Internal (erased) tool definition stored at runtime.
20
+ * The typed version lives only in TypeScript's type system via ToolFactory<TConfig>.
21
+ */
22
+ interface ToolDef {
23
+ name: string;
24
+ label?: string;
25
+ description: string;
26
+ parameters: unknown;
27
+ execute(params: Record<string, unknown>, config: unknown): Promise<unknown>;
28
+ }
29
+ /**
30
+ * Typed tool factory injected into the `tools` callback of `definePlugin`.
31
+ *
32
+ * TConfig is fixed by the enclosing `definePlugin` call, so every tool in the
33
+ * array receives the same config type. TSchema is inferred per tool from the
34
+ * `parameters` field, giving typed `params` in `execute`.
35
+ *
36
+ * You never construct this directly — it is passed to you by `definePlugin`.
37
+ */
38
+ type ToolFactory<TConfig> = <TSchema extends TObject>(def: {
39
+ /** Machine-readable name, used as the CLI subcommand. snake_case recommended. */
40
+ name: string;
41
+ /** Human-readable label shown in OpenClaw's UI. Defaults to `name`. */
42
+ label?: string;
43
+ /** One-sentence description shown in --help and OpenClaw's tool inspector. */
44
+ description: string;
45
+ /**
46
+ * TypeBox schema for the tool's parameters.
47
+ * The type is inferred as `Static<TSchema>` in `execute`'s first argument.
48
+ */
49
+ parameters: TSchema;
50
+ /**
51
+ * The tool's implementation.
52
+ *
53
+ * @param params - Typed parameters derived from `parameters` schema. No casts needed.
54
+ * @param config - Typed config derived from `definePlugin`'s `configSchema`. No casts needed.
55
+ * @returns Any JSON-serialisable value. The SDK wraps it in the OpenClaw result format.
56
+ */
57
+ execute(params: Static<TSchema>, config: TConfig): Promise<unknown>;
58
+ }) => ToolDef;
59
+ /**
60
+ * Define an OpenClaw plugin with full TypeScript inference.
61
+ *
62
+ * Returns a `createEntry` function — export it from your `src/index.ts`.
63
+ * The SDK handles all registration, result wrapping, and config plumbing.
64
+ *
65
+ * Config type is inferred from `configSchema` and flows into every tool's
66
+ * `execute(params, config)` without any manual type annotations.
67
+ *
68
+ * @example
69
+ * // src/index.ts — the entire plugin
70
+ * import { definePlugin } from "carapace-plugin-sdk";
71
+ * import { Type } from "@sinclair/typebox";
72
+ *
73
+ * export const createEntry = definePlugin({
74
+ * id: "my-plugin",
75
+ * name: "My Plugin",
76
+ * configSchema: Type.Object({
77
+ * apiKey: Type.Optional(Type.String({ description: "API key." })),
78
+ * }),
79
+ * tools: (tool) => [
80
+ * tool({
81
+ * name: "do_thing",
82
+ * description: "Does the thing.",
83
+ * parameters: Type.Object({
84
+ * input: Type.String({ description: "Input value." }),
85
+ * }),
86
+ * execute: async ({ input }, config) => {
87
+ * // input: string ✓ config.apiKey: string | undefined ✓
88
+ * return { result: input };
89
+ * },
90
+ * }),
91
+ * ],
92
+ * });
93
+ */
94
+ declare function definePlugin<TConfigSchema extends TObject = TObject>(def: {
95
+ /** Unique plugin id. Lowercase alphanumeric with hyphens. Used as the CLI binary name. */
96
+ id: string;
97
+ /** Human-readable display name. */
98
+ name: string;
99
+ /** One-sentence description of what the plugin does. */
100
+ description?: string;
101
+ /** When to load the plugin. Defaults to `{ onStartup: true }`. */
102
+ activation?: {
103
+ onStartup?: boolean;
104
+ };
105
+ /**
106
+ * TypeBox schema for the plugin's config block.
107
+ *
108
+ * Used for three things simultaneously:
109
+ * 1. Runtime JSON Schema for the OpenClaw manifest (validated before register())
110
+ * 2. TypeScript type inference for `config` in every tool's `execute`
111
+ * 3. Environment variable mapping for the standalone CLI
112
+ */
113
+ configSchema?: TConfigSchema;
114
+ /**
115
+ * Declare your tools here. Receives a typed `tool()` factory as its argument.
116
+ *
117
+ * Using a callback (rather than a plain array) is what allows TypeScript to
118
+ * thread the config type through to each tool's `execute` function.
119
+ */
120
+ tools: (tool: ToolFactory<Static<TConfigSchema>>) => ToolDef[];
121
+ }): () => PluginEntry;
122
+ /**
123
+ * The API object passed to your plugin's `register()` function.
124
+ *
125
+ * When using `definePlugin`, you never see this directly — the SDK handles it.
126
+ * It is exported for advanced use cases and for the generated adapter.
127
+ */
128
+ type PluginApi = {
129
+ /** Register a tool with OpenClaw. Call once per tool inside register(). */
130
+ registerTool: (tool: unknown) => void;
131
+ /**
132
+ * The user's config values for this plugin, keyed by field name.
133
+ * Validated against configSchema by OpenClaw before register() is called.
134
+ * May be undefined if the user has not configured the plugin.
135
+ */
136
+ pluginConfig?: Record<string, unknown>;
137
+ };
138
+ /**
139
+ * The object returned by `createEntry()` — the plugin's public contract.
140
+ *
141
+ * When using `definePlugin`, this is constructed automatically.
142
+ * Exported for advanced plugins that build it manually.
143
+ */
144
+ interface PluginEntry {
145
+ /** Unique plugin id. Used as the CLI binary name and OpenClaw config key. */
146
+ id: string;
147
+ /** Human-readable display name. */
148
+ name: string;
149
+ /** One-sentence description. */
150
+ description?: string;
151
+ /** Tool names this plugin promises to register. Derived automatically by `definePlugin`. */
152
+ contracts?: {
153
+ tools: string[];
154
+ };
155
+ /** When to load the plugin. Defaults to `{ onStartup: true }`. */
156
+ activation?: {
157
+ onStartup?: boolean;
158
+ };
159
+ /**
160
+ * JSON Schema for the plugin's config block.
161
+ * Pass a TypeBox `Type.Object(...)` — it is valid JSON Schema and gives you type inference.
162
+ */
163
+ configSchema?: unknown;
164
+ /** Called once by OpenClaw at startup. Use `definePlugin` instead of implementing this directly. */
165
+ register(api: PluginApi): void;
166
+ }
167
+ /**
168
+ * Wrap a value in the standard OpenClaw tool result format.
169
+ *
170
+ * When using `definePlugin`, you do NOT call this yourself — the SDK calls it
171
+ * automatically after your `execute` function returns.
172
+ *
173
+ * For advanced plugins that implement `register()` directly, wrap your
174
+ * execute return values with this function.
175
+ *
176
+ * @param data - Anything JSON-serialisable, or a plain string.
177
+ * @returns `{ content: [{ type: "text", text: "<json>" }], details: {} }`
178
+ */
179
+ declare function formatResult(data: unknown): {
180
+ content: {
181
+ type: "text";
182
+ text: string;
183
+ }[];
184
+ details: {};
185
+ };
186
+ /**
187
+ * Create the plugin's OpenClaw adapter export.
188
+ *
189
+ * **You never call this yourself.** It is called by the generated `dist/adapter.js`.
190
+ *
191
+ * Attempts to load the optional `openclaw` peer dependency and wrap the plugin
192
+ * entry with the host's `definePluginEntry()`. Falls back to the raw entry if
193
+ * `openclaw` is not installed (standalone CLI mode).
194
+ *
195
+ * @param entry - The object returned by `createEntry()`.
196
+ * @param callerUrl - Pass `import.meta.url` from the generated adapter file.
197
+ */
198
+ declare function createAdapter(entry: PluginEntry, callerUrl: string): unknown;
199
+
200
+ export { type PluginApi, type PluginEntry, createAdapter, definePlugin, formatResult };
package/dist/index.js ADDED
@@ -0,0 +1,72 @@
1
+ // src/index.ts
2
+ import { createRequire } from "module";
3
+ function definePlugin(def) {
4
+ return () => {
5
+ const toolFactory = ((toolDef) => toolDef);
6
+ const toolDefs = def.tools(toolFactory);
7
+ return {
8
+ id: def.id,
9
+ name: def.name,
10
+ description: def.description,
11
+ activation: def.activation ?? { onStartup: true },
12
+ // Derive contracts from the declared tools so the manifest is always accurate.
13
+ contracts: { tools: toolDefs.map((t) => t.name) },
14
+ // TypeBox TObject is valid JSON Schema — pass through for the manifest generator.
15
+ configSchema: def.configSchema,
16
+ register(api) {
17
+ const config = api.pluginConfig ?? {};
18
+ for (const toolDef of toolDefs) {
19
+ api.registerTool({
20
+ name: toolDef.name,
21
+ label: toolDef.label ?? toolDef.name,
22
+ description: toolDef.description,
23
+ parameters: toolDef.parameters,
24
+ // Wrap the result automatically — execute() returns plain values, not formatResult().
25
+ execute: async (_toolCallId, params) => formatResult(await toolDef.execute(params, config))
26
+ });
27
+ }
28
+ }
29
+ };
30
+ };
31
+ }
32
+ function formatResult(data) {
33
+ let text;
34
+ if (typeof data === "string") {
35
+ text = data;
36
+ } else {
37
+ try {
38
+ text = JSON.stringify(data) ?? String(data);
39
+ } catch {
40
+ text = String(data);
41
+ }
42
+ }
43
+ return {
44
+ content: [{ type: "text", text }],
45
+ details: {}
46
+ };
47
+ }
48
+ function createAdapter(entry, callerUrl) {
49
+ const req = createRequire(callerUrl);
50
+ try {
51
+ const sdk = req("openclaw/plugin-sdk/plugin-entry");
52
+ if (typeof sdk.definePluginEntry !== "function") {
53
+ throw new Error(
54
+ "OpenClaw SDK loaded but did not export `definePluginEntry`. Upgrade the `openclaw` package."
55
+ );
56
+ }
57
+ return sdk.definePluginEntry(entry);
58
+ } catch (err) {
59
+ if (isModuleNotFoundError(err)) return entry;
60
+ throw err;
61
+ }
62
+ }
63
+ function isModuleNotFoundError(err) {
64
+ if (!(err instanceof Error)) return false;
65
+ return "code" in err && (err.code === "MODULE_NOT_FOUND" || err.code === "ERR_MODULE_NOT_FOUND");
66
+ }
67
+ export {
68
+ createAdapter,
69
+ definePlugin,
70
+ formatResult
71
+ };
72
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\r\n * carapace-plugin-sdk — Core types and helpers for OpenClaw plugins.\r\n *\r\n * This is the main entry point. Import `definePlugin` to author a plugin\r\n * with full TypeScript inference — typed config and typed tool parameters\r\n * with no boilerplate.\r\n *\r\n * Quick-start:\r\n *\r\n * src/plugin.ts — the only file you write; export `createEntry` from `definePlugin`\r\n * dist/adapter.js — auto-generated at build time; do not write by hand\r\n *\r\n * @module carapace-plugin-sdk\r\n */\r\n\r\nimport { createRequire } from \"node:module\";\r\nimport { type TObject, type Static } from \"@sinclair/typebox\";\r\n\r\n// ---------------------------------------------------------------------------\r\n// definePlugin — the primary authoring API\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Internal (erased) tool definition stored at runtime.\r\n * The typed version lives only in TypeScript's type system via ToolFactory<TConfig>.\r\n */\r\ninterface ToolDef {\r\n name: string;\r\n label?: string;\r\n description: string;\r\n parameters: unknown;\r\n execute(params: Record<string, unknown>, config: unknown): Promise<unknown>;\r\n}\r\n\r\n/**\r\n * Typed tool factory injected into the `tools` callback of `definePlugin`.\r\n *\r\n * TConfig is fixed by the enclosing `definePlugin` call, so every tool in the\r\n * array receives the same config type. TSchema is inferred per tool from the\r\n * `parameters` field, giving typed `params` in `execute`.\r\n *\r\n * You never construct this directly — it is passed to you by `definePlugin`.\r\n */\r\ntype ToolFactory<TConfig> = <TSchema extends TObject>(def: {\r\n /** Machine-readable name, used as the CLI subcommand. snake_case recommended. */\r\n name: string;\r\n /** Human-readable label shown in OpenClaw's UI. Defaults to `name`. */\r\n label?: string;\r\n /** One-sentence description shown in --help and OpenClaw's tool inspector. */\r\n description: string;\r\n /**\r\n * TypeBox schema for the tool's parameters.\r\n * The type is inferred as `Static<TSchema>` in `execute`'s first argument.\r\n */\r\n parameters: TSchema;\r\n /**\r\n * The tool's implementation.\r\n *\r\n * @param params - Typed parameters derived from `parameters` schema. No casts needed.\r\n * @param config - Typed config derived from `definePlugin`'s `configSchema`. No casts needed.\r\n * @returns Any JSON-serialisable value. The SDK wraps it in the OpenClaw result format.\r\n */\r\n execute(params: Static<TSchema>, config: TConfig): Promise<unknown>;\r\n}) => ToolDef;\r\n\r\n/**\r\n * Define an OpenClaw plugin with full TypeScript inference.\r\n *\r\n * Returns a `createEntry` function — export it from your `src/index.ts`.\r\n * The SDK handles all registration, result wrapping, and config plumbing.\r\n *\r\n * Config type is inferred from `configSchema` and flows into every tool's\r\n * `execute(params, config)` without any manual type annotations.\r\n *\r\n * @example\r\n * // src/index.ts — the entire plugin\r\n * import { definePlugin } from \"carapace-plugin-sdk\";\r\n * import { Type } from \"@sinclair/typebox\";\r\n *\r\n * export const createEntry = definePlugin({\r\n * id: \"my-plugin\",\r\n * name: \"My Plugin\",\r\n * configSchema: Type.Object({\r\n * apiKey: Type.Optional(Type.String({ description: \"API key.\" })),\r\n * }),\r\n * tools: (tool) => [\r\n * tool({\r\n * name: \"do_thing\",\r\n * description: \"Does the thing.\",\r\n * parameters: Type.Object({\r\n * input: Type.String({ description: \"Input value.\" }),\r\n * }),\r\n * execute: async ({ input }, config) => {\r\n * // input: string ✓ config.apiKey: string | undefined ✓\r\n * return { result: input };\r\n * },\r\n * }),\r\n * ],\r\n * });\r\n */\r\nexport function definePlugin<TConfigSchema extends TObject = TObject>(def: {\r\n /** Unique plugin id. Lowercase alphanumeric with hyphens. Used as the CLI binary name. */\r\n id: string;\r\n /** Human-readable display name. */\r\n name: string;\r\n /** One-sentence description of what the plugin does. */\r\n description?: string;\r\n /** When to load the plugin. Defaults to `{ onStartup: true }`. */\r\n activation?: { onStartup?: boolean };\r\n /**\r\n * TypeBox schema for the plugin's config block.\r\n *\r\n * Used for three things simultaneously:\r\n * 1. Runtime JSON Schema for the OpenClaw manifest (validated before register())\r\n * 2. TypeScript type inference for `config` in every tool's `execute`\r\n * 3. Environment variable mapping for the standalone CLI\r\n */\r\n configSchema?: TConfigSchema;\r\n /**\r\n * Declare your tools here. Receives a typed `tool()` factory as its argument.\r\n *\r\n * Using a callback (rather than a plain array) is what allows TypeScript to\r\n * thread the config type through to each tool's `execute` function.\r\n */\r\n tools: (tool: ToolFactory<Static<TConfigSchema>>) => ToolDef[];\r\n}): () => PluginEntry {\r\n return () => {\r\n // The factory is identity at runtime — all type magic is compile-time only.\r\n const toolFactory = ((toolDef: unknown) => toolDef) as ToolFactory<Static<TConfigSchema>>;\r\n const toolDefs = def.tools(toolFactory);\r\n\r\n return {\r\n id: def.id,\r\n name: def.name,\r\n description: def.description,\r\n activation: def.activation ?? { onStartup: true },\r\n // Derive contracts from the declared tools so the manifest is always accurate.\r\n contracts: { tools: toolDefs.map((t) => t.name) },\r\n // TypeBox TObject is valid JSON Schema — pass through for the manifest generator.\r\n configSchema: def.configSchema as unknown as PluginEntry[\"configSchema\"],\r\n register(api: PluginApi) {\r\n // OpenClaw validates pluginConfig against configSchema before calling register(),\r\n // so this cast is safe. Fall back to empty object if config is not yet set.\r\n const config = (api.pluginConfig ?? {}) as Static<TConfigSchema>;\r\n\r\n for (const toolDef of toolDefs) {\r\n api.registerTool({\r\n name: toolDef.name,\r\n label: toolDef.label ?? toolDef.name,\r\n description: toolDef.description,\r\n parameters: toolDef.parameters,\r\n // Wrap the result automatically — execute() returns plain values, not formatResult().\r\n execute: async (_toolCallId: string, params: Record<string, unknown>) =>\r\n formatResult(await toolDef.execute(params, config)),\r\n });\r\n }\r\n },\r\n };\r\n };\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// Low-level types — the plugin contract\r\n//\r\n// These are used by the generated adapter and by advanced plugins that need\r\n// more control than definePlugin provides (e.g. dynamic tool registration).\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * The API object passed to your plugin's `register()` function.\r\n *\r\n * When using `definePlugin`, you never see this directly — the SDK handles it.\r\n * It is exported for advanced use cases and for the generated adapter.\r\n */\r\nexport type PluginApi = {\r\n /** Register a tool with OpenClaw. Call once per tool inside register(). */\r\n registerTool: (tool: unknown) => void;\r\n /**\r\n * The user's config values for this plugin, keyed by field name.\r\n * Validated against configSchema by OpenClaw before register() is called.\r\n * May be undefined if the user has not configured the plugin.\r\n */\r\n pluginConfig?: Record<string, unknown>;\r\n};\r\n\r\n/**\r\n * The object returned by `createEntry()` — the plugin's public contract.\r\n *\r\n * When using `definePlugin`, this is constructed automatically.\r\n * Exported for advanced plugins that build it manually.\r\n */\r\nexport interface PluginEntry {\r\n /** Unique plugin id. Used as the CLI binary name and OpenClaw config key. */\r\n id: string;\r\n /** Human-readable display name. */\r\n name: string;\r\n /** One-sentence description. */\r\n description?: string;\r\n /** Tool names this plugin promises to register. Derived automatically by `definePlugin`. */\r\n contracts?: { tools: string[] };\r\n /** When to load the plugin. Defaults to `{ onStartup: true }`. */\r\n activation?: { onStartup?: boolean };\r\n /**\r\n * JSON Schema for the plugin's config block.\r\n * Pass a TypeBox `Type.Object(...)` — it is valid JSON Schema and gives you type inference.\r\n */\r\n configSchema?: unknown;\r\n /** Called once by OpenClaw at startup. Use `definePlugin` instead of implementing this directly. */\r\n register(api: PluginApi): void;\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// Helpers\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Wrap a value in the standard OpenClaw tool result format.\r\n *\r\n * When using `definePlugin`, you do NOT call this yourself — the SDK calls it\r\n * automatically after your `execute` function returns.\r\n *\r\n * For advanced plugins that implement `register()` directly, wrap your\r\n * execute return values with this function.\r\n *\r\n * @param data - Anything JSON-serialisable, or a plain string.\r\n * @returns `{ content: [{ type: \"text\", text: \"<json>\" }], details: {} }`\r\n */\r\nexport function formatResult(data: unknown) {\r\n let text: string;\r\n if (typeof data === \"string\") {\r\n text = data;\r\n } else {\r\n try {\r\n text = JSON.stringify(data) ?? String(data);\r\n } catch {\r\n text = String(data);\r\n }\r\n }\r\n return {\r\n content: [{ type: \"text\" as const, text }],\r\n details: {},\r\n };\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// Adapter factory\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Create the plugin's OpenClaw adapter export.\r\n *\r\n * **You never call this yourself.** It is called by the generated `dist/adapter.js`.\r\n *\r\n * Attempts to load the optional `openclaw` peer dependency and wrap the plugin\r\n * entry with the host's `definePluginEntry()`. Falls back to the raw entry if\r\n * `openclaw` is not installed (standalone CLI mode).\r\n *\r\n * @param entry - The object returned by `createEntry()`.\r\n * @param callerUrl - Pass `import.meta.url` from the generated adapter file.\r\n */\r\nexport function createAdapter(entry: PluginEntry, callerUrl: string): unknown {\r\n const req = createRequire(callerUrl);\r\n\r\n try {\r\n const sdk = req(\"openclaw/plugin-sdk/plugin-entry\") as {\r\n definePluginEntry?: (e: unknown) => unknown;\r\n };\r\n\r\n if (typeof sdk.definePluginEntry !== \"function\") {\r\n throw new Error(\r\n \"OpenClaw SDK loaded but did not export `definePluginEntry`. Upgrade the `openclaw` package.\",\r\n );\r\n }\r\n\r\n return sdk.definePluginEntry(entry);\r\n } catch (err: unknown) {\r\n if (isModuleNotFoundError(err)) return entry;\r\n throw err;\r\n }\r\n}\r\n\r\nfunction isModuleNotFoundError(err: unknown): boolean {\r\n if (!(err instanceof Error)) return false;\r\n return (\r\n \"code\" in err &&\r\n ((err as { code: string }).code === \"MODULE_NOT_FOUND\" ||\r\n (err as { code: string }).code === \"ERR_MODULE_NOT_FOUND\")\r\n );\r\n}\r\n"],"mappings":";AAeA,SAAS,qBAAqB;AAqFvB,SAAS,aAAsD,KAyBhD;AACpB,SAAO,MAAM;AAEX,UAAM,eAAe,CAAC,YAAqB;AAC3C,UAAM,WAAW,IAAI,MAAM,WAAW;AAEtC,WAAO;AAAA,MACL,IAAI,IAAI;AAAA,MACR,MAAM,IAAI;AAAA,MACV,aAAa,IAAI;AAAA,MACjB,YAAY,IAAI,cAAc,EAAE,WAAW,KAAK;AAAA;AAAA,MAEhD,WAAW,EAAE,OAAO,SAAS,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE;AAAA;AAAA,MAEhD,cAAc,IAAI;AAAA,MAClB,SAAS,KAAgB;AAGvB,cAAM,SAAU,IAAI,gBAAgB,CAAC;AAErC,mBAAW,WAAW,UAAU;AAC9B,cAAI,aAAa;AAAA,YACf,MAAM,QAAQ;AAAA,YACd,OAAO,QAAQ,SAAS,QAAQ;AAAA,YAChC,aAAa,QAAQ;AAAA,YACrB,YAAY,QAAQ;AAAA;AAAA,YAEpB,SAAS,OAAO,aAAqB,WACnC,aAAa,MAAM,QAAQ,QAAQ,QAAQ,MAAM,CAAC;AAAA,UACtD,CAAC;AAAA,QACH;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAoEO,SAAS,aAAa,MAAe;AAC1C,MAAI;AACJ,MAAI,OAAO,SAAS,UAAU;AAC5B,WAAO;AAAA,EACT,OAAO;AACL,QAAI;AACF,aAAO,KAAK,UAAU,IAAI,KAAK,OAAO,IAAI;AAAA,IAC5C,QAAQ;AACN,aAAO,OAAO,IAAI;AAAA,IACpB;AAAA,EACF;AACA,SAAO;AAAA,IACL,SAAS,CAAC,EAAE,MAAM,QAAiB,KAAK,CAAC;AAAA,IACzC,SAAS,CAAC;AAAA,EACZ;AACF;AAkBO,SAAS,cAAc,OAAoB,WAA4B;AAC5E,QAAM,MAAM,cAAc,SAAS;AAEnC,MAAI;AACF,UAAM,MAAM,IAAI,kCAAkC;AAIlD,QAAI,OAAO,IAAI,sBAAsB,YAAY;AAC/C,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAEA,WAAO,IAAI,kBAAkB,KAAK;AAAA,EACpC,SAAS,KAAc;AACrB,QAAI,sBAAsB,GAAG,EAAG,QAAO;AACvC,UAAM;AAAA,EACR;AACF;AAEA,SAAS,sBAAsB,KAAuB;AACpD,MAAI,EAAE,eAAe,OAAQ,QAAO;AACpC,SACE,UAAU,QACR,IAAyB,SAAS,sBACjC,IAAyB,SAAS;AAEzC;","names":[]}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * carapace-plugin-sdk/tsup — Shared tsup configuration factory for OpenClaw plugins.
3
+ *
4
+ * Import this in your plugin's tsup.config.ts to get the standard build
5
+ * settings without repeating them in every plugin:
6
+ *
7
+ * ```ts
8
+ * // tsup.config.ts (the whole file)
9
+ * import { defineConfig } from "tsup";
10
+ * import { definePluginConfig } from "carapace-plugin-sdk/tsup";
11
+ * export default defineConfig(definePluginConfig());
12
+ * ```
13
+ *
14
+ * Works identically in standalone plugin repos and in workspace monorepos —
15
+ * the configuration is the same in both cases.
16
+ *
17
+ * To add extra entry points or override any setting, pass an overrides object:
18
+ *
19
+ * ```ts
20
+ * export default defineConfig(definePluginConfig({
21
+ * entry: ["src/plugin.ts", "src/extra.ts"],
22
+ * }));
23
+ * ```
24
+ *
25
+ * @module carapace-plugin-sdk/tsup
26
+ */
27
+ /**
28
+ * Options accepted by tsup's defineConfig. Typed loosely so this file does
29
+ * not need tsup as a runtime dependency — the plugin's own tsup install is used.
30
+ */
31
+ type PluginConfigOverrides = Record<string, unknown>;
32
+ /**
33
+ * Returns the standard tsup configuration for an OpenClaw plugin.
34
+ *
35
+ * Defaults:
36
+ * - entry: ["src/plugin.ts"] — your plugin definition file
37
+ * - format: ESM only (OpenClaw and Node 20+ are fully ESM)
38
+ * - outDir: dist
39
+ * - dts: false (type declarations are not needed at runtime)
40
+ * - sourcemap: true (for debuggable stack traces)
41
+ * - clean: true (remove stale dist files before each build)
42
+ * - target: node20
43
+ * - splitting, shims: false (keeps output simple and portable)
44
+ * - skipNodeModulesBundle: true (dependencies are not inlined)
45
+ *
46
+ * Note: `src/adapter.ts` is intentionally NOT in the default entry list.
47
+ * The adapter (`dist/adapter.js`) is generated at build time by
48
+ * `carapace-generate-cli` and does not need to be compiled from source.
49
+ *
50
+ * @param overrides - Any tsup config options to merge on top of the defaults.
51
+ */
52
+ declare function definePluginConfig(overrides?: PluginConfigOverrides): PluginConfigOverrides;
53
+
54
+ export { definePluginConfig };
@@ -0,0 +1,20 @@
1
+ // src/tsup.plugin.config.ts
2
+ function definePluginConfig(overrides = {}) {
3
+ return {
4
+ entry: ["src/plugin.ts"],
5
+ format: ["esm"],
6
+ outDir: "dist",
7
+ dts: false,
8
+ sourcemap: true,
9
+ clean: true,
10
+ target: "node20",
11
+ splitting: false,
12
+ shims: false,
13
+ skipNodeModulesBundle: true,
14
+ ...overrides
15
+ };
16
+ }
17
+ export {
18
+ definePluginConfig
19
+ };
20
+ //# sourceMappingURL=tsup.plugin.config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/tsup.plugin.config.ts"],"sourcesContent":["/**\n * carapace-plugin-sdk/tsup — Shared tsup configuration factory for OpenClaw plugins.\n *\n * Import this in your plugin's tsup.config.ts to get the standard build\n * settings without repeating them in every plugin:\n *\n * ```ts\n * // tsup.config.ts (the whole file)\n * import { defineConfig } from \"tsup\";\n * import { definePluginConfig } from \"carapace-plugin-sdk/tsup\";\n * export default defineConfig(definePluginConfig());\n * ```\n *\n * Works identically in standalone plugin repos and in workspace monorepos —\n * the configuration is the same in both cases.\n *\n * To add extra entry points or override any setting, pass an overrides object:\n *\n * ```ts\n * export default defineConfig(definePluginConfig({\n * entry: [\"src/plugin.ts\", \"src/extra.ts\"],\n * }));\n * ```\n *\n * @module carapace-plugin-sdk/tsup\n */\n\n/**\n * Options accepted by tsup's defineConfig. Typed loosely so this file does\n * not need tsup as a runtime dependency — the plugin's own tsup install is used.\n */\ntype PluginConfigOverrides = Record<string, unknown>;\n\n/**\n * Returns the standard tsup configuration for an OpenClaw plugin.\n *\n * Defaults:\n * - entry: [\"src/plugin.ts\"] — your plugin definition file\n * - format: ESM only (OpenClaw and Node 20+ are fully ESM)\n * - outDir: dist\n * - dts: false (type declarations are not needed at runtime)\n * - sourcemap: true (for debuggable stack traces)\n * - clean: true (remove stale dist files before each build)\n * - target: node20\n * - splitting, shims: false (keeps output simple and portable)\n * - skipNodeModulesBundle: true (dependencies are not inlined)\n *\n * Note: `src/adapter.ts` is intentionally NOT in the default entry list.\n * The adapter (`dist/adapter.js`) is generated at build time by\n * `carapace-generate-cli` and does not need to be compiled from source.\n *\n * @param overrides - Any tsup config options to merge on top of the defaults.\n */\nexport function definePluginConfig(overrides: PluginConfigOverrides = {}): PluginConfigOverrides {\n return {\n entry: [\"src/plugin.ts\"],\n format: [\"esm\"],\n outDir: \"dist\",\n dts: false,\n sourcemap: true,\n clean: true,\n target: \"node20\",\n splitting: false,\n shims: false,\n skipNodeModulesBundle: true,\n ...overrides,\n };\n}\n"],"mappings":";AAqDO,SAAS,mBAAmB,YAAmC,CAAC,GAA0B;AAC/F,SAAO;AAAA,IACL,OAAO,CAAC,eAAe;AAAA,IACvB,QAAQ,CAAC,KAAK;AAAA,IACd,QAAQ;AAAA,IACR,KAAK;AAAA,IACL,WAAW;AAAA,IACX,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,WAAW;AAAA,IACX,OAAO;AAAA,IACP,uBAAuB;AAAA,IACvB,GAAG;AAAA,EACL;AACF;","names":[]}
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "carapace-plugin-sdk",
3
+ "version": "1.0.0",
4
+ "description": "🦞🐚 SDK for building OpenClaw plugins — types, helpers, CLI generation",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "import": "./dist/index.js"
10
+ },
11
+ "./cli": {
12
+ "types": "./dist/cli.d.ts",
13
+ "import": "./dist/cli.js"
14
+ },
15
+ "./tsup": {
16
+ "import": "./dist/tsup.plugin.config.js"
17
+ },
18
+ "./tsconfig.base.json": "./tsconfig.base.json"
19
+ },
20
+ "bin": {
21
+ "carapace-generate-cli": "./dist/generate-cli.js"
22
+ },
23
+ "scripts": {
24
+ "build": "tsup",
25
+ "prepare": "tsup",
26
+ "test": "vitest run"
27
+ },
28
+ "files": [
29
+ "dist",
30
+ "tsconfig.base.json",
31
+ "README.md",
32
+ "LICENSE"
33
+ ],
34
+ "keywords": [
35
+ "openclaw",
36
+ "plugin",
37
+ "sdk",
38
+ "carapace",
39
+ "cli"
40
+ ],
41
+ "license": "MIT",
42
+ "devDependencies": {
43
+ "@types/node": "^22",
44
+ "tsup": "^8.3.0",
45
+ "typescript": "^5.7",
46
+ "vitest": "^2.1.8"
47
+ },
48
+ "engines": {
49
+ "node": ">=20"
50
+ },
51
+ "dependencies": {
52
+ "@sinclair/typebox": "^0.34.0"
53
+ }
54
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "outDir": "dist",
7
+ "rootDir": "src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "declaration": true,
11
+ "types": ["node"]
12
+ },
13
+ "exclude": ["tests", "vitest.config.ts", "dist", "node_modules"]
14
+ }