@easysociable/cli 0.1.22 → 0.1.24

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.22",
3
+ "version": "0.1.24",
4
4
  "description": "Agent-first CLI for EasySociable: Content Engine, Template, Slideshow, and MCP",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -5,8 +5,8 @@ description: >
5
5
  EasySociable Studio. Research facts, pick a proven hook and structure
6
6
  (Formula), lay pages out from reference designs (the user's, a Formula's,
7
7
  or the catalog's, shuffled), render-check every page locally, then create the Run and hand the user a Studio link.
8
- Ask the user only for what only they know (their Brand, once; their own
9
- images/screenshots). Final pixels come only from Studio Download.
8
+ Never block on a question: a missing Brand shows as a bound sample the user
9
+ fills once in Studio. Final pixels come only from Studio Download.
10
10
  ---
11
11
 
12
12
  # EasySociable Slideshow Skill
@@ -16,10 +16,28 @@ enough to post, where every word is still editable. The output is editable,
16
16
  so **a wrong guess is cheap and a question is expensive**: decide platform,
17
17
  page count, arc, style and copy yourself; the user fixes details in Studio.
18
18
 
19
- Ask only for what only the user knows:
19
+ What only the user has never blocks the Run; it becomes a visible, replaceable
20
+ stand-in they fill in Studio:
20
21
 
21
- - their Brand (IP name, handle, avatar) — once, ever;
22
- - their own material (screenshots, photos) when a page needs it.
22
+ - their Brand (name, handle, avatar) → bound samples (step 1);
23
+ - their own material (screenshots, photos) → image placeholders (step 5).
24
+
25
+ ## Command output (`--json`)
26
+
27
+ Read results from these paths; don't guess:
28
+
29
+ | Command | Result |
30
+ |---|---|
31
+ | `auth status` | `authenticated`, `account.email` (no `data` wrapper) |
32
+ | `brands list` / `brands get` | `data[]` / `data` (`id`, `name`, `handle`, `colors`, `logoUrl`) |
33
+ | `hooks recommend` | `data.cards[]` (`id`, `hook_type`, `status`, `format`) |
34
+ | `formulas recommend` | `data.cards[]` (`id`, `status`, `recipe`, `swipe`), `data.stop` |
35
+ | `formulas get` | `data` (`beats[]`, `looks[].layouts`) |
36
+ | `templates references` | a top-level array (`familyId`, `name`, `roles`, `style`, `requiresImage`) |
37
+ | `templates source` | top level: `familyId`, `canvas`, `studioJson` |
38
+ | `icons search --query <word>` | icon list for the word |
39
+ | `images upload --path …` | `data[]` (`path`, `asset.url`, or `error`); exit 1 if any failed |
40
+ | `slideshows create` | top level: `id`, `studioUrl` |
23
41
 
24
42
  ## Workflow
25
43
 
@@ -32,22 +50,25 @@ Ask only for what only the user knows:
32
50
  - `authenticated: true` → say once: "This Run will be created as
33
51
  `<account.email>` — open Studio with the same account."
34
52
 
35
- ### 1. Brand (ask once, ever)
53
+ ### 1. Brand (never ask)
36
54
 
37
55
  `easysociable brands list --json`.
38
56
 
39
57
  - A Brand exists → use it (the most recently updated if several), silently.
40
- - None → ask in **one** message, everything skippable:
41
- "What name and @handle should appear on your carousels? (Optional: an
42
- avatar/logo image path.) Reply 'skip' to post without a signature."
43
- Create it with `easysociable brands create --file brand.json`
44
- (`{ "name", "handle", "colors", "logoAssetId" }`) using the default palette
45
- that matches the deck's style (`references/REFERENCES.md`). An avatar: upload
46
- it first (step 5's `images upload`) and pass the returned `asset.id` as
47
- `logoAssetId`; `brands get` then returns its `logoUrl`.
48
- Skipped → no Brand: header shows the topic, the CTA has no handle, and that
49
- palette goes in `theme.json` (without a Brand or theme Studio falls back to
50
- its dark default).
58
+ Its colours and fonts style the Run (`--brand-id` in step 7).
59
+ - None, but the prompt gives a name / handle / avatar → create it:
60
+ `easysociable brands create --file brand.json`
61
+ (`{ "name", "handle", "colors", "logoAssetId" }`, colours = the deck style's
62
+ default palette in `references/REFERENCES.md`). An avatar: upload it first
63
+ (`images upload --path <file> --json`) and pass `data[0].asset.id` as
64
+ `logoAssetId`.
65
+ - None and nothing given → no Brand, no question. The style's palette goes in
66
+ `theme.json`.
67
+
68
+ Either way every Run carries the Brand spots (cover signature, CTA handle),
69
+ bound with `data-brand`; whatever the Brand lacks shows the sample `Your Name`
70
+ / `@yourhandle` / sample avatar (`references/REFERENCES.md` rule 5). Studio
71
+ asks the user for exactly those fields and updates every page when they fill them.
51
72
 
52
73
  ### 2. Facts and evidence
53
74
 
@@ -69,9 +90,11 @@ neither.
69
90
  - Take image URLs from pages you already opened (`og:image`, images in the
70
91
  body) and download them: `curl -sL -o assets/<name>.png <url>`. PNG, JPEG or
71
92
  WebP only (convert anything else to PNG), ≤ 20 MB.
72
- - Launch posts often show the product only in demo videos: download the video
73
- and pull frames of the real UI (`ffmpeg -ss <seconds> -i demo.mp4 -frames:v 1
74
- frame.png`), then crop to the phone or window. A frame where the key moment is
93
+ - Launch posts often show the product only in demo videos: pull frames of the
94
+ real UI straight from the video URL, without downloading the whole file
95
+ (`ffmpeg -ss <seconds> -i <video url> -frames:v 1 frame.png`; a contact sheet
96
+ to find the moment: `ffmpeg -i <video url> -vf "fps=1/3,scale=240:-1,tile=6x2"
97
+ -frames:v 1 sheet.jpg`), then crop to the phone or window. A frame where the key moment is
75
98
  on screen (an approval prompt, a result) beats a generic screen.
76
99
  - **Look at every file** before keeping it. Drop it when it is off-topic,
77
100
  unreadable at phone size, would need more than 1.25× upscaling to fill its
@@ -172,10 +195,8 @@ Say 'same as this'. Want another look? Say 'different look'."
172
195
  - Fill each image area in this order:
173
196
  1. The user's own image, when they gave one.
174
197
  2. An evidence image from step 2. Upload it
175
- (`easysociable images upload --file body.json` with
176
- `{ "file": "data:image/png;base64,…", "filename": "x.png" }`; build it with
177
- `node -e "const f=process.argv[1],t=f.match(/\.(jpe?g|webp)$/i)?(/webp/i.test(f)?'webp':'jpeg'):'png';console.log(JSON.stringify({file:'data:image/'+t+';base64,'+require('fs').readFileSync(f,'base64'),filename:require('path').basename(f)}))" assets/x.png > body.json`)
178
- and use the returned `asset.url` as `src`.
198
+ (all at once: `easysociable images upload --path assets/a.png assets/b.jpg --json`)
199
+ and use each returned `data[i].asset.url` as `src`.
179
200
  Crop and scale it (`objectFit: 'cover'`, `objectPosition`), never alter
180
201
  what it shows. To point at something in it, add a separate element on top
181
202
  (a red ellipse border, an underline bar) so the user can move or delete it.
@@ -212,7 +233,11 @@ easysociable slideshows create --file pack.json --brand-id <brandId> --json
212
233
  # no Brand: --theme theme.json instead of --brand-id
213
234
  ```
214
235
 
215
- Reply once with: the `studioUrl`, the arc in one line, the sources (facts and
236
+ Reply once with: the `studioUrl`, the arc in one line, any Brand sample
237
+ left ("The name, handle and avatar on pages 1 and 6 are samples: fill them
238
+ once in the bar at the top of Studio, or in Brands at
239
+ https://my.easysociable.com/brands, and every page updates"; name only the
240
+ fields that are samples), the sources (facts and
216
241
  each evidence image: "p1 image: <url>"), and each image placeholder to replace (page + what to upload). Tell them to edit in
217
242
  Studio and click **Download** for the PNG ZIP.
218
243
 
@@ -33,16 +33,26 @@ Roles overlap in the catalog, so pass related ones together: `hook,cover` ·
33
33
  not need a real one: keep the area's box and fill it from the content with a
34
34
  large number or keyword, a short quote with an oversized quote mark, an
35
35
  accent-colour block carrying one word, or one icon from
36
- `easysociable icons search` on a tinted block. Never a grey placeholder or an
36
+ `easysociable icons search --query <word>` on a tinted block. Never a grey placeholder or an
37
37
  empty box on the cover. If nothing fits, delete the area and give the
38
38
  headline its space.
39
- 5. Brand chrome: the header label is the Brand name (or the topic when there is
40
- no Brand); the CTA names the Brand handle. Never leave a reference's original
41
- name or handle (e.g. "Will.AI.m") on a page. **Bind them** so a Brand change
42
- updates every page: the element holding only the Brand name gets
43
- `data-brand="name"`, only the handle `data-brand="handle"`, and an avatar /
44
- logo `<img>` gets `data-brand="avatar"` with the Brand's `logoUrl` as `src`.
45
- Without a Brand, leave them unbound (the topic is not the Brand).
39
+ 5. Brand chrome — every Run carries the Brand, bound so one Brand edit updates
40
+ every page. The cover has a signature (avatar + name + handle); the CTA has
41
+ avatar + handle next to the action. Inner pages keep only the Brand spots
42
+ their reference already has (never add chrome by habit). Never leave a
43
+ reference's original name or handle (e.g. "Will.AI.m") on a page.
44
+ - The element holding only the name gets `data-brand="name"`, only the
45
+ handle `data-brand="handle"`, the avatar / logo `<img>` `data-brand="avatar"`.
46
+ - Value: the Brand's `name`, `@handle`, `logoUrl`. A field the Brand lacks
47
+ (or no Brand at all) → the sample: `Your Name`, `@yourhandle`, and the
48
+ sample avatar below, still bound. Studio swaps each sample the moment the
49
+ user fills that field, and asks for it until then.
50
+ - Size the spots for a real name (up to ~22 characters) — `check-pages`
51
+ renders them with a long one.
52
+
53
+ Sample avatar (`objectFit: 'cover'`, round it with `borderRadius: '50%'`):
54
+ `data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHdpZHRoPScxNjAnIGhlaWdodD0nMTYwJyB2aWV3Qm94PScwIDAgMTYwIDE2MCc+PHJlY3Qgd2lkdGg9JzE2MCcgaGVpZ2h0PScxNjAnIGZpbGw9JyNDOUNERDMnLz48Y2lyY2xlIGN4PSc4MCcgY3k9JzYyJyByPScyOCcgZmlsbD0nI0Y0RjVGNycvPjxwYXRoIGQ9J00yOCAxNjBjMi0zNCAyNC01MiA1Mi01MnM1MCAxOCA1MiA1MnonIGZpbGw9JyNGNEY1RjcnLz48L3N2Zz4=`
55
+
46
56
  6. Accent colour only on large text (≥ 36px, or ≥ 28px bold) and blocks. Small
47
57
  labels use the text or muted colour.
48
58
  7. Semantic `data-type` values (`title`, `subtitle`, `kicker`, `metric`,
@@ -24,6 +24,8 @@ const brandRaw = themeFile ? JSON.parse(readFileSync(themeFile, 'utf8')) : null
24
24
  const brand = brandRaw?.data ?? brandRaw
25
25
  const c = brand?.colors ?? brand?.color ?? {}
26
26
  // Studio default theme (apps/easysociable/app/lib/themeTokens.ts DEFAULT_AUTHORING_THEME_TOKENS).
27
+ // Brand spots render with the real Brand, or a long stand-in so a real name fits later.
28
+ const BRAND = { name: brand?.name || 'Alexandra Montgomery', handle: brand?.handle ? `@${brand.handle.replace(/^@/, '')}` : '@alexandra.montgomery', avatar: brand?.logoUrl || null }
27
29
  const colors = { canvas: c.canvas ?? '#0f1419', text: c.text ?? '#e8eef5', muted: c.textMuted ?? c.muted ?? '#8b9bb0', accent: c.accent ?? '#5b8cff', primary: c.primary ?? c.accent ?? '#5b8cff' }
28
30
 
29
31
  const chrome = [process.env.CHROME_PATH,
@@ -57,6 +59,7 @@ for (const [i, src] of SOURCES.entries()) {
57
59
  const box = document.createElement('div')
58
60
  box.setAttribute('data-authoring-theme', '')
59
61
  box.innerHTML = toHtml(src.json)
62
+ for (const el of box.querySelectorAll('[data-brand]')) { const f = el.dataset.brand; if (f === 'avatar') { if (BRAND.avatar) el.src = BRAND.avatar } else if (BRAND[f]) el.textContent = BRAND[f] }
60
63
  if (GRID) { box.style.cssText = 'width:' + src.width * S + 'px;height:' + src.height * S + 'px;overflow:hidden'; box.firstElementChild.style.transform = 'scale(' + S + ')'; box.firstElementChild.style.transformOrigin = '0 0' }
61
64
  stage.appendChild(box)
62
65
  }
@@ -112,7 +115,7 @@ for (const page of pack.pages) if (!page.layout?.studioJson) console.log(`page $
112
115
  const COLS = 4
113
116
  const html = `<!doctype html><meta charset="utf-8"><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=block">
114
117
  <style>${themeCss}</style><div id="stage"></div><pre id="report"></pre>
115
- <script>const SOURCES = ${JSON.stringify(pages).replace(/</g, '\\u003c')}, COLS = ${COLS};${pageScript}</script>`
118
+ <script>const SOURCES = ${JSON.stringify(pages).replace(/</g, '\\u003c')}, COLS = ${COLS}, BRAND = ${JSON.stringify(BRAND).replace(/</g, '\\u003c')};${pageScript}</script>`
116
119
  const file = join(outDir, 'pages.html')
117
120
  writeFileSync(file, html)
118
121
  const W = Math.max(...pages.map(p => p.width)), Hs = pages.map(p => p.height)
@@ -124,9 +127,13 @@ const rows = Math.ceil(pages.length / COLS)
124
127
  execFileSync(chrome, [...base, `--window-size=${Math.min(COLS, pages.length) * (W / 2 + 16) + 16},${rows * (Math.max(...Hs) / 2 + 16) + 16}`, `--screenshot=${join(outDir, 'grid.png')}`, pathToFileURL(file).href + '#grid'], { stdio: 'ignore' })
125
128
  const raw = dom.match(/<pre id="report">([\s\S]*?)<\/pre>/)?.[1]?.replace(/&quot;/g, '"').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&')
126
129
  const reports = raw ? JSON.parse(raw) : pages.map(() => ({ errors: ['no report (page script did not finish)'], warnings: [] }))
130
+ // Every Run carries the Brand: a cover signature and a CTA handle (references/REFERENCES.md rule 5).
131
+ const brandSpots = { 0: ['name', 'handle', 'avatar'], [pages.length - 1]: ['handle', 'avatar'] }
127
132
  let failed = false
128
133
  for (const [i, page] of pages.entries()) {
129
134
  const report = reports[i]
135
+ for (const f of (brandSpots[i] ?? []).filter(f => !page.json.includes(`data-brand="${f}"`)))
136
+ report.errors.push(`no Brand ${f}: the ${i ? 'last' : 'first'} page needs a data-brand="${f}" element`)
130
137
  if (report.errors.length) failed = true
131
138
  console.log(`page ${page.n} (${page.role}): ${report.errors.length ? 'FAIL' : 'ok'}`)
132
139
  for (const e of report.errors) console.log(` ERROR ${e}`)