@easysociable/cli 0.1.28 → 0.1.30

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easysociable/cli",
3
- "version": "0.1.28",
3
+ "version": "0.1.30",
4
4
  "mcpName": "io.github.murphywuwu/easysociable-mcp",
5
5
  "description": "Agent-first CLI for EasySociable: Content Engine, Template, Slideshow, and MCP",
6
6
  "type": "module",
@@ -10,7 +10,7 @@ export const PAGE_ROLES = [
10
10
 
11
11
  export const PACK_PLATFORMS = ['tiktok', 'xiaohongshu', 'instagram', 'linkedin', 'threads']
12
12
 
13
- export const SLIDESHOW_PLATFORMS = ['tiktok', 'instagram', 'linkedin', 'threads']
13
+ export const SLIDESHOW_PLATFORMS = PACK_PLATFORMS
14
14
 
15
15
  export const PLATFORM_CAPS = {
16
16
  tiktok: { hardMin: 4, hardMax: 35 },
@@ -22,8 +22,8 @@ export const PLATFORM_CAPS = {
22
22
 
23
23
  const FORBIDDEN_PACK_KEYS = ['deck_template_id', 'deckTemplateId']
24
24
 
25
- const PACK_KEYS = new Set(['schemaVersion', 'platform', 'title', 'pages'])
26
- const PAGE_KEYS = new Set(['layoutFamilyId', 'pageTemplateId', 'role', 'slots'])
25
+ const PACK_KEYS = new Set(['schemaVersion', 'platform', 'title', 'contentSlug', 'pages'])
26
+ const PAGE_KEYS = new Set(['layoutFamilyId', 'pageTemplateId', 'layout', 'role', 'slots'])
27
27
  export const RUN_FILE_STATUSES = [
28
28
  'RUN_OPEN', 'BRIEF_WAIT', 'DRAFTED', 'EDIT_WAIT', 'SCORED',
29
29
  'READY_TO_POST', 'PUBLISHED', 'HOLD', 'KILLED', 'SUPERSEDED',
@@ -98,8 +98,14 @@ export function validateContentPack(pack, options = {}) {
98
98
  pushUnknownKeys(errors, page, PAGE_KEYS, label)
99
99
  if (!PAGE_ROLES.includes(page.role)) errors.push(`${label}.role: invalid page role`)
100
100
  const layoutRef = page.layoutFamilyId ?? page.pageTemplateId
101
- if (typeof layoutRef !== 'string' || layoutRef.length === 0) {
102
- errors.push(`${label}.layoutFamilyId: required (or pageTemplateId) and must be a non-empty string`)
101
+ if (page.layout !== undefined) {
102
+ // Agent-generated page: its own Studio source on the pack platform's canvas.
103
+ const { studioJson, canvas } = isRecord(page.layout) ? page.layout : {}
104
+ if (typeof studioJson !== 'string' || !studioJson || !isRecord(canvas) || !Number.isInteger(canvas.width) || !Number.isInteger(canvas.height))
105
+ errors.push(`${label}.layout: needs studioJson and canvas { width, height }`)
106
+ }
107
+ else if (typeof layoutRef !== 'string' || layoutRef.length === 0) {
108
+ errors.push(`${label}.layoutFamilyId: required (or pageTemplateId, or a generated layout) and must be a non-empty string`)
103
109
  }
104
110
  if (!isRecord(page.slots)) {
105
111
  errors.push(`${label}.slots: must be an object`)
@@ -137,47 +143,59 @@ export function ideaField(text, name) {
137
143
  return match ? match[1].trim() : ''
138
144
  }
139
145
 
146
+ /**
147
+ * A Run folder in either layout:
148
+ * - current (Formula-led): content.md with run_id/platform/status frontmatter; the pack is pack.json,
149
+ * pack-<platform>.json, or the JSON block under content.md "## Pack".
150
+ * - legacy (idea → draft → pack): idea.md + draft.md + pack-<platform>.json.
151
+ */
140
152
  export function validateRunLayout(runDir, options = {}) {
141
153
  const { requirePack = false } = options
142
154
  const errors = []
143
155
  const warnings = []
144
- const files = { ideaMd: false, draftMd: false, packMd: false, packs: [] }
145
- const ideaPath = join(runDir, 'idea.md')
156
+ const files = { contentMd: false, ideaMd: false, draftMd: false, packMd: false, packs: [] }
146
157
 
147
158
  if (!existsSync(runDir) || !statSync(runDir).isDirectory()) {
148
159
  errors.push('run: directory required')
149
160
  return { valid: false, errors, warnings, files, frontmatter: {} }
150
161
  }
151
- if (!existsSync(ideaPath)) {
152
- errors.push('idea.md: required')
162
+ files.contentMd = existsSync(join(runDir, 'content.md'))
163
+ files.ideaMd = existsSync(join(runDir, 'idea.md'))
164
+ const main = files.contentMd ? 'content.md' : files.ideaMd ? 'idea.md' : ''
165
+ if (!main) {
166
+ errors.push('content.md: required (or idea.md for a legacy run)')
153
167
  return { valid: false, errors, warnings, files, frontmatter: {} }
154
168
  }
155
169
 
156
- files.ideaMd = true
157
- const ideaText = readFileSync(ideaPath, 'utf8')
158
- const frontmatter = parseFrontmatter(ideaText)
159
- const platform = (frontmatter.platform || ideaField(ideaText, 'primary_platform') || ideaField(ideaText, 'platforms')).replace(/\s.*/, '')
170
+ const mainText = readFileSync(join(runDir, main), 'utf8')
171
+ const frontmatter = parseFrontmatter(mainText)
172
+ const platform = (frontmatter.platform || ideaField(mainText, 'primary_platform') || ideaField(mainText, 'platforms')).replace(/\s.*/, '')
160
173
  frontmatter.platform = platform
161
174
  frontmatter.slug = frontmatter.slug || basename(runDir)
162
- if (!frontmatter.run_id) errors.push('idea.md frontmatter: missing run_id')
163
- if (!frontmatter.status) errors.push('idea.md frontmatter: missing status')
175
+ if (!frontmatter.run_id) errors.push(`${main} frontmatter: missing run_id`)
176
+ if (!frontmatter.status) errors.push(`${main} frontmatter: missing status`)
164
177
  if (frontmatter.status && !RUN_STATUSES.includes(frontmatter.status)) {
165
- errors.push(`idea.md status: must be one of the engine run statuses`)
178
+ errors.push(`${main} status: must be one of the engine run statuses`)
166
179
  }
167
- if (!platform) warnings.push('idea.md: primary_platform recommended')
180
+ if (!platform) warnings.push(`${main}: platform recommended`)
168
181
 
169
- files.draftMd = existsSync(join(runDir, 'draft.md'))
182
+ if (!files.contentMd) {
183
+ files.draftMd = existsSync(join(runDir, 'draft.md'))
184
+ if (!files.draftMd) warnings.push('draft.md: recommended')
185
+ }
170
186
  files.packMd = existsSync(join(runDir, 'pack.md')) || readdirSync(runDir).some((name) => /^pack-[a-z0-9_-]+\.md$/i.test(name))
171
- if (!files.draftMd) warnings.push('draft.md: recommended')
172
187
 
173
- files.packs = readdirSync(runDir).filter((name) => /^pack-[a-z0-9_-]+\.json$/i.test(name))
188
+ files.packs = readdirSync(runDir).filter((name) => /^pack(-[a-z0-9_-]+)?\.json$/i.test(name))
189
+ // content.md may carry the pack inline under "## Pack" instead of a file.
190
+ const inline = files.contentMd ? mainText.match(/^## Pack\b[\s\S]*?```json\s*\n([\s\S]*?)\n```/m) : null
191
+ if (inline) files.inlinePack = inline[1]
174
192
  const slideshowRun = SLIDESHOW_PLATFORMS.includes(platform) || platform === 'ig'
175
193
  const expectedPack = platform === 'ig' ? 'pack-instagram.json' : (platform ? `pack-${platform}.json` : 'pack-<platform>.json')
176
- if (requirePack && files.packs.length === 0) {
177
- errors.push(`${expectedPack}: required for slideshow handoff`)
194
+ if (requirePack && files.packs.length === 0 && !inline) {
195
+ errors.push(`${expectedPack} (or pack.json, or a json block under content.md ## Pack): required for slideshow handoff`)
178
196
  }
179
- if (!slideshowRun && files.packs.length > 0) {
180
- errors.push('pack-<platform>.json: only slideshow platforms may contain a Content Pack')
197
+ if (!slideshowRun && (files.packs.length > 0 || inline)) {
198
+ errors.push('pack: only slideshow platforms may contain a Content Pack')
181
199
  }
182
200
 
183
201
  return { valid: errors.length === 0, errors, warnings, files, frontmatter }
@@ -39,10 +39,13 @@ try {
39
39
  const runDir = resolve(options.run)
40
40
  const layout = validateRunLayout(runDir, { requirePack: options.requirePack })
41
41
  const packs = []
42
- for (const name of layout.files.packs) {
43
- const platform = name.replace(/^pack-/, '').replace(/\.json$/i, '')
42
+ const sources = layout.files.packs.map(name => ({ name, read: () => readFileSync(join(runDir, name), 'utf8') }))
43
+ if (layout.files.inlinePack) sources.push({ name: 'content.md ## Pack', read: () => layout.files.inlinePack })
44
+ for (const { name, read } of sources) {
45
+ // pack-<platform>.json names its platform; pack.json and the inline block follow the Run's.
46
+ const platform = /^pack-/.test(name) ? name.replace(/^pack-/, '').replace(/\.json$/i, '') : layout.frontmatter.platform || undefined
44
47
  try {
45
- const pack = JSON.parse(readFileSync(join(runDir, name), 'utf8'))
48
+ const pack = JSON.parse(read())
46
49
  packs.push({ file: name, pages: Array.isArray(pack.pages) ? pack.pages.length : 0, ...validateContentPack(pack, {
47
50
  requirePageTemplateId: options.requirePageTemplateId,
48
51
  platform,