@easysociable/cli 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.
Files changed (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +95 -0
  3. package/bin/easysociable.js +15 -0
  4. package/dist/easysociable.js +35464 -0
  5. package/dist/easysociable.js.map +1 -0
  6. package/package.json +58 -0
  7. package/skills/ONBOARDING.md +61 -0
  8. package/skills/easysociable-content/SKILL.md +115 -0
  9. package/skills/easysociable-content/platforms/claude.md +5 -0
  10. package/skills/easysociable-content/platforms/codex.md +5 -0
  11. package/skills/easysociable-content/platforms/hermes.md +5 -0
  12. package/skills/easysociable-content/platforms/openclaw.md +5 -0
  13. package/skills/easysociable-content/references/CLI.md +27 -0
  14. package/skills/easysociable-content/references/MCP.md +20 -0
  15. package/skills/easysociable-content/references/PAGE_PACK.md +6 -0
  16. package/skills/easysociable-content/references/TROUBLESHOOTING.md +11 -0
  17. package/skills/easysociable-content/references/VAULT.md +20 -0
  18. package/skills/easysociable-content/scripts/setup.mjs +156 -0
  19. package/skills/easysociable-content/scripts/setup.test.mjs +71 -0
  20. package/skills/easysociable-content/scripts/starter.mjs +53 -0
  21. package/skills/easysociable-content/workflows/ANALYSIS_AND_COVERAGE.md +8 -0
  22. package/skills/easysociable-content/workflows/CONTENT_EXPORT.md +19 -0
  23. package/skills/easysociable-content/workflows/DEVICE_TASKS.md +5 -0
  24. package/skills/easysociable-content/workflows/EXPERIMENTS.md +6 -0
  25. package/skills/easysociable-content/workflows/MCP_FIRST.md +10 -0
  26. package/skills/easysociable-content/workflows/PUBLICATION.md +5 -0
  27. package/skills/easysociable-content/workflows/TOPIC_DRAFT.md +5 -0
  28. package/skills/easysociable-content/workflows/WORKING_DRAFT.md +10 -0
  29. package/skills/easysociable-slideshow/SKILL.md +77 -0
  30. package/skills/easysociable-slideshow/references/CONTENT_PACK.md +25 -0
  31. package/skills/easysociable-slideshow/references/GATEWAY.md +16 -0
  32. package/skills/easysociable-slideshow/references/PLATFORM_CAPS.md +17 -0
  33. package/skills/easysociable-slideshow/references/VAULT.md +18 -0
  34. package/skills/easysociable-slideshow/workflows/CREATE_RUN.md +7 -0
  35. package/skills/easysociable-slideshow/workflows/MCP_FIRST.md +14 -0
  36. package/skills/easysociable-slideshow/workflows/OUT_OF_SCOPE.md +8 -0
  37. package/skills/easysociable-slideshow/workflows/PUBLISH_RUN.md +4 -0
  38. package/skills/easysociable-slideshow/workflows/RENDER_RUN.md +6 -0
  39. package/skills/easysociable-template/SKILL.md +106 -0
  40. package/skills/easysociable-template/references/AUTHORING_SOURCE.md +129 -0
  41. package/skills/easysociable-template/references/CATALOG_METADATA.md +221 -0
  42. package/skills/easysociable-template/references/DESIGN_INTELLIGENCE.md +69 -0
  43. package/skills/easysociable-template/references/DESIGN_V2_1.md +278 -0
  44. package/skills/easysociable-template/references/EXTERNAL_RESOURCES.md +52 -0
  45. package/skills/easysociable-template/references/GATEWAY.md +25 -0
  46. package/skills/easysociable-template/references/PLATFORM_SPECS.md +134 -0
  47. package/skills/easysociable-template/references/SLOTS.md +104 -0
  48. package/skills/easysociable-template/references/THEME_PRESETS.md +48 -0
  49. package/skills/easysociable-template/references/THEME_TOKENS.md +89 -0
  50. package/skills/easysociable-template/references/fixtures/surface-gradient.design-plan.json +74 -0
  51. package/skills/easysociable-template/references/theme-presets.json +95 -0
  52. package/skills/easysociable-template/scripts/generate-theme-from-hex.mjs +402 -0
  53. package/skills/easysociable-template/scripts/validate-authoring-source.mjs +228 -0
  54. package/skills/easysociable-template/scripts/validate-design-v2.mjs +318 -0
  55. package/skills/easysociable-template/workflows/ASSET_PLAN.md +34 -0
  56. package/skills/easysociable-template/workflows/AUTHORING_FROM_BRIEF.md +52 -0
  57. package/skills/easysociable-template/workflows/AUTHORING_FROM_IMAGE.md +52 -0
  58. package/skills/easysociable-template/workflows/AUTHORING_STUDIO.md +122 -0
  59. package/skills/easysociable-template/workflows/DESIGN_BRIEF.md +92 -0
  60. package/skills/easysociable-template/workflows/DESIGN_CONCEPTS.md +41 -0
  61. package/skills/easysociable-template/workflows/DESIGN_PHILOSOPHY.md +69 -0
  62. package/skills/easysociable-template/workflows/FILL_AND_RENDER.md +86 -0
  63. package/skills/easysociable-template/workflows/MCP_FIRST.md +9 -0
  64. package/skills/easysociable-template/workflows/OUT_OF_SCOPE.md +15 -0
  65. package/skills/easysociable-template/workflows/PALETTE_REVIEW.md +71 -0
  66. package/skills/easysociable-template/workflows/PUBLISH_FROM_DRAFT.md +60 -0
  67. package/skills/easysociable-template/workflows/SURFACE_REVIEW.md +178 -0
  68. package/skills/easysociable-template/workflows/THEME_FROM_BRAND.md +73 -0
  69. package/skills/easysociable-template/workflows/VISUAL_QA.md +55 -0
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@easysociable/cli",
3
+ "version": "0.1.0",
4
+ "description": "Agent-first CLI and MCP host for EasySociable Template and Slideshow V1",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "EasySociable",
8
+ "homepage": "https://easysociable.com",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/murphywuwu/8MRR-Project.git",
12
+ "directory": "apps/cli"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/murphywuwu/8MRR-Project/issues"
16
+ },
17
+ "keywords": [
18
+ "easysociable",
19
+ "cli",
20
+ "mcp",
21
+ "social-media",
22
+ "templates",
23
+ "slideshow",
24
+ "agent"
25
+ ],
26
+ "engines": {
27
+ "node": ">=20.0.0"
28
+ },
29
+ "bin": {
30
+ "easysociable": "./bin/easysociable.js"
31
+ },
32
+ "files": [
33
+ "bin",
34
+ "dist",
35
+ "skills",
36
+ "LICENSE",
37
+ "README.md"
38
+ ],
39
+ "publishConfig": {
40
+ "access": "public",
41
+ "registry": "https://registry.npmjs.org/"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^22.14.0",
45
+ "commander": "^11.1.0",
46
+ "tsup": "^8.0.2",
47
+ "typescript": "^5.8.3",
48
+ "vitest": "^1.3.1",
49
+ "@easysociable/tsconfig": "0.0.0",
50
+ "@easysociable/vitest-config": "0.0.0"
51
+ },
52
+ "scripts": {
53
+ "typecheck": "tsc --noEmit",
54
+ "test": "vitest run",
55
+ "build": "tsup",
56
+ "pack:check": "npm pack --dry-run"
57
+ }
58
+ }
@@ -0,0 +1,61 @@
1
+ # EasySociable agent-first onboarding
2
+
3
+ V1 expects Codex or Claude Code to drive setup. The website is not the primary
4
+ workspace.
5
+
6
+ ## User steps (3)
7
+
8
+ 1. **Install CLI**
9
+
10
+ ```bash
11
+ curl -fsSL https://easysociable.com/install.sh | bash
12
+ ```
13
+
14
+ 2. **Install hosted Skills + MCP** (Template + Slideshow by default)
15
+
16
+ ```bash
17
+ easysociable install --yes
18
+ ```
19
+
20
+ 3. **Ask the Agent to create graphics** — account creation is deferred until a
21
+ hosted operation requires authentication.
22
+
23
+ Optional later:
24
+
25
+ ```bash
26
+ easysociable install --skills content --yes
27
+ ```
28
+
29
+ Then run the `easysociable-content` setup workflow (Obsidian Vault + Content
30
+ Engine). Skipping Content does **not** block single-page or Slideshow renders.
31
+
32
+ ## Lazy auth (Agent script)
33
+
34
+ When a Gateway call returns `AUTH_REQUIRED`, `API_KEY_INVALID`, or the CLI says
35
+ `Run easysociable auth login`, respond with:
36
+
37
+ ```text
38
+ EasySociable needs a workspace API key before it can preview or render on the
39
+ Gateway.
40
+
41
+ 1. Open https://my.easysociable.com/auth/signup (or sign in with Google).
42
+ 2. Verify your email if you used password signup (required before API keys).
43
+ 3. Dashboard → API Keys → Create API key → copy the secret once.
44
+ 4. Run: easysociable auth login --api-key <paste-key> --gateway-url https://gateway.easysociable.com
45
+
46
+ Tell me when that is done and I will continue.
47
+ ```
48
+
49
+ After `auth login` succeeds, retry the failed operation with the same
50
+ idempotency key when applicable.
51
+
52
+ ## What works without auth
53
+
54
+ - `easysociable install` / `update`
55
+ - Reading installed Skill instructions
56
+ - Public template catalog in the browser (`/templates`)
57
+
58
+ ## What requires auth
59
+
60
+ - Profiles, Themes, images, drafts, Slideshow Runs
61
+ - Hosted preview sessions and final PNG renders (`hosted_render_page` allowance)
@@ -0,0 +1,115 @@
1
+ ---
2
+ name: easysociable-content
3
+ description: >
4
+ Set up and operate a local Obsidian-based Content Engine for EasySociable.
5
+ Obsidian is required. During setup, ask the user to choose 中文 or English;
6
+ that locale names the engine folders and starter notes. This Skill records
7
+ Runs, Packs and exports for the Template and Slideshow Skills. It does not
8
+ provide a hosted content backend, Content API, SQLite runtime, content MCP
9
+ server or content CLI.
10
+ ---
11
+
12
+ # EasySociable Content Skill
13
+
14
+ This Skill owns the user's local knowledge base. It is intentionally filesystem-
15
+ first: the Agent writes Markdown and JSON under the user's existing Obsidian
16
+ Vault, while hosted Template and Slideshow operations are performed later by
17
+ the unified `easysociable` CLI/MCP.
18
+
19
+ ## Non-negotiable rules
20
+
21
+ 1. Obsidian must already be installed. If it is missing, stop and give the
22
+ official installation guidance; do not create a partial vault.
23
+ 2. Ask the user to choose **中文 (`zh`)** or **English (`en`)** before the first
24
+ preview. Do not default the locale silently.
25
+ 3. Ask the user to choose an existing Vault. Never create a new Vault silently.
26
+ 4. Create or validate only the locale root: `Content Engine/` (`en`) or
27
+ `内容引擎/` (`zh`). Never create both in one Vault.
28
+ 5. Existing Content Engine files are read-only during setup. Report differences
29
+ and ask before any migration; never auto-repair or overwrite user content.
30
+ 6. Do not start a Local API, open SQLite, call Content MCP, or call private
31
+ Directus/R2/Stripe endpoints.
32
+ 7. Every intent/run is saved under the locale runs folder (`runs/<slug>/` or
33
+ `成稿/<slug>/`). Every platform gets a Pack. Binary outputs belong only
34
+ under the locale exports folder.
35
+ 8. The host Agent owns copy and semantic decisions. Template and Slideshow Skills
36
+ own template selection, hosted preview and final rendering.
37
+ 9. V1 final output is PNG only. Never create ZIP, JPEG, WebP, PDF or video output.
38
+ 10. `content-pack.v1` JSON field names, `role` values and slot keys stay English
39
+ even when the vault language is 中文. Folder names and Markdown notes follow
40
+ the chosen locale.
41
+
42
+ ## Setup workflow
43
+
44
+ 1. Detect Obsidian.app.
45
+ 2. Ask: 内容引擎用中文还是 English? Map the answer to `--locale zh` or `--locale en`.
46
+ 3. Ask for an existing Vault path.
47
+ 4. Preview with `scripts/setup.mjs --vault <path> --locale zh|en --preview --json`.
48
+ 5. Create the sanitized Starter and generalized `CLAUDE.md` only after confirmation,
49
+ using `--apply`.
50
+ 6. Open the Vault in Obsidian.
51
+ 7. Return the Vault path, locale, engine folder name, and the next workflow.
52
+
53
+ The deterministic setup helper is `scripts/setup.mjs`. Use `--preview` before
54
+ `--apply`; it never overwrites an existing file and refuses to create anything
55
+ when Obsidian is not installed. It is a helper for the host Agent, not a
56
+ separate product CLI.
57
+
58
+ After setup, read `locale.json` in the engine root (or detect `内容引擎/` vs
59
+ `Content Engine/`) and keep using that tree. Do not switch language later.
60
+
61
+ The Starter contains empty indexes, templates and operating rules only. It must
62
+ not contain the owner's personal examples, previous Runs or generated media.
63
+
64
+ ## Run and Pack workflow
65
+
66
+ For each piece of content, create a local Run with:
67
+
68
+ - a stable slug and title;
69
+ - platform and language;
70
+ - semantic outline and page roles;
71
+ - generated copy and selected media references;
72
+ - the chosen Profile/Theme identifiers when known;
73
+ - links to the corresponding platform Pack and export Manifest.
74
+
75
+ Write user-facing notes in the vault locale. The Pack is the handoff to
76
+ `easysociable-slideshow`. It contains public page roles and slot values, never
77
+ private AST, Binding Maps, RenderPlans or a hosted ContentDocument:
78
+
79
+ ```json
80
+ {
81
+ "schemaVersion": "content-pack.v1",
82
+ "platform": "tiktok",
83
+ "title": "A short story",
84
+ "pages": [
85
+ { "role": "hook", "slots": { "text_0": "Hook", "text_1": "Support" } },
86
+ { "role": "value", "slots": { "text_0": "One idea" } }
87
+ ]
88
+ }
89
+ ```
90
+
91
+ ## Handoff
92
+
93
+ For a single image, hand the selected published Page Template and slot values to
94
+ the Template workflow. For a slideshow:
95
+
96
+ 1. write the Pack to the Run folder;
97
+ 2. ask the Slideshow Skill to create/update a temporary Run;
98
+ 3. let the Agent review the returned Studio URL;
99
+ 4. after confirmation, let the Agent call `easysociable slideshows render`;
100
+ 5. let the Agent call `easysociable renders export` to atomically write PNGs and
101
+ `manifest.json` under the Run's exports directory.
102
+
103
+ Preview is not a final local export and does not consume hosted Render Page
104
+ allowance. The final render does.
105
+
106
+ Network research is optional. If the host does not expose a search tool, keep
107
+ the source URL in the local note and continue with user-provided evidence; do
108
+ not install an MCP server automatically and do not block vault setup.
109
+
110
+ ## Required references
111
+
112
+ - `references/PAGE_PACK.md`
113
+ - `references/VAULT.md`
114
+ - `workflows/CONTENT_EXPORT.md`
115
+ - `workflows/MCP_FIRST.md` (for the unified CLI/MCP handoff only)
@@ -0,0 +1,5 @@
1
+ # Claude Desktop
2
+
3
+ - Install the unified EasySociable Skill bundle. Content remains Obsidian-first;
4
+ hosted Template/Slideshow calls use the unified `easysociable` CLI/MCP.
5
+ - Prefer the Skill frontmatter in each `SKILL.md`.
@@ -0,0 +1,5 @@
1
+ # Codex
2
+
3
+ - Install `skills/easysociable-content` under the host skill directory for local content workflows.
4
+ - Install the unified EasySociable Skill bundle; hosted preview/render uses the
5
+ unified `easysociable` CLI/MCP.
@@ -0,0 +1,5 @@
1
+ # Hermes overrides
2
+
3
+ - Point Hermes skill loader at this directory.
4
+ - Prefer explicit Vault paths in multiprocess environments; do not use the
5
+ retired `--data-dir` Runtime option.
@@ -0,0 +1,5 @@
1
+ # OpenClaw overrides
2
+
3
+ - Register the unified `easysociable` CLI/MCP entry.
4
+ - Keep API Keys and session material out of tool logs. Gateway authentication is
5
+ owned by the unified client.
@@ -0,0 +1,27 @@
1
+ # Content Skill filesystem reference
2
+
3
+ The Content Skill does not own a Content CLI in V1. It writes the user's
4
+ existing Obsidian Vault directly and uses the unified `easysociable` command only
5
+ for the hosted Template/Slideshow handoff.
6
+
7
+ ## Local workflow
8
+
9
+ Use the host's filesystem tools to detect Obsidian, ask for 中文 or English,
10
+ choose a Vault, preview the locale engine paths (`Content Engine/` or
11
+ `内容引擎/`) with `--locale zh|en`, create or validate Markdown/JSON files after
12
+ confirmation, and open the Vault in Obsidian.
13
+
14
+ Do not invent `easysociable-content init/start/doctor` commands and do not pass
15
+ `--data-dir`. Those belong to the retired Content Runtime.
16
+
17
+ ## Hosted handoff
18
+
19
+ ```bash
20
+ easysociable templates list --platforms tiktok --json
21
+ easysociable slideshows create --file ./runs/<slug>/pack-tiktok.json --json
22
+ easysociable slideshows render --id run_xxx --idempotency-key run_xxx-final-1 --json
23
+ easysociable renders wait --id job_xxx --json
24
+ easysociable renders export --id job_xxx --output ./exports/<slug>/tiktok --json
25
+ ```
26
+
27
+ Use the unified MCP first when the host exposes it. The CLI is the fallback.
@@ -0,0 +1,20 @@
1
+ # Unified MCP reference
2
+
3
+ V1 has one MCP server installed by the unified EasySociable installer. It owns
4
+ hosted Template, Image, Profile, Theme, Slideshow and Render tools. The Content
5
+ Skill itself does not require a Content MCP or local MCP bridge.
6
+
7
+ Use the MCP tool registry exposed by `easysociable mcp serve --transport stdio`. Content work stays
8
+ in the Obsidian filesystem; hosted calls use the same Gateway client and account
9
+ as the CLI.
10
+
11
+ ## Handoff sequence
12
+
13
+ 1. Write a `content-pack.v1` into the local Run folder.
14
+ 2. Call `create_slideshow_run` with the Pack.
15
+ 3. Open the returned Studio URL and wait for Save.
16
+ 4. After confirmation call `render_slideshow_run`.
17
+ 5. Poll the render job and export the PNG set into the Vault.
18
+
19
+ The MCP server must never expose private AST, Binding Maps, RenderPlans, R2
20
+ credentials, Directus credentials, Stripe credentials or Local Runtime tokens.
@@ -0,0 +1,6 @@
1
+ # PagePack handoff
2
+
3
+ `content-pack.v1` is the boundary between semantic content and visual
4
+ composition. It contains platform, optional title, and ordered pages with a
5
+ semantic `role` and public `slots`. The Slideshow Skill performs template
6
+ selection and rendering; Content does not call Template APIs.
@@ -0,0 +1,11 @@
1
+ # Troubleshooting
2
+
3
+ The V1 Content Skill has no Content CLI, Local API, SQLite runtime or runtime
4
+ token. If setup fails, stop before writing files, show the Obsidian install and
5
+ Vault-selection guidance, and ask the user to retry after the prerequisite is
6
+ ready. Never invent `--data-dir`, `init`, `doctor` or runtime credentials.
7
+
8
+ If a local Run is incomplete, keep the existing Markdown/JSON intact and report
9
+ the missing file or field. Do not auto-migrate or repair an existing Content
10
+ Engine folder. Publication, metrics, retrospectives and experiments are
11
+ deferred in V1; keep those notes as ordinary Markdown only.
@@ -0,0 +1,20 @@
1
+ # Vault layout
2
+
3
+ Setup asks the user to choose a locale, then creates one engine root:
4
+
5
+ | Locale | Root | Runs | Exports |
6
+ |---|---|---|---|
7
+ | `en` | `Content Engine/` | `runs/<slug>/` | `exports/<slug>/<platform>/` |
8
+ | `zh` | `内容引擎/` | `成稿/<slug>/` | `导出/<slug>/<platform>/` |
9
+
10
+ Read `locale.json` in that root after setup:
11
+
12
+ ```json
13
+ { "locale": "zh" }
14
+ ```
15
+
16
+ English folders stay English (`accounts`, `inbox`, `swipe`, `topics`, …).
17
+ Chinese folders are `账号`, `收件箱`, `拆解`, `选题`, `素材`, `档案`, `已发布`,
18
+ `百科`, `平台`. Platform note filenames stay `x.md`, `threads.md`, `tiktok.md`.
19
+
20
+ Never create both roots in one Vault. Pack JSON keys stay English.
@@ -0,0 +1,156 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync, mkdirSync, statSync, writeFileSync } from 'node:fs'
4
+ import { homedir } from 'node:os'
5
+ import { join, resolve } from 'node:path'
6
+ import { spawnSync } from 'node:child_process'
7
+ import { ROOT, filesFor } from './starter.mjs'
8
+
9
+ const LOCALES = new Set(['en', 'zh'])
10
+
11
+ function obsidianCandidates() {
12
+ if (process.env.EASYSOCIABLE_OBSIDIAN_APP)
13
+ return [process.env.EASYSOCIABLE_OBSIDIAN_APP]
14
+ return [
15
+ '/Applications/Obsidian.app',
16
+ join(homedir(), 'Applications/Obsidian.app'),
17
+ '/System/Applications/Obsidian.app',
18
+ ].filter(Boolean)
19
+ }
20
+
21
+ function hasObsidian() {
22
+ if (process.env.EASYSOCIABLE_SKIP_OBSIDIAN_CHECK === 'true') return true
23
+ return obsidianCandidates().some((path) => existsSync(path))
24
+ }
25
+
26
+ function parseArgs(argv) {
27
+ const options = { vault: '', locale: '', apply: false, validate: false, preview: false, noOpen: false, json: false }
28
+ for (let i = 0; i < argv.length; i += 1) {
29
+ const arg = argv[i]
30
+ if (arg === '--vault') options.vault = argv[++i] ?? ''
31
+ else if (arg === '--locale') options.locale = argv[++i] ?? ''
32
+ else if (arg === '--apply') options.apply = true
33
+ else if (arg === '--validate') options.validate = true
34
+ else if (arg === '--preview') options.preview = true
35
+ else if (arg === '--no-open') options.noOpen = true
36
+ else if (arg === '--json') options.json = true
37
+ else if (arg === '--help' || arg === '-h') options.help = true
38
+ else throw new Error(`Unknown argument: ${arg}`)
39
+ }
40
+ return options
41
+ }
42
+
43
+ function output(value, json) {
44
+ process.stdout.write(json ? `${JSON.stringify(value, null, 2)}\n` : `${value.message ?? value}\n`)
45
+ }
46
+
47
+ function isDir(path) {
48
+ return existsSync(path) && statSync(path).isDirectory()
49
+ }
50
+
51
+ function enginePaths(vault) {
52
+ return { en: join(vault, ROOT.en), zh: join(vault, ROOT.zh) }
53
+ }
54
+
55
+ function detectedLocale(vault) {
56
+ const paths = enginePaths(vault)
57
+ const en = isDir(paths.en)
58
+ const zh = isDir(paths.zh)
59
+ if (en && zh) return { code: 'BOTH_ENGINES', message: 'Vault has both Content Engine/ and 内容引擎/. Choose one folder before setup.' }
60
+ if (zh) return { locale: 'zh' }
61
+ if (en) return { locale: 'en' }
62
+ return { locale: '' }
63
+ }
64
+
65
+ function resolveLocale(vault, requested) {
66
+ if (requested && !LOCALES.has(requested)) {
67
+ return { code: 'LOCALE_INVALID', message: 'Choose --locale en or --locale zh.' }
68
+ }
69
+ const found = detectedLocale(vault)
70
+ if (found.code) return found
71
+ if (requested && found.locale && requested !== found.locale) {
72
+ return { code: 'LOCALE_MISMATCH', message: `Vault already uses locale ${found.locale}. Do not create the other language tree.` }
73
+ }
74
+ const locale = requested || found.locale
75
+ if (!locale) return { code: 'LOCALE_REQUIRED', message: 'Ask the user to choose 中文 (zh) or English (en), then pass --locale.' }
76
+ return { locale }
77
+ }
78
+
79
+ function inventory(root, locale) {
80
+ const files = filesFor(locale)
81
+ const engine = join(root, ROOT[locale])
82
+ const missing = []
83
+ const existing = []
84
+ for (const relative of Object.keys(files)) {
85
+ const path = join(engine, relative)
86
+ ;(existsSync(path) ? existing : missing).push(relative)
87
+ }
88
+ return { engine, locale, rootName: ROOT[locale], missing, existing }
89
+ }
90
+
91
+ function validateVault(vault, requestedLocale) {
92
+ const absolute = resolve(vault)
93
+ const errors = []
94
+ if (!existsSync(absolute) || !statSync(absolute).isDirectory()) errors.push('Vault path does not exist or is not a directory')
95
+ const resolved = errors.length ? { locale: requestedLocale || 'en' } : resolveLocale(absolute, requestedLocale)
96
+ if (resolved.code) return { vault: absolute, valid: false, errors: [resolved.message], ...resolved, created: [] }
97
+ const result = inventory(absolute, resolved.locale)
98
+ if (existsSync(result.engine) && !isDir(result.engine)) errors.push(`${result.rootName} exists but is not a directory`)
99
+ return { vault: absolute, ...result, valid: errors.length === 0, errors }
100
+ }
101
+
102
+ function applyStarter(vault, locale, noOpen) {
103
+ const report = validateVault(vault, locale)
104
+ if (!report.valid) return report
105
+ const files = filesFor(report.locale)
106
+ mkdirSync(report.engine, { recursive: true })
107
+ for (const relative of report.missing) {
108
+ const path = join(report.engine, relative)
109
+ mkdirSync(join(path, '..'), { recursive: true })
110
+ writeFileSync(path, files[relative], { encoding: 'utf8', flag: 'wx' })
111
+ }
112
+ if (!noOpen && process.env.EASYSOCIABLE_NO_OPEN !== 'true') {
113
+ const vaultName = report.vault.split('/').pop() ?? 'Vault'
114
+ spawnSync('open', [`obsidian://open?vault=${encodeURIComponent(vaultName)}`], { stdio: 'ignore' })
115
+ }
116
+ return { ...validateVault(vault, report.locale), created: [...report.missing] }
117
+ }
118
+
119
+ function printHelp() {
120
+ process.stdout.write(`Usage: node setup.mjs --vault <existing-vault> --locale en|zh [--preview|--apply|--validate] [--no-open] [--json]\n\nObsidian must be installed. Ask the user to choose English or 中文 before apply. Existing files are never overwritten.\n`)
121
+ }
122
+
123
+ try {
124
+ const options = parseArgs(process.argv.slice(2))
125
+ if (options.help) {
126
+ printHelp()
127
+ process.exit(0)
128
+ }
129
+ if (!hasObsidian()) {
130
+ const error = { code: 'OBSIDIAN_REQUIRED', message: 'Obsidian.app is required. Install Obsidian from https://obsidian.md/download before setup.', created: false }
131
+ output(error, options.json)
132
+ process.exitCode = 2
133
+ }
134
+ else if (!options.vault) {
135
+ output({ code: 'VAULT_REQUIRED', message: 'Choose an existing Obsidian Vault with --vault.', created: false }, options.json)
136
+ process.exitCode = 2
137
+ }
138
+ else {
139
+ const report = validateVault(options.vault, options.locale)
140
+ if (!report.valid) {
141
+ output({ ...report, created: [] }, options.json)
142
+ process.exitCode = 2
143
+ }
144
+ else if (options.apply) {
145
+ const applied = applyStarter(options.vault, report.locale, options.noOpen)
146
+ output({ ...applied, action: 'applied' }, options.json)
147
+ }
148
+ else {
149
+ output({ ...report, action: options.validate ? 'validated' : 'preview', created: [] }, options.json)
150
+ }
151
+ }
152
+ }
153
+ catch (error) {
154
+ output({ code: 'CONTENT_SETUP_FAILED', message: error instanceof Error ? error.message : String(error), created: false }, true)
155
+ process.exitCode = 1
156
+ }
@@ -0,0 +1,71 @@
1
+ import assert from 'node:assert/strict'
2
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
3
+ import { tmpdir } from 'node:os'
4
+ import { join } from 'node:path'
5
+ import { spawnSync } from 'node:child_process'
6
+
7
+ const script = new URL('./setup.mjs', import.meta.url).pathname
8
+ const fixture = mkdtempSync(join(tmpdir(), 'easysociable-content-'))
9
+ const vault = join(fixture, 'Vault')
10
+ mkdirSync(vault)
11
+ const env = { ...process.env, EASYSOCIABLE_SKIP_OBSIDIAN_CHECK: 'true', EASYSOCIABLE_NO_OPEN: 'true' }
12
+
13
+ function run(args) {
14
+ return spawnSync(process.execPath, [script, ...args, '--json'], { encoding: 'utf8', env })
15
+ }
16
+
17
+ try {
18
+ const missingObsidian = spawnSync(process.execPath, [script, '--vault', vault, '--locale', 'en', '--apply', '--json'], {
19
+ encoding: 'utf8',
20
+ env: { ...env, EASYSOCIABLE_SKIP_OBSIDIAN_CHECK: 'false', EASYSOCIABLE_OBSIDIAN_APP: join(fixture, 'missing-Obsidian.app') },
21
+ })
22
+ assert.equal(missingObsidian.status, 2)
23
+ assert.equal(existsSync(join(vault, 'Content Engine')), false)
24
+
25
+ const missingLocale = run(['--vault', vault, '--preview'])
26
+ assert.equal(missingLocale.status, 2)
27
+ assert.equal(JSON.parse(missingLocale.stdout).code, 'LOCALE_REQUIRED')
28
+
29
+ const preview = run(['--vault', vault, '--locale', 'en', '--preview'])
30
+ assert.equal(preview.status, 0)
31
+ const previewReport = JSON.parse(preview.stdout)
32
+ assert.equal(previewReport.created.length, 0)
33
+ assert.equal(previewReport.valid, true)
34
+ assert.equal(previewReport.locale, 'en')
35
+
36
+ const apply = run(['--vault', vault, '--locale', 'en', '--apply', '--no-open'])
37
+ assert.equal(apply.status, 0)
38
+ const applied = JSON.parse(apply.stdout)
39
+ assert.equal(applied.action, 'applied')
40
+ assert.equal(applied.created.length > 5, true)
41
+ assert.equal(readFileSync(join(vault, 'Content Engine', 'CLAUDE.md'), 'utf8').includes('local Obsidian'), true)
42
+ assert.equal(JSON.parse(readFileSync(join(vault, 'Content Engine', 'locale.json'), 'utf8')).locale, 'en')
43
+
44
+ const sentinel = join(vault, 'Content Engine', 'runs', 'user-note.md')
45
+ writeFileSync(sentinel, 'keep me', 'utf8')
46
+ const second = run(['--vault', vault, '--apply', '--no-open'])
47
+ assert.equal(second.status, 0)
48
+ assert.equal(readFileSync(sentinel, 'utf8'), 'keep me')
49
+
50
+ const mismatch = run(['--vault', vault, '--locale', 'zh', '--apply', '--no-open'])
51
+ assert.equal(mismatch.status, 2)
52
+ assert.equal(JSON.parse(mismatch.stdout).code, 'LOCALE_MISMATCH')
53
+ assert.equal(existsSync(join(vault, '内容引擎')), false)
54
+
55
+ const zhVault = join(fixture, 'ZhVault')
56
+ mkdirSync(zhVault)
57
+ const zhApply = run(['--vault', zhVault, '--locale', 'zh', '--apply', '--no-open'])
58
+ assert.equal(zhApply.status, 0)
59
+ const zhEngine = join(zhVault, '内容引擎')
60
+ assert.equal(existsSync(join(vault, 'Content Engine')), true)
61
+ assert.equal(readFileSync(join(zhEngine, 'CLAUDE.md'), 'utf8').includes('内容引擎'), true)
62
+ assert.equal(existsSync(join(zhEngine, '成稿', '_索引.md')), true)
63
+ assert.equal(existsSync(join(zhEngine, '选题', '观察清单.md')), true)
64
+ assert.equal(JSON.parse(readFileSync(join(zhEngine, 'locale.json'), 'utf8')).locale, 'zh')
65
+ assert.equal(existsSync(join(zhVault, 'Content Engine')), false)
66
+
67
+ console.log('Content Skill setup smoke passed.')
68
+ }
69
+ finally {
70
+ rmSync(fixture, { recursive: true, force: true })
71
+ }
@@ -0,0 +1,53 @@
1
+ export const ROOT = { en: 'Content Engine', zh: '内容引擎' }
2
+
3
+ export const STARTER = {
4
+ en: {
5
+ 'CLAUDE.md': `# Content Engine\n\nThis folder is the local Obsidian source of truth for EasySociable content runs.\n\n- Keep semantic copy and media references here.\n- Save each run under \`runs/<slug>/\`.\n- Save final PNGs and manifest files only under \`exports/<slug>/<platform>/\`.\n- Do not put private Template AST, Binding Maps or RenderPlans in this vault.\n- Use the unified \`easysociable\` CLI/MCP for hosted Template and Slideshow operations.\n- Pack JSON (\`content-pack.v1\`) field names stay English for the Slideshow handoff.\n`,
6
+ 'accounts/_index.md': '# Accounts\n\nAdd account notes here. Do not store API keys or passwords.\n',
7
+ 'exports/README.md': '# Exports\n\nFinal PNGs and manifests written by the unified render export command.\n',
8
+ 'inbox/README.md': '# Inbox\n\nTemporary notes awaiting triage into topics, swipe files or runs.\n',
9
+ 'inbox/_index.md': '# Inbox Index\n',
10
+ 'profiles/_index.md': '# Profiles\n\nStore local profile references and presentation preferences.\n',
11
+ 'published/_index.md': '# Published\n\nOptional local index of content already published elsewhere.\n',
12
+ 'raw/_index.md': '# Raw\n\nSource notes and imported research.\n',
13
+ 'runs/README.md': '# Runs\n\nEach run stores semantic content, platform Packs and render references.\n',
14
+ 'runs/_index.md': '# Runs Index\n',
15
+ 'swipe/_index.md': '# Swipe\n',
16
+ 'swipe/_template.md': '# Swipe Item\n\n## Source\n\n## Pattern\n\n## Reusable insight\n',
17
+ 'topics/_index.md': '# Topics\n',
18
+ 'topics/WATCHLIST.md': '# Watchlist\n\nKeep links and observations here. Network tools are optional.\n',
19
+ 'topics/_template-item.md': '# Topic\n\n## Thesis\n\n## Audience\n\n## Evidence\n',
20
+ 'wiki/README.md': '# Wiki\n\nStable concepts and operating rules.\n',
21
+ 'platforms/x.md': '# X\n\nKeep X-specific copy and constraints here.\n',
22
+ 'platforms/threads.md': '# Threads\n\nKeep Threads-specific copy and constraints here.\n',
23
+ 'platforms/tiktok.md': '# TikTok\n\nKeep TikTok-specific copy and constraints here.\n',
24
+ },
25
+ zh: {
26
+ 'CLAUDE.md': `# 内容引擎\n\n此文件夹是 EasySociable 内容成稿的本地 Obsidian 真相源。\n\n- 在此保存语义文案和素材引用。\n- 每条成稿放在 \`成稿/<slug>/\`。\n- 最终 PNG 和清单只放在 \`导出/<slug>/<platform>/\`。\n- 不要把模板私有 AST、Binding Map 或 RenderPlan 放进此库。\n- 托管的模板与幻灯片操作使用统一的 \`easysociable\` CLI/MCP。\n- Pack JSON(\`content-pack.v1\`)的字段名保持英文,以便交给 Slideshow Skill。\n`,
27
+ '账号/_索引.md': '# 账号\n\n在此记录账号备注。不要存放 API 密钥或密码。\n',
28
+ '导出/说明.md': '# 导出\n\n由统一渲染导出命令写入的最终 PNG 和清单文件。\n',
29
+ '收件箱/说明.md': '# 收件箱\n\n待整理到选题、拆解或成稿的临时笔记。\n',
30
+ '收件箱/_索引.md': '# 收件箱索引\n',
31
+ '档案/_索引.md': '# 档案\n\n存放本地档案引用和呈现偏好。\n',
32
+ '已发布/_索引.md': '# 已发布\n\n可选:已在其他地方发布的内容本地索引。\n',
33
+ '素材/_索引.md': '# 素材\n\n源笔记和导入的调研。\n',
34
+ '成稿/说明.md': '# 成稿\n\n每条成稿保存语义内容、各平台 Pack 和渲染引用。\n',
35
+ '成稿/_索引.md': '# 成稿索引\n',
36
+ '拆解/_索引.md': '# 拆解\n',
37
+ '拆解/_模板.md': '# 拆解条目\n\n## 来源\n\n## 模式\n\n## 可复用洞察\n',
38
+ '选题/_索引.md': '# 选题\n',
39
+ '选题/观察清单.md': '# 观察清单\n\n在此保存链接和观察。网络检索工具是可选的。\n',
40
+ '选题/_模板.md': '# 选题\n\n## 论点\n\n## 受众\n\n## 证据\n',
41
+ '百科/说明.md': '# 百科\n\n稳定概念和操作规则。\n',
42
+ '平台/x.md': '# X\n\n在此保存 X 平台的文案和约束。\n',
43
+ '平台/threads.md': '# Threads\n\n在此保存 Threads 平台的文案和约束。\n',
44
+ '平台/tiktok.md': '# TikTok\n\n在此保存 TikTok 平台的文案和约束。\n',
45
+ },
46
+ }
47
+
48
+ export function filesFor(locale) {
49
+ return {
50
+ ...STARTER[locale],
51
+ 'locale.json': `${JSON.stringify({ locale }, null, 2)}\n`,
52
+ }
53
+ }
@@ -0,0 +1,8 @@
1
+ # Local analysis notes
2
+
3
+ Save source notes and evidence as Markdown under the selected Obsidian Run.
4
+ Record missing context as a checklist in the Run; do not call a Content CLI,
5
+ Local API or hosted analysis backend.
6
+
7
+ Template and Slideshow Skills consume the resulting local Pack only after the
8
+ Agent and user agree on the content.
@@ -0,0 +1,19 @@
1
+ # Pack and export workflow
2
+
3
+ The Content Skill writes a public `content-pack.v1` handoff. It does not choose
4
+ private template internals and does not render images itself.
5
+
6
+ ```text
7
+ Obsidian Run
8
+ → platform Pack
9
+ → unified Template/Slideshow MCP or CLI
10
+ → Studio preview
11
+ → user confirmation
12
+ → final PNG render
13
+ → renders export
14
+ → locale exports/<slug>/<platform>/01.png + manifest.json
15
+ ```
16
+
17
+ For a single image, call the published Page Template flow. For multiple pages,
18
+ call the Slideshow Run flow. Preview never consumes Render Page allowance;
19
+ final rendering does.