@bendyline/gilde 0.1.21 → 0.1.22
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/authoring/chat-models/nemotron3.5-lightning-30b-q4.json +3 -2
- package/authoring/chat-models/nemotron3.5-lightning-30b-q8.json +3 -2
- package/authoring/page-demo-stub.js +85 -0
- package/data/chat-models/index.json +1 -1
- package/data/chat-models/ne/nemotron3.5-lightning-30b-q4/manifest.json +3 -2
- package/data/chat-models/ne/nemotron3.5-lightning-30b-q8/manifest.json +3 -2
- package/data/connector-types/bl/bluesky-posts/manifest.json +16 -0
- package/data/connector-types/bl/bluesky-posts/versions/1.0.0/manifest.json +70 -0
- package/data/connector-types/gi/github-issues/versions/1.0.1/manifest.json +67 -0
- package/data/connector-types/in/instagram-media/manifest.json +17 -0
- package/data/connector-types/in/instagram-media/versions/1.0.0/manifest.json +70 -0
- package/data/connector-types/index.json +1 -1
- package/data/connector-types/x-/x-posts/manifest.json +17 -0
- package/data/connector-types/x-/x-posts/versions/1.0.0/manifest.json +66 -0
- package/data/craftbook-templates/dr/draft-social-post/manifest.json +23 -0
- package/data/craftbook-templates/dr/draft-social-post/versions/1.0.0/craftbook.json +176 -0
- package/data/craftbook-templates/dr/draft-social-post/versions/1.0.0/test.json +161 -0
- package/data/craftbook-templates/index.json +1 -1
- package/data/craftbook-templates/re/reception-report/manifest.json +21 -0
- package/data/craftbook-templates/re/reception-report/versions/1.0.0/craftbook.json +170 -0
- package/data/craftbook-templates/re/reception-report/versions/1.0.0/test.json +168 -0
- package/data/craftbook-templates/so/social-digest/manifest.json +21 -0
- package/data/craftbook-templates/so/social-digest/versions/1.0.0/craftbook.json +133 -0
- package/data/craftbook-templates/so/social-digest/versions/1.0.0/test.json +168 -0
- package/data/gezel-templates/index.json +1 -1
- package/data/gezel-templates/om/omroeper/manifest.json +20 -0
- package/data/gezel-templates/om/omroeper/versions/1.0.0/about.md +33 -0
- package/data/gezel-templates/om/omroeper/versions/1.0.0/manifest.json +23 -0
- package/data/project-types/im/image-feed/manifest.json +20 -0
- package/data/project-types/im/image-feed/versions/1.0.0/about.md +21 -0
- package/data/project-types/im/image-feed/versions/1.0.0/manifest.json +330 -0
- package/data/project-types/im/image-feed/versions/1.0.0/mission.md +10 -0
- package/data/project-types/im/image-feed/versions/1.0.0/pages/gallery/index.html +831 -0
- package/data/project-types/im/image-feed/versions/1.0.0/posts/index.json +4 -0
- package/data/project-types/im/image-feed/versions/1.0.0/style-guide.md +24 -0
- package/data/project-types/index.json +1 -1
- package/data/project-types/so/social-feed/manifest.json +20 -0
- package/data/project-types/so/social-feed/versions/1.0.0/about.md +19 -0
- package/data/project-types/so/social-feed/versions/1.0.0/manifest.json +330 -0
- package/data/project-types/so/social-feed/versions/1.0.0/mission.md +10 -0
- package/data/project-types/so/social-feed/versions/1.0.0/pages/dashboard/index.html +754 -0
- package/data/project-types/so/social-feed/versions/1.0.0/posts/index.json +4 -0
- package/data/project-types/so/social-feed/versions/1.0.0/voice-guide.md +22 -0
- package/package.json +1 -1
- package/schemas/connector-type-version.schema.json +6 -0
- package/schemas/craftbook-test.schema.json +1 -0
- package/schemas/project-type-version.schema.json +4 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"releasedAt": "2026-08-12T00:00:00Z",
|
|
5
|
+
"extends": "social-media",
|
|
6
|
+
"meesterManaged": true,
|
|
7
|
+
"mode": "crew",
|
|
8
|
+
"leadLabel": "Omroeper",
|
|
9
|
+
"params": {
|
|
10
|
+
"type": "object",
|
|
11
|
+
"properties": {
|
|
12
|
+
"brand": {
|
|
13
|
+
"type": "string",
|
|
14
|
+
"title": "Who does this feed speak for?",
|
|
15
|
+
"description": "The name, brand, or studio this feed speaks for.",
|
|
16
|
+
"default": "Our studio"
|
|
17
|
+
},
|
|
18
|
+
"instagramHandle": {
|
|
19
|
+
"type": "string",
|
|
20
|
+
"title": "Instagram handle",
|
|
21
|
+
"description": "Instagram handle (e.g. @studio). Leave empty if unused.",
|
|
22
|
+
"default": ""
|
|
23
|
+
},
|
|
24
|
+
"visualStyle": {
|
|
25
|
+
"type": "string",
|
|
26
|
+
"title": "Visual style",
|
|
27
|
+
"description": "One sentence on how the feed should look — seeded into style-guide.md.",
|
|
28
|
+
"default": "Natural light, honest color, one subject per image."
|
|
29
|
+
},
|
|
30
|
+
"voice": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"title": "Tone of voice",
|
|
33
|
+
"description": "One sentence on tone of voice for captions.",
|
|
34
|
+
"default": "Warm, plain, and human — never corporate."
|
|
35
|
+
},
|
|
36
|
+
"cadence": {
|
|
37
|
+
"type": "string",
|
|
38
|
+
"title": "Posting cadence",
|
|
39
|
+
"description": "Target posting cadence (e.g. 4 images a week).",
|
|
40
|
+
"default": "4 images a week"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"required": [
|
|
44
|
+
"brand"
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
"nameTemplate": "{{brand}} Image Feed",
|
|
48
|
+
"aboutTemplate": "about.md",
|
|
49
|
+
"missionTemplate": "mission.md",
|
|
50
|
+
"gezels": [
|
|
51
|
+
{
|
|
52
|
+
"templateId": "omroeper",
|
|
53
|
+
"voorman": true
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"templateId": "image-generator"
|
|
57
|
+
}
|
|
58
|
+
],
|
|
59
|
+
"scripts": {
|
|
60
|
+
"posts-store": "import { type InferredInput, defineScript, gezel } from '@bendyline/gezel-sdk';\n\nexport const meta = defineScript({\n name: 'posts-store',\n description:\n \"Read and update the post pipeline under posts/. Call action:'list' to see every post (optional status/platform filters); 'get' for one post's frontmatter, body, variants, and media; 'create' to commit the staged posts/_drafting/ folder as a real post; 'update' to patch frontmatter or move a post one pipeline stage (idea, draft, in-review, approved, scheduled, published, archived from anywhere); 'record_publication' to stamp a platform variant with its permalink after it goes live. Illegal stage jumps fail with the legal next stages — move one stage at a time.\",\n inputs: {\n action: {\n type: 'string',\n description: \"One of 'list', 'get', 'create', 'update', or 'record_publication'.\",\n required: true,\n },\n id: { type: 'string', description: \"Post id — its folder name under posts/, e.g. '2026-08-12-launch-teaser'.\" },\n status: {\n type: 'string',\n description: \"Target status: idea, draft, in-review, approved, scheduled, published, or archived. For 'create', the initial status (defaults to approved).\",\n },\n platform: { type: 'string', description: \"Platform filter for 'list', or the published variant for 'record_publication' (e.g. 'bluesky').\" },\n fields: {\n type: 'json',\n description: \"For 'update': an object of frontmatter patches, e.g. {\\\"scheduledFor\\\": \\\"2026-08-15T09:00:00Z\\\", \\\"campaign\\\": \\\"autumn\\\"}. A null value clears the key.\",\n },\n permalink: { type: 'string', description: 'Public URL of the live post, for record_publication.' },\n postId: { type: 'string', description: \"The platform's own id for the live post, for record_publication.\" },\n corpusRef: { type: 'string', description: 'Connector corpus record reference for the live post, when one exists.' },\n publishedAt: { type: 'string', description: 'ISO timestamp the post went live (defaults to now).' },\n markPublished: {\n type: 'boolean',\n description: 'For record_publication: force the post to published even if other target platforms are not stamped yet.',\n },\n },\n outputs: {\n posts: { type: 'json', description: \"For 'list': the post listing entries.\" },\n total: { type: 'number', description: \"For 'list': how many posts exist before filtering.\" },\n post: { type: 'json', description: \"For 'get': frontmatter plus body, variants map, and media names.\" },\n id: { type: 'string', description: 'The post id the action touched.' },\n status: { type: 'string', description: \"The post's status after the action.\" },\n summary: { type: 'string', description: 'Human-readable result.' },\n },\n requires: ['workspace.read', 'workspace.write'],\n} as const);\n\nconst input = gezel.input as InferredInput<typeof meta>;\n\nconst POSTS_DIR = 'posts';\nconst DRAFTING = 'posts/_drafting';\nconst INDEX_FILE = 'posts/index.json';\nconst ID_RE = /^[a-z0-9][a-z0-9-]{0,99}$/;\nconst PLATFORM_RE = /^[a-z0-9][a-z0-9-]{0,31}$/;\nconst STAGES = ['idea', 'draft', 'in-review', 'approved', 'scheduled', 'published'] as const;\nconst STATUSES: string[] = [...STAGES, 'archived'];\n\n// ── frontmatter ──\n// Hand-rolled, deliberately simple: `---`-delimited `key: value` lines,\n// arrays as [a, b]. Unknown keys and even unparseable lines are preserved\n// in order so a round-trip never loses what a human or another tool wrote.\n\ntype FmEntry = { key: string; value: string | string[] } | { raw: string };\ninterface FmDoc {\n entries: FmEntry[];\n body: string;\n}\n\nfunction parseArrayish(value: string): string[] {\n return value\n .slice(1, -1)\n .split(',')\n .map((part) => part.trim().replace(/^[\"']|[\"']$/g, ''))\n .filter((part) => part.length > 0);\n}\n\nfunction parseFrontmatter(text: string): FmDoc {\n const lines = text.split(/\\r?\\n/);\n if (lines[0]?.trim() !== '---') return { entries: [], body: text };\n const entries: FmEntry[] = [];\n let at = 1;\n for (; at < lines.length; at += 1) {\n const line = lines[at]!;\n if (line.trim() === '---') {\n return { entries, body: lines.slice(at + 1).join('\\n').replace(/^\\n/, '') };\n }\n const sep = line.indexOf(':');\n if (sep <= 0) {\n if (line.trim().length > 0) entries.push({ raw: line });\n continue;\n }\n const key = line.slice(0, sep).trim();\n const value = line.slice(sep + 1).trim();\n if (!key) {\n entries.push({ raw: line });\n continue;\n }\n entries.push({\n key,\n value: value.startsWith('[') && value.endsWith(']') ? parseArrayish(value) : value,\n });\n }\n return { entries: [], body: text };\n}\n\nfunction serializeFrontmatter(doc: FmDoc): string {\n const lines = doc.entries.map((entry) => {\n if ('raw' in entry) return entry.raw;\n const value = Array.isArray(entry.value) ? `[${entry.value.join(', ')}]` : entry.value;\n return `${entry.key}: ${value}`;\n });\n return `---\\n${lines.join('\\n')}\\n---\\n\\n${doc.body.replace(/^\\n+/, '')}`;\n}\n\nfunction fmGet(doc: FmDoc, key: string): string | string[] | undefined {\n for (const entry of doc.entries) {\n if ('key' in entry && entry.key === key) return entry.value;\n }\n return undefined;\n}\n\nfunction fmGetString(doc: FmDoc, key: string): string {\n const value = fmGet(doc, key);\n if (Array.isArray(value)) return value.join(', ');\n return value ?? '';\n}\n\nfunction fmGetArray(doc: FmDoc, key: string): string[] {\n const value = fmGet(doc, key);\n if (Array.isArray(value)) return value;\n if (typeof value === 'string' && value.length > 0) return [value];\n return [];\n}\n\nfunction fmSet(doc: FmDoc, key: string, value: string | string[]): void {\n for (const entry of doc.entries) {\n if ('key' in entry && entry.key === key) {\n entry.value = value;\n return;\n }\n }\n doc.entries.push({ key, value });\n}\n\nfunction fmDelete(doc: FmDoc, key: string): void {\n doc.entries = doc.entries.filter((entry) => !('key' in entry && entry.key === key));\n}\n\n// ── pipeline rules ──\n\nfunction legalNext(from: string): string[] {\n if (from === 'archived') return [];\n const at = STAGES.indexOf(from as (typeof STAGES)[number]);\n const forward = at >= 0 && at < STAGES.length - 1 ? [STAGES[at + 1]!] : [];\n return [...forward, 'archived'];\n}\n\nfunction assertTransition(id: string, from: string, to: string): void {\n if (to === from) return;\n if (!STATUSES.includes(to)) {\n throw new Error(`Unknown status '${to}'. Statuses: ${STATUSES.join(', ')}.`);\n }\n if (!legalNext(from).includes(to)) {\n const next = legalNext(from);\n throw new Error(\n `Cannot move '${id}' from ${from} to ${to} — posts move one stage at a time. ` +\n (next.length > 0 ? `Legal next: ${next.join(', ')}.` : `'${from}' is final; create a new post instead.`),\n );\n }\n}\n\nfunction requireId(): string {\n const id = (input.id ?? '').trim();\n if (!id) throw new Error(\"Pass 'id' — the post's folder name under posts/ (see action:'list').\");\n if (!ID_RE.test(id)) {\n throw new Error(`Invalid post id '${id}'. Ids are lowercase [a-z0-9-], no slashes or dots.`);\n }\n return id;\n}\n\nfunction today(): string {\n return new Date().toISOString().slice(0, 10);\n}\n\nfunction slugify(text: string): string {\n return text\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 60);\n}\n\n// ── store ──\n\ninterface IndexEntry {\n id: string;\n title: string;\n status: string;\n platforms: string[];\n created: string;\n scheduledFor?: string;\n campaign?: string;\n}\n\nasync function readPostDoc(id: string): Promise<FmDoc> {\n let raw: string;\n try {\n raw = await gezel.fs.read(`${POSTS_DIR}/${id}/post.md`);\n } catch {\n throw new Error(`No post '${id}' — posts/${id}/post.md does not exist. Call action:'list' to see what does.`);\n }\n return parseFrontmatter(raw);\n}\n\nasync function writePostDoc(id: string, doc: FmDoc): Promise<void> {\n await gezel.fs.write(`${POSTS_DIR}/${id}/post.md`, serializeFrontmatter(doc));\n}\n\nfunction toIndexEntry(id: string, doc: FmDoc): IndexEntry {\n const entry: IndexEntry = {\n id,\n title: fmGetString(doc, 'title') || id,\n status: fmGetString(doc, 'status') || 'idea',\n platforms: fmGetArray(doc, 'platforms'),\n created: fmGetString(doc, 'created') || today(),\n };\n const scheduledFor = fmGetString(doc, 'scheduledFor');\n if (scheduledFor) entry.scheduledFor = scheduledFor;\n const campaign = fmGetString(doc, 'campaign');\n if (campaign) entry.campaign = campaign;\n return entry;\n}\n\nasync function regenerateIndex(): Promise<IndexEntry[]> {\n let names: string[] = [];\n try {\n names = (await gezel.fs.list(POSTS_DIR))\n .filter((entry) => entry.isDirectory && entry.name !== '_drafting' && ID_RE.test(entry.name))\n .map((entry) => entry.name);\n } catch {\n names = [];\n }\n const posts: IndexEntry[] = [];\n for (const name of names) {\n try {\n posts.push(toIndexEntry(name, await readPostDoc(name)));\n } catch {\n // A folder without a parseable post.md is not a post; leave it be.\n }\n }\n posts.sort((a, b) => (a.created === b.created ? (a.id < b.id ? 1 : -1) : a.created < b.created ? 1 : -1));\n await gezel.fs.write(INDEX_FILE, `${JSON.stringify({ version: 1, posts }, null, 2)}\\n`);\n return posts;\n}\n\nasync function readIndex(): Promise<IndexEntry[]> {\n try {\n const parsed = JSON.parse(await gezel.fs.read(INDEX_FILE)) as { posts?: IndexEntry[] };\n return Array.isArray(parsed.posts) ? parsed.posts : [];\n } catch {\n return [];\n }\n}\n\nasync function listVariants(id: string): Promise<Record<string, Record<string, unknown>>> {\n let names: string[] = [];\n try {\n names = (await gezel.fs.list(`${POSTS_DIR}/${id}/variants`))\n .filter((entry) => !entry.isDirectory && entry.name.endsWith('.md'))\n .map((entry) => entry.name);\n } catch {\n return {};\n }\n const variants: Record<string, Record<string, unknown>> = {};\n for (const name of names) {\n const platform = name.slice(0, -3);\n const doc = parseFrontmatter(await gezel.fs.read(`${POSTS_DIR}/${id}/variants/${name}`));\n const body = doc.body.trim();\n const variant: Record<string, unknown> = {\n charCount: Number(fmGetString(doc, 'charCount')) || body.length,\n body,\n };\n for (const key of ['publishedAt', 'permalink', 'postId', 'corpusRef']) {\n const value = fmGetString(doc, key);\n if (value) variant[key] = value;\n }\n variants[platform] = variant;\n }\n return variants;\n}\n\nasync function listMedia(id: string): Promise<string[]> {\n try {\n return (await gezel.fs.list(`${POSTS_DIR}/${id}/media`))\n .filter((entry) => !entry.isDirectory)\n .map((entry) => entry.name);\n } catch {\n return [];\n }\n}\n\n// ── actions ──\n\nif (input.action === 'list') {\n const all = await readIndex();\n let posts = all;\n if (input.status) posts = posts.filter((post) => post.status === input.status);\n if (input.platform) posts = posts.filter((post) => post.platforms.includes(input.platform!));\n const counts = STATUSES.map((status) => {\n const n = all.filter((post) => post.status === status).length;\n return n > 0 ? `${status} ${n}` : null;\n })\n .filter((line) => line !== null)\n .join(', ');\n gezel.output({\n posts,\n total: all.length,\n summary: all.length === 0 ? 'No posts yet — stage one in posts/_drafting/ and call create.' : `${all.length} post(s): ${counts}.`,\n });\n} else if (input.action === 'get') {\n const id = requireId();\n const doc = await readPostDoc(id);\n const fields: Record<string, unknown> = {};\n for (const entry of doc.entries) {\n if ('key' in entry) fields[entry.key] = entry.value;\n }\n const post = {\n id,\n ...fields,\n status: fmGetString(doc, 'status') || 'idea',\n platforms: fmGetArray(doc, 'platforms'),\n body: doc.body.trim(),\n variants: await listVariants(id),\n media: await listMedia(id),\n };\n gezel.output({\n post,\n id,\n status: post.status,\n summary: `'${fmGetString(doc, 'title') || id}' is ${post.status}; ${Object.keys(post.variants).length} variant(s), ${post.media.length} media file(s).`,\n });\n} else if (input.action === 'create') {\n let staged: FmDoc;\n try {\n staged = parseFrontmatter(await gezel.fs.read(`${DRAFTING}/post.md`));\n } catch {\n throw new Error('Nothing is staged — write posts/_drafting/post.md (frontmatter + body) first, then call create again.');\n }\n const slug = slugify(fmGetString(staged, 'slug') || fmGetString(staged, 'title'));\n if (!slug) throw new Error(\"The staged post.md needs a 'slug' or a 'title' in its frontmatter to name the post folder.\");\n const createdRaw = fmGetString(staged, 'created');\n const created = /^\\d{4}-\\d{2}-\\d{2}$/.test(createdRaw) ? createdRaw : today();\n const id = `${created}-${slug}`;\n if (!ID_RE.test(id)) throw new Error(`Derived post id '${id}' is invalid — keep slug to lowercase letters, digits, and dashes.`);\n const siblings = await gezel.fs.list(POSTS_DIR).catch(() => []);\n if (siblings.some((entry) => entry.name === id)) {\n throw new Error(`posts/${id}/ already exists. Change the staged slug (or created date) and call create again.`);\n }\n const status = input.status ?? 'approved';\n if (!STATUSES.includes(status)) {\n throw new Error(`'${status}' is not a status. Statuses: ${STATUSES.join(', ')}.`);\n }\n fmSet(staged, 'status', status);\n fmSet(staged, 'slug', slug);\n fmSet(staged, 'created', created);\n await gezel.fs.write(`${DRAFTING}/post.md`, serializeFrontmatter(staged));\n await gezel.fs.rename(DRAFTING, `${POSTS_DIR}/${id}`);\n await regenerateIndex();\n gezel.output({\n id,\n status,\n summary: `Committed posts/_drafting/ as posts/${id}/ with status ${status}.`,\n });\n} else if (input.action === 'update') {\n const id = requireId();\n const doc = await readPostDoc(id);\n const patches = (input.fields && typeof input.fields === 'object' && !Array.isArray(input.fields) ? input.fields : {}) as Record<string, unknown>;\n let nextStatus = input.status;\n if (typeof patches.status === 'string' && !nextStatus) nextStatus = patches.status;\n delete patches.status;\n if (!nextStatus && Object.keys(patches).length === 0) {\n throw new Error(\"Nothing to change — pass 'status' and/or a 'fields' object of frontmatter patches.\");\n }\n const from = fmGetString(doc, 'status') || 'idea';\n if (nextStatus) assertTransition(id, from, nextStatus);\n for (const [key, value] of Object.entries(patches)) {\n if (value === null || value === undefined) fmDelete(doc, key);\n else if (Array.isArray(value)) fmSet(doc, key, value.map((item) => String(item)));\n else fmSet(doc, key, String(value));\n }\n if (nextStatus) fmSet(doc, 'status', nextStatus);\n await writePostDoc(id, doc);\n await regenerateIndex();\n const status = nextStatus ?? from;\n gezel.output({\n id,\n status,\n summary: nextStatus && nextStatus !== from ? `Moved '${id}' from ${from} to ${status}.` : `Updated '${id}' (${status}).`,\n });\n} else if (input.action === 'record_publication') {\n const id = requireId();\n const platform = (input.platform ?? '').trim();\n if (!PLATFORM_RE.test(platform)) {\n throw new Error(\"Pass 'platform' — the variant that went live, e.g. 'bluesky', 'x', or 'instagram'.\");\n }\n const doc = await readPostDoc(id);\n const variantPath = `${POSTS_DIR}/${id}/variants/${platform}.md`;\n const variant = parseFrontmatter(await gezel.fs.read(variantPath).catch(() => '---\\n---\\n'));\n fmSet(variant, 'publishedAt', input.publishedAt ?? new Date().toISOString());\n if (input.permalink) fmSet(variant, 'permalink', input.permalink);\n if (input.postId) fmSet(variant, 'postId', input.postId);\n if (input.corpusRef) fmSet(variant, 'corpusRef', input.corpusRef);\n await gezel.fs.write(variantPath, serializeFrontmatter(variant));\n const targets = fmGetArray(doc, 'platforms');\n const stamped = await listVariants(id);\n const allLive = targets.length > 0 && targets.every((target) => typeof stamped[target]?.publishedAt === 'string');\n let status = fmGetString(doc, 'status') || 'idea';\n // Recording reality bypasses the stage chain: once every target variant\n // is live (or the caller says so), the post IS published.\n if (allLive || input.markPublished === true) {\n status = 'published';\n fmSet(doc, 'status', status);\n await writePostDoc(id, doc);\n }\n await regenerateIndex();\n const waiting = targets.filter((target) => typeof stamped[target]?.publishedAt !== 'string');\n gezel.output({\n id,\n status,\n summary:\n status === 'published'\n ? `Recorded ${platform} publication for '${id}' — the post is published.`\n : `Recorded ${platform} publication for '${id}'; still waiting on: ${waiting.join(', ') || '(none)'}.`,\n });\n} else {\n throw new Error(`Unknown action '${input.action}'. Use list, get, create, update, or record_publication.`);\n}\n"
|
|
61
|
+
},
|
|
62
|
+
"tools": [
|
|
63
|
+
{
|
|
64
|
+
"name": "list_posts",
|
|
65
|
+
"description": "See the whole post pipeline: every post with its status, platforms, and dates. Optionally filter by status or platform.",
|
|
66
|
+
"script": "posts-store",
|
|
67
|
+
"inputs": {
|
|
68
|
+
"type": "object",
|
|
69
|
+
"properties": {
|
|
70
|
+
"status": {
|
|
71
|
+
"type": "string",
|
|
72
|
+
"enum": [
|
|
73
|
+
"idea",
|
|
74
|
+
"draft",
|
|
75
|
+
"in-review",
|
|
76
|
+
"approved",
|
|
77
|
+
"scheduled",
|
|
78
|
+
"published",
|
|
79
|
+
"archived"
|
|
80
|
+
],
|
|
81
|
+
"description": "Only posts currently in this status."
|
|
82
|
+
},
|
|
83
|
+
"platform": {
|
|
84
|
+
"type": "string",
|
|
85
|
+
"description": "Only posts targeting this platform, e.g. 'bluesky'."
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
"bind": {
|
|
90
|
+
"action": "list"
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"name": "get_post",
|
|
95
|
+
"description": "Read one post in full: frontmatter, body, each platform variant with its character count (and permalink once live), and media files.",
|
|
96
|
+
"script": "posts-store",
|
|
97
|
+
"inputs": {
|
|
98
|
+
"type": "object",
|
|
99
|
+
"properties": {
|
|
100
|
+
"id": {
|
|
101
|
+
"type": "string",
|
|
102
|
+
"description": "Post id — its folder name under posts/, e.g. '2026-08-12-launch-teaser'."
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
"required": [
|
|
106
|
+
"id"
|
|
107
|
+
]
|
|
108
|
+
},
|
|
109
|
+
"bind": {
|
|
110
|
+
"action": "get"
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"name": "create_post",
|
|
115
|
+
"description": "Commit the staged posts/_drafting/ folder as a real post. Stage post.md (frontmatter + body), variants/, and media/ there first; the post folder is named from the created date and slug.",
|
|
116
|
+
"script": "posts-store",
|
|
117
|
+
"inputs": {
|
|
118
|
+
"type": "object",
|
|
119
|
+
"properties": {
|
|
120
|
+
"status": {
|
|
121
|
+
"type": "string",
|
|
122
|
+
"enum": [
|
|
123
|
+
"idea",
|
|
124
|
+
"draft",
|
|
125
|
+
"in-review",
|
|
126
|
+
"approved",
|
|
127
|
+
"scheduled",
|
|
128
|
+
"published",
|
|
129
|
+
"archived"
|
|
130
|
+
],
|
|
131
|
+
"description": "Initial status for the new post. Defaults to approved."
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
"bind": {
|
|
136
|
+
"action": "create"
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
"name": "update_post",
|
|
141
|
+
"description": "Patch a post's frontmatter (fields object; null clears a key) and/or move it one pipeline stage. Stages run idea, draft, in-review, approved, scheduled, published — archived from anywhere.",
|
|
142
|
+
"script": "posts-store",
|
|
143
|
+
"inputs": {
|
|
144
|
+
"type": "object",
|
|
145
|
+
"properties": {
|
|
146
|
+
"id": {
|
|
147
|
+
"type": "string",
|
|
148
|
+
"description": "Post id."
|
|
149
|
+
},
|
|
150
|
+
"fields": {
|
|
151
|
+
"type": "object",
|
|
152
|
+
"description": "Frontmatter patches, e.g. {\"scheduledFor\": \"2026-08-15T09:00:00Z\", \"campaign\": \"autumn\"}."
|
|
153
|
+
},
|
|
154
|
+
"status": {
|
|
155
|
+
"type": "string",
|
|
156
|
+
"enum": [
|
|
157
|
+
"idea",
|
|
158
|
+
"draft",
|
|
159
|
+
"in-review",
|
|
160
|
+
"approved",
|
|
161
|
+
"scheduled",
|
|
162
|
+
"published",
|
|
163
|
+
"archived"
|
|
164
|
+
],
|
|
165
|
+
"description": "Target status (one stage at a time)."
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
"required": [
|
|
169
|
+
"id"
|
|
170
|
+
]
|
|
171
|
+
},
|
|
172
|
+
"bind": {
|
|
173
|
+
"action": "update"
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
"name": "record_publication",
|
|
178
|
+
"description": "The moment a variant goes live, stamp it with its permalink and platform post id. The post flips to published once every target platform is stamped (or markPublished is passed).",
|
|
179
|
+
"script": "posts-store",
|
|
180
|
+
"inputs": {
|
|
181
|
+
"type": "object",
|
|
182
|
+
"properties": {
|
|
183
|
+
"id": {
|
|
184
|
+
"type": "string",
|
|
185
|
+
"description": "Post id."
|
|
186
|
+
},
|
|
187
|
+
"platform": {
|
|
188
|
+
"type": "string",
|
|
189
|
+
"description": "The platform that went live, e.g. 'bluesky'."
|
|
190
|
+
},
|
|
191
|
+
"permalink": {
|
|
192
|
+
"type": "string",
|
|
193
|
+
"description": "Public URL of the live post."
|
|
194
|
+
},
|
|
195
|
+
"postId": {
|
|
196
|
+
"type": "string",
|
|
197
|
+
"description": "The platform's own id for the live post."
|
|
198
|
+
},
|
|
199
|
+
"corpusRef": {
|
|
200
|
+
"type": "string",
|
|
201
|
+
"description": "Connector corpus record reference, when one exists."
|
|
202
|
+
},
|
|
203
|
+
"publishedAt": {
|
|
204
|
+
"type": "string",
|
|
205
|
+
"description": "ISO timestamp it went live (defaults to now)."
|
|
206
|
+
},
|
|
207
|
+
"markPublished": {
|
|
208
|
+
"type": "boolean",
|
|
209
|
+
"description": "Force the post to published even with unstamped platforms."
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
"required": [
|
|
213
|
+
"id",
|
|
214
|
+
"platform"
|
|
215
|
+
]
|
|
216
|
+
},
|
|
217
|
+
"bind": {
|
|
218
|
+
"action": "record_publication"
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
"name": "page_set_status",
|
|
223
|
+
"description": "Page-only: move a post between pipeline columns on the gallery.",
|
|
224
|
+
"script": "posts-store",
|
|
225
|
+
"inputs": {
|
|
226
|
+
"type": "object",
|
|
227
|
+
"properties": {
|
|
228
|
+
"id": {
|
|
229
|
+
"type": "string",
|
|
230
|
+
"description": "Post id."
|
|
231
|
+
},
|
|
232
|
+
"status": {
|
|
233
|
+
"type": "string",
|
|
234
|
+
"enum": [
|
|
235
|
+
"idea",
|
|
236
|
+
"draft",
|
|
237
|
+
"in-review",
|
|
238
|
+
"approved",
|
|
239
|
+
"scheduled",
|
|
240
|
+
"published",
|
|
241
|
+
"archived"
|
|
242
|
+
],
|
|
243
|
+
"description": "The column the post was moved to."
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
"required": [
|
|
247
|
+
"id",
|
|
248
|
+
"status"
|
|
249
|
+
]
|
|
250
|
+
},
|
|
251
|
+
"bind": {
|
|
252
|
+
"action": "update"
|
|
253
|
+
},
|
|
254
|
+
"reaction": {
|
|
255
|
+
"gezel": "omroeper",
|
|
256
|
+
"hideSeed": true,
|
|
257
|
+
"prompt": "[Feed gallery]: The user moved post {{output.id}} to '{{output.status}}' on the gallery. Reply with ONE short line acknowledging the move. If it landed in 'draft', offer to run the draft-social-post craftbook on it. Do not restate the pipeline."
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
"name": "page_get_post",
|
|
262
|
+
"description": "Page-only: the detail drawer's read of one post with variants and media.",
|
|
263
|
+
"script": "posts-store",
|
|
264
|
+
"inputs": {
|
|
265
|
+
"type": "object",
|
|
266
|
+
"properties": {
|
|
267
|
+
"id": {
|
|
268
|
+
"type": "string",
|
|
269
|
+
"description": "Post id."
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
"required": [
|
|
273
|
+
"id"
|
|
274
|
+
]
|
|
275
|
+
},
|
|
276
|
+
"bind": {
|
|
277
|
+
"action": "get"
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
],
|
|
281
|
+
"craftbooks": [
|
|
282
|
+
"draft-social-post",
|
|
283
|
+
"social-digest",
|
|
284
|
+
"reception-report"
|
|
285
|
+
],
|
|
286
|
+
"schedules": [
|
|
287
|
+
{
|
|
288
|
+
"runMode": "night-shift",
|
|
289
|
+
"craftbook": "social-digest",
|
|
290
|
+
"consent": "ask",
|
|
291
|
+
"overlap": "skip"
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
"runMode": "scheduled",
|
|
295
|
+
"cron": "0 15 * * 1",
|
|
296
|
+
"craftbook": "reception-report",
|
|
297
|
+
"consent": "ask",
|
|
298
|
+
"overlap": "skip"
|
|
299
|
+
}
|
|
300
|
+
],
|
|
301
|
+
"pages": {
|
|
302
|
+
"entry": "gallery/index.html",
|
|
303
|
+
"api": 1,
|
|
304
|
+
"reads": [
|
|
305
|
+
{
|
|
306
|
+
"source": "workspace",
|
|
307
|
+
"path": "posts",
|
|
308
|
+
"subtree": true
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
"source": "workspace",
|
|
312
|
+
"path": "style-guide.md"
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
"source": "artifacts",
|
|
316
|
+
"path": "data",
|
|
317
|
+
"subtree": true
|
|
318
|
+
}
|
|
319
|
+
],
|
|
320
|
+
"tools": [
|
|
321
|
+
"page_set_status",
|
|
322
|
+
"page_get_post"
|
|
323
|
+
]
|
|
324
|
+
},
|
|
325
|
+
"workspaceSeed": [
|
|
326
|
+
"style-guide.md",
|
|
327
|
+
"posts/index.json"
|
|
328
|
+
],
|
|
329
|
+
"notes": "posts/index.json is seeded at its subpath (applyProjectType preserves seed subpaths and creates parents) and is ALSO regenerated by posts-store on every mutation, so the gallery tolerates a missing or stale index. Posts live one folder per post under posts/ (post.md + variants/ + media/), staged in posts/_drafting/ until create_post commits them. page_set_status and page_get_post are the page-only twins of update_post/get_post — pages.tools names are removed from the model surface, so the model keeps its own read tools."
|
|
330
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
- {{brand}} keeps a steady cadence of {{cadence}} — the rhythm is the promise.
|
|
2
|
+
- Every post walks the pipeline: idea → draft → in-review → approved →
|
|
3
|
+
scheduled → published. Statuses move when reality moves, one stage at a
|
|
4
|
+
time, and archived is always an honest exit.
|
|
5
|
+
- No image publishes off-style: everything follows `style-guide.md`, and
|
|
6
|
+
every caption reads like {{brand}}, not like a caption generator.
|
|
7
|
+
- Each post's images live in its `media/` folder before it leaves review —
|
|
8
|
+
a post with no picture is not ready.
|
|
9
|
+
- Published posts carry their permalinks — record_publication the moment a
|
|
10
|
+
post goes live, so the gallery never lies.
|