@kvasar/openclaw-storyblok-plugin 0.1.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/README.md +58 -0
- package/openclaw.plugin.json +50 -0
- package/package.json +44 -0
- package/src/__tests__/README.md +115 -0
- package/src/__tests__/openclaw.plugin.json +30 -0
- package/src/__tests__/package-lock.json +1555 -0
- package/src/__tests__/package.json +22 -0
- package/src/__tests__/storyblok.test.ts +55 -0
- package/src/__tests__/tsconfig.json +19 -0
- package/src/client.ts +176 -0
- package/src/config.ts +35 -0
- package/src/index.ts +228 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "openclaw-storyblok-plugin",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "OpenClaw plugin for Storyblok AI page generation (thin client)",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"typecheck": "tsc --noEmit",
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"test": "vitest run"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@sinclair/typebox": "^0.34.30",
|
|
15
|
+
"node-fetch": "^3.3.2"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/node": "^22.10.6",
|
|
19
|
+
"typescript": "^5.7.3",
|
|
20
|
+
"vitest": "^2.1.8"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import fetch from 'node-fetch';
|
|
3
|
+
|
|
4
|
+
vi.mock('node-fetch');
|
|
5
|
+
|
|
6
|
+
describe('storyblok_generate_page', () => {
|
|
7
|
+
const mockConfig = {
|
|
8
|
+
serviceUrl: 'http://localhost:8000',
|
|
9
|
+
apiKey: 'fake-key',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// We need to import the plugin after mocking fetch
|
|
13
|
+
// but we can't directly import the tool. Instead, we'll test the fetch call.
|
|
14
|
+
// For simplicity, we'll just test the fetch mock.
|
|
15
|
+
|
|
16
|
+
it('should call service with correct URL and headers', async () => {
|
|
17
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
18
|
+
ok: true,
|
|
19
|
+
json: () => Promise.resolve({ status: 'created' }),
|
|
20
|
+
text: () => Promise.resolve(''),
|
|
21
|
+
} as any);
|
|
22
|
+
|
|
23
|
+
// Simulate the plugin's fetch call
|
|
24
|
+
const url = `${mockConfig.serviceUrl}/generate-page`;
|
|
25
|
+
const headers = {
|
|
26
|
+
'Content-Type': 'application/json',
|
|
27
|
+
Authorization: `Bearer ${mockConfig.apiKey}`,
|
|
28
|
+
};
|
|
29
|
+
const body = JSON.stringify({ prompt: 'test prompt' });
|
|
30
|
+
|
|
31
|
+
await fetch(url, {
|
|
32
|
+
method: 'POST',
|
|
33
|
+
headers,
|
|
34
|
+
body,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
expect(fetch).toHaveBeenCalledWith(url, {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers,
|
|
40
|
+
body,
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should handle service error', async () => {
|
|
45
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
46
|
+
ok: false,
|
|
47
|
+
status: 500,
|
|
48
|
+
text: () => Promise.resolve('Internal server error'),
|
|
49
|
+
} as any);
|
|
50
|
+
|
|
51
|
+
const url = `${mockConfig.serviceUrl}/generate-page`;
|
|
52
|
+
const response = await fetch(url, { method: 'POST' });
|
|
53
|
+
expect(response.ok).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"resolveJsonModule": true,
|
|
12
|
+
"isolatedModules": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"declarationMap": false,
|
|
15
|
+
"outDir": "dist"
|
|
16
|
+
},
|
|
17
|
+
"include": ["src/**/*"],
|
|
18
|
+
"exclude": ["node_modules", "dist"]
|
|
19
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storyblok client for OpenClaw plugin.
|
|
3
|
+
*
|
|
4
|
+
* Wraps Storyblok Management API (v1) and optionally Delivery API (v2) for reads.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface StoryblokConfig {
|
|
8
|
+
baseUrl: string;
|
|
9
|
+
spaceId: string;
|
|
10
|
+
managementToken: string;
|
|
11
|
+
previewToken?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class StoryblokClient {
|
|
15
|
+
private cfg: StoryblokConfig;
|
|
16
|
+
|
|
17
|
+
constructor(cfg: StoryblokConfig) {
|
|
18
|
+
this.cfg = cfg;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private async request<T>(url: string, options: RequestInit = {}): Promise<T> {
|
|
22
|
+
const res = await fetch(url, {
|
|
23
|
+
...options,
|
|
24
|
+
headers: {
|
|
25
|
+
"Content-Type": "application/json",
|
|
26
|
+
...options.headers,
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
if (!res.ok) {
|
|
31
|
+
const text = await res.text();
|
|
32
|
+
throw new Error(`HTTP ${res.status}: ${text}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// For 204 No Content, return null
|
|
36
|
+
if (res.status === 204) return null as any;
|
|
37
|
+
return res.json();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private managementUrl(path: string, params: Record<string, string> = {}): string {
|
|
41
|
+
const base = this.cfg.baseUrl.replace(/\/$/, "");
|
|
42
|
+
const url = `${base}/v1/spaces/${this.cfg.spaceId}${path}`;
|
|
43
|
+
params.token = this.cfg.managementToken;
|
|
44
|
+
const qs = new URLSearchParams(params).toString();
|
|
45
|
+
return `${url}?${qs}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private deliveryUrl(path: string, params: Record<string, string> = {}): string {
|
|
49
|
+
const base = this.cfg.baseUrl.replace(/\/$/, "");
|
|
50
|
+
const url = `${base}/v2/cdn${path}`;
|
|
51
|
+
if (this.cfg.previewToken) {
|
|
52
|
+
params.token = this.cfg.previewToken;
|
|
53
|
+
}
|
|
54
|
+
const qs = new URLSearchParams(params).toString();
|
|
55
|
+
return qs ? `${url}?${qs}` : url;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async getSpace(): Promise<any> {
|
|
59
|
+
// Management API: GET /v1/spaces/:space_id
|
|
60
|
+
const url = this.managementUrl("");
|
|
61
|
+
return this.request(url);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async getStory(identifier: string, opts: { version?: string; language?: string; svg_render?: boolean } = {}): Promise<any> {
|
|
65
|
+
// Decide API based on version: if version === 'published' and previewToken exists, use Delivery API; else Management API.
|
|
66
|
+
const version = opts.version === 'published' ? 'published' : 'draft';
|
|
67
|
+
const params: Record<string, string> = {};
|
|
68
|
+
|
|
69
|
+
// Management API endpoint: /v1/stories/:id
|
|
70
|
+
// Delivery API endpoint: /v2/cdn/stories/:slug or /v2/cdn/stories/:id
|
|
71
|
+
// We'll use Management API for flexibility and to support both versions.
|
|
72
|
+
// But note: Management API returns full content; Delivery API returns rendered content.
|
|
73
|
+
// The description mentions "svg_render" which is a Feature of Management API? Actually Storyblok management has an option to render as SVG.
|
|
74
|
+
if (opts.svg_render) params.svg = 'true';
|
|
75
|
+
if (opts.language) params.language = opts.language;
|
|
76
|
+
if (version) params.version = version;
|
|
77
|
+
|
|
78
|
+
// Use Management API (it allows version selection)
|
|
79
|
+
const url = this.managementUrl(`/stories/${encodeURIComponent(identifier)}`, params);
|
|
80
|
+
return this.request(url);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async listStories(params: {
|
|
84
|
+
folder_id?: number;
|
|
85
|
+
parent_id?: number;
|
|
86
|
+
status?: string;
|
|
87
|
+
tag?: string;
|
|
88
|
+
per_page?: number;
|
|
89
|
+
page?: number;
|
|
90
|
+
sort_by?: string;
|
|
91
|
+
direction?: string;
|
|
92
|
+
} = {}): Promise<any> {
|
|
93
|
+
const q: Record<string, string> = {};
|
|
94
|
+
if (params.folder_id !== undefined) q.folder_id = String(params.folder_id);
|
|
95
|
+
if (params.parent_id !== undefined) q.parent_id = String(params.parent_id);
|
|
96
|
+
if (params.status) q.status = params.status;
|
|
97
|
+
if (params.tag) q.tag = params.tag;
|
|
98
|
+
if (params.per_page) q.per_page = String(params.per_page);
|
|
99
|
+
if (params.page) q.page = String(params.page);
|
|
100
|
+
if (params.sort_by) q.sort_by = params.sort_by;
|
|
101
|
+
if (params.direction) q.direction = params.direction;
|
|
102
|
+
|
|
103
|
+
const url = this.managementUrl("/stories", q);
|
|
104
|
+
return this.request(url);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async createStory(data: {
|
|
108
|
+
title: string;
|
|
109
|
+
slug?: string;
|
|
110
|
+
folder_id?: number;
|
|
111
|
+
parent_id?: number;
|
|
112
|
+
content?: Record<string, any>;
|
|
113
|
+
tags?: string[];
|
|
114
|
+
is_folder?: boolean;
|
|
115
|
+
language?: string;
|
|
116
|
+
}): Promise<any> {
|
|
117
|
+
const body: Record<string, any> = { title: data.title };
|
|
118
|
+
if (data.slug) body.slug = data.slug;
|
|
119
|
+
if (data.folder_id !== undefined) body.folder_id = data.folder_id;
|
|
120
|
+
if (data.parent_id !== undefined) body.parent_id = data.parent_id;
|
|
121
|
+
if (data.content) body.content = data.content;
|
|
122
|
+
if (data.tags) body.tags = data.tags;
|
|
123
|
+
if (data.is_folder !== undefined) body.is_folder = data.is_folder;
|
|
124
|
+
if (data.language) body.language = data.language;
|
|
125
|
+
|
|
126
|
+
const url = this.managementUrl("/stories");
|
|
127
|
+
return this.request(url, { method: "POST", body: JSON.stringify(body) });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async updateStory(storyId: string, data: {
|
|
131
|
+
title?: string;
|
|
132
|
+
slug?: string;
|
|
133
|
+
content?: Record<string, any>;
|
|
134
|
+
parent_id?: number;
|
|
135
|
+
tags?: string[];
|
|
136
|
+
language?: string;
|
|
137
|
+
version?: string;
|
|
138
|
+
}): Promise<any> {
|
|
139
|
+
const body: Record<string, any> = {};
|
|
140
|
+
if (data.title) body.title = data.title;
|
|
141
|
+
if (data.slug) body.slug = data.slug;
|
|
142
|
+
if (data.content) body.content = data.content;
|
|
143
|
+
if (data.parent_id !== undefined) body.parent_id = data.parent_id;
|
|
144
|
+
if (data.tags) body.tags = data.tags;
|
|
145
|
+
if (data.language) body.language = data.language;
|
|
146
|
+
if (data.version) body.version = data.version;
|
|
147
|
+
|
|
148
|
+
const url = this.managementUrl(`/stories/${encodeURIComponent(storyId)}`);
|
|
149
|
+
return this.request(url, { method: "PUT", body: JSON.stringify(body) });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async publishStory(storyId: string, opts: { version?: string; language?: string; publish_notes?: string } = {}): Promise<any> {
|
|
153
|
+
const body: Record<string, any> = {};
|
|
154
|
+
if (opts.version) body.version = opts.version;
|
|
155
|
+
if (opts.language) body.language = opts.language;
|
|
156
|
+
if (opts.publish_notes) body.publish_notes = opts.publish_notes;
|
|
157
|
+
|
|
158
|
+
const url = this.managementUrl(`/stories/${encodeURIComponent(storyId )}/publish`);
|
|
159
|
+
return this.request(url, { method: "POST", body: JSON.stringify(body) });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async unpublishStory(storyId: string, language?: string): Promise<any> {
|
|
163
|
+
const body: Record<string, any> = {};
|
|
164
|
+
if (language) body.language = language;
|
|
165
|
+
const url = this.managementUrl(`/stories/${encodeURIComponent(storyId)}/unpublish`);
|
|
166
|
+
return this.request(url, { method: "POST", body: JSON.stringify(body) });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async getComponents(version?: string, language?: string): Promise<any> {
|
|
170
|
+
const params: Record<string, string> = {};
|
|
171
|
+
if (version) params.version = version;
|
|
172
|
+
if (language) params.language = language;
|
|
173
|
+
const url = this.managementUrl("/components", params);
|
|
174
|
+
return this.request(url);
|
|
175
|
+
}
|
|
176
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config validation and redaction helpers.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export function validateConfig(cfg: Record<string, any>): void {
|
|
6
|
+
const errors: string[] = [];
|
|
7
|
+
|
|
8
|
+
if (!cfg.baseUrl || typeof cfg.baseUrl !== "string") {
|
|
9
|
+
errors.push("baseUrl is required and must be a string");
|
|
10
|
+
}
|
|
11
|
+
if (!cfg.spaceId && cfg.spaceId !== 0) {
|
|
12
|
+
errors.push("spaceId is required");
|
|
13
|
+
}
|
|
14
|
+
if (!cfg.managementToken || typeof cfg.managementToken !== "string") {
|
|
15
|
+
errors.push("managementToken is required and must be a string");
|
|
16
|
+
}
|
|
17
|
+
if (cfg.previewToken && typeof cfg.previewToken !== "string") {
|
|
18
|
+
errors.push("previewToken, if provided, must be a string");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (errors.length > 0) {
|
|
22
|
+
throw new Error(`Invalid configuration for Storyblok plugin: ${errors.join("; ")}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function redactTokens(message: string, cfg: { managementToken?: string; previewToken?: string }): string {
|
|
27
|
+
let redacted = message;
|
|
28
|
+
if (cfg.managementToken) {
|
|
29
|
+
redacted = redacted.replace(new RegExp(cfg.managementToken, "g"), "[REDACTED]");
|
|
30
|
+
}
|
|
31
|
+
if (cfg.previewToken) {
|
|
32
|
+
redacted = redacted.replace(new RegExp(cfg.previewToken, "g"), "[REDACTED]");
|
|
33
|
+
}
|
|
34
|
+
return redacted;
|
|
35
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* index.ts — OpenClaw Storyblok Plugin
|
|
3
|
+
*
|
|
4
|
+
* Tools for interacting with Storyblok Management API and Delivery API.
|
|
5
|
+
* Authentication: Management token required for writes; preview token optional for reads.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Type } from "@sinclair/typebox";
|
|
9
|
+
import { StoryblokClient } from "./client.js";
|
|
10
|
+
import { validateConfig, redactTokens } from "./config.js";
|
|
11
|
+
|
|
12
|
+
const definePluginEntry = <T>(def: T): T => def;
|
|
13
|
+
|
|
14
|
+
// Shared response helpers
|
|
15
|
+
function ok(data: any) {
|
|
16
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }], metadata: { storyblok: true } };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function fail(error: string) {
|
|
20
|
+
return { content: [{ type: "text", text: `❌ Storyblok: ${error}` }], isError: true };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default definePluginEntry({
|
|
24
|
+
id: "openclaw-storyblok",
|
|
25
|
+
name: "Storyblok Integration",
|
|
26
|
+
description: "Interact with Storyblok CMS: manage stories, components, and space information.",
|
|
27
|
+
register(api: { config?: unknown; registerTool: (def: any, opts?: { optional?: boolean }) => void }) {
|
|
28
|
+
const rawCfg = (api.config ?? {}) as Record<string, any>;
|
|
29
|
+
validateConfig(rawCfg);
|
|
30
|
+
const cfg = {
|
|
31
|
+
baseUrl: rawCfg.baseUrl ?? "https://api.storyblok.com",
|
|
32
|
+
spaceId: rawCfg.spaceId,
|
|
33
|
+
managementToken: rawCfg.managementToken,
|
|
34
|
+
previewToken: rawCfg.previewToken,
|
|
35
|
+
};
|
|
36
|
+
const client = new StoryblokClient(cfg);
|
|
37
|
+
|
|
38
|
+
// ── storyblok_get_space ─────────────────────────────────────────────────────
|
|
39
|
+
api.registerTool({
|
|
40
|
+
name: "storyblok_get_space",
|
|
41
|
+
description: "Retrieve Storyblok space details.",
|
|
42
|
+
parameters: Type.Object({}),
|
|
43
|
+
async execute(_id: string, _params: {}) {
|
|
44
|
+
try {
|
|
45
|
+
const result = await client.getSpace();
|
|
46
|
+
return ok(result);
|
|
47
|
+
} catch (e: any) {
|
|
48
|
+
return fail(redactTokens(e.message, cfg));
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// ── storyblok_get_story ─────────────────────────────────────────────────────
|
|
54
|
+
api.registerTool({
|
|
55
|
+
name: "storyblok_get_story",
|
|
56
|
+
description: "Retrieve a story by ID, UUID, or slug. Use preview token for published version, management token for draft.",
|
|
57
|
+
parameters: Type.Object({
|
|
58
|
+
identifier: Type.String({ description: "Story ID, UUID, or slug" }),
|
|
59
|
+
version: Type.Optional(Type.String({ description: "Version to fetch: 'draft' (default) or 'published'" })),
|
|
60
|
+
language: Type.Optional(Type.String({ description: "Language code (e.g., 'en') for multilingual spaces" })),
|
|
61
|
+
svg_render: Type.Optional(Type.Boolean({ description: "If true, returns SVG render of the story (experimental)" })),
|
|
62
|
+
}),
|
|
63
|
+
async execute(_id: string, params: { identifier: string; version?: string; language?: string; svg_render?: boolean }) {
|
|
64
|
+
try {
|
|
65
|
+
const result = await client.getStory(params.identifier, { version: params.version, language: params.language, svg_render: params.svg_render });
|
|
66
|
+
return ok(result);
|
|
67
|
+
} catch (e: any) {
|
|
68
|
+
return fail(redactTokens(e.message, cfg));
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// ── storyblok_list_stories ───────────────────────────────────────────────────
|
|
74
|
+
api.registerTool({
|
|
75
|
+
name: "storyblok_list_stories",
|
|
76
|
+
description: "List stories with optional filters (folder, status, tags, etc.).",
|
|
77
|
+
parameters: Type.Object({
|
|
78
|
+
folder_id: Type.Optional(Type.Number({ description: "Filter by folder ID" })),
|
|
79
|
+
parent_id: Type.Optional(Type.Number({ description: "Filter by parent story ID" })),
|
|
80
|
+
status: Type.Optional(Type.String({ description: "Filter by status: 'draft', 'published'" })),
|
|
81
|
+
tag: Type.Optional(Type.String({ description: "Filter by tag" })),
|
|
82
|
+
per_page: Type.Optional(Type.Number({ description: "Number of results per page (max 100)", minimum: 1, maximum: 100 })),
|
|
83
|
+
page: Type.Optional(Type.Number({ description: "Page number (starting from 1)", minimum: 1 })),
|
|
84
|
+
sort_by: Type.Optional(Type.String({ description: "Sort field (e.g., 'created_at', 'published_at')" })),
|
|
85
|
+
direction: Type.Optional(Type.String({ description: "Sort direction: 'asc' or 'desc'" })),
|
|
86
|
+
}),
|
|
87
|
+
async execute(_id: string, params: {
|
|
88
|
+
folder_id?: number;
|
|
89
|
+
parent_id?: number;
|
|
90
|
+
status?: string;
|
|
91
|
+
tag?: string;
|
|
92
|
+
per_page?: number;
|
|
93
|
+
page?: number;
|
|
94
|
+
sort_by?: string;
|
|
95
|
+
direction?: string;
|
|
96
|
+
}) {
|
|
97
|
+
try {
|
|
98
|
+
const result = await client.listStories(params);
|
|
99
|
+
return ok(result);
|
|
100
|
+
} catch (e: any) {
|
|
101
|
+
return fail(redactTokens(e.message, cfg));
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// ── storyblok_create_story ───────────────────────────────────────────────────
|
|
107
|
+
api.registerTool({
|
|
108
|
+
name: "storyblok_create_story",
|
|
109
|
+
description: "Create a new story (optionally in a folder). By default creates a draft.",
|
|
110
|
+
parameters: Type.Object({
|
|
111
|
+
title: Type.String({ description: "Story title" }),
|
|
112
|
+
slug: Type.Optional(Type.String({ description: "URL slug (autogenerated if omitted)" })),
|
|
113
|
+
folder_id: Type.Optional(Type.Number({ description: "Folder ID to place the story in" })),
|
|
114
|
+
parent_id: Type.Optional(Type.Number({ description: "Parent story ID for nested stories" })),
|
|
115
|
+
content: Type.Optional(Type.Record(Type.String(), Type.Any())),
|
|
116
|
+
tags: Type.Optional(Type.Array(Type.String())),
|
|
117
|
+
is_folder: Type.Optional(Type.Boolean({ description: "If true, creates a folder instead of a story" })),
|
|
118
|
+
language: Type.Optional(Type.String({ description: "Language code for multilingual spaces" })),
|
|
119
|
+
}),
|
|
120
|
+
async execute(_id: string, params: {
|
|
121
|
+
title: string;
|
|
122
|
+
slug?: string;
|
|
123
|
+
folder_id?: number;
|
|
124
|
+
parent_id?: number;
|
|
125
|
+
content?: Record<string, any>;
|
|
126
|
+
tags?: string[];
|
|
127
|
+
is_folder?: boolean;
|
|
128
|
+
language?: string;
|
|
129
|
+
}) {
|
|
130
|
+
try {
|
|
131
|
+
const result = await client.createStory(params);
|
|
132
|
+
return ok(result);
|
|
133
|
+
} catch (e: any) {
|
|
134
|
+
return fail(redactTokens(e.message, cfg));
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// ── storyblok_update_story ───────────────────────────────────────────────────
|
|
140
|
+
api.registerTool({
|
|
141
|
+
name: "storyblok_update_story",
|
|
142
|
+
description: "Update a story (draft or published). Use with caution on published stories.",
|
|
143
|
+
parameters: Type.Object({
|
|
144
|
+
story_id: Type.String({ description: "Story ID or UUID" }),
|
|
145
|
+
title: Type.Optional(Type.String({ description: "New title" })),
|
|
146
|
+
slug: Type.Optional(Type.String({ description: "New slug" })),
|
|
147
|
+
content: Type.Optional(Type.Record(Type.String(), Type.Any())),
|
|
148
|
+
parent_id: Type.Optional(Type.Number({ description: "New parent ID" })),
|
|
149
|
+
tags: Type.Optional(Type.Array(Type.String())),
|
|
150
|
+
language: Type.Optional(Type.String({ description: "Language code" })),
|
|
151
|
+
version: Type.Optional(Type.String({ description: "Which version to update: 'draft' (default) or 'published'" })),
|
|
152
|
+
}),
|
|
153
|
+
async execute(_id: string, params: {
|
|
154
|
+
story_id: string;
|
|
155
|
+
title?: string;
|
|
156
|
+
slug?: string;
|
|
157
|
+
content?: Record<string, any>;
|
|
158
|
+
parent_id?: number;
|
|
159
|
+
tags?: string[];
|
|
160
|
+
language?: string;
|
|
161
|
+
version?: string;
|
|
162
|
+
}) {
|
|
163
|
+
try {
|
|
164
|
+
const result = await client.updateStory(params.story_id, params);
|
|
165
|
+
return ok(result);
|
|
166
|
+
} catch (e: any) {
|
|
167
|
+
return fail(redactTokens(e.message, cfg));
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// ── storyblok_publish_story ──────────────────────────────────────────────────
|
|
173
|
+
api.registerTool({
|
|
174
|
+
name: "storyblok_publish_story",
|
|
175
|
+
description: "Publish a story (from draft). Optionally set a version note.",
|
|
176
|
+
parameters: Type.Object({
|
|
177
|
+
story_id: Type.String({ description: "Story ID or UUID" }),
|
|
178
|
+
version: Type.Optional(Type.String({ description: "Version to publish: usually 'draft' to publish the latest draft" })),
|
|
179
|
+
language: Type.Optional(Type.String({ description: "Language code" })),
|
|
180
|
+
publish_notes: Type.Optional(Type.String({ description: "Version note / changelog" })),
|
|
181
|
+
}),
|
|
182
|
+
async execute(_id: string, params: { story_id: string; version?: string; language?: string; publish_notes?: string }) {
|
|
183
|
+
try {
|
|
184
|
+
const result = await client.publishStory(params.story_id, { version: params.version, language: params.language, publish_notes: params.publish_notes });
|
|
185
|
+
return ok(result);
|
|
186
|
+
} catch (e: any) {
|
|
187
|
+
return fail(redactTokens(e.message, cfg));
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// ── storyblok_unpublish_story ───────────────────────────────────────────────
|
|
193
|
+
api.registerTool({
|
|
194
|
+
name: "storyblok_unpublish_story",
|
|
195
|
+
description: "Unpublish a story (makes it draft-only).",
|
|
196
|
+
parameters: Type.Object({
|
|
197
|
+
story_id: Type.String({ description: "Story ID or UUID" }),
|
|
198
|
+
language: Type.Optional(Type.String({ description: "Language code" })),
|
|
199
|
+
}),
|
|
200
|
+
async execute(_id: string, params: { story_id: string; language?: string }) {
|
|
201
|
+
try {
|
|
202
|
+
const result = await client.unpublishStory(params.story_id, params.language);
|
|
203
|
+
return ok(result);
|
|
204
|
+
} catch (e: any) {
|
|
205
|
+
return fail(redactTokens(e.message, cfg));
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// ── storyblok_get_components ─────────────────────────────────────────────────
|
|
211
|
+
api.registerTool({
|
|
212
|
+
name: "storyblok_get_components",
|
|
213
|
+
description: "Retrieve the list of component schemas (blok types) defined in the space.",
|
|
214
|
+
parameters: Type.Object({
|
|
215
|
+
version: Type.Optional(Type.String({ description: "Version: 'draft' (default) or 'published'" })),
|
|
216
|
+
language: Type.Optional(Type.String({ description: "Language code for localized component definitions" })),
|
|
217
|
+
}),
|
|
218
|
+
async execute(_id: string, params: { version?: string; language?: string }) {
|
|
219
|
+
try {
|
|
220
|
+
const result = await client.getComponents(params.version, params.language);
|
|
221
|
+
return ok(result);
|
|
222
|
+
} catch (e:any) {
|
|
223
|
+
return fail(redactTokens(e.message, cfg));
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
},
|
|
228
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"outDir": "dist",
|
|
11
|
+
"rootDir": ".",
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"declarationMap": true,
|
|
14
|
+
"sourceMap": true
|
|
15
|
+
},
|
|
16
|
+
"include": ["index.ts", "src/**/*.ts"],
|
|
17
|
+
"exclude": ["node_modules", "dist"]
|
|
18
|
+
}
|