@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.
- package/LICENSE +21 -0
- package/README.md +95 -0
- package/bin/easysociable.js +15 -0
- package/dist/easysociable.js +35464 -0
- package/dist/easysociable.js.map +1 -0
- package/package.json +58 -0
- package/skills/ONBOARDING.md +61 -0
- package/skills/easysociable-content/SKILL.md +115 -0
- package/skills/easysociable-content/platforms/claude.md +5 -0
- package/skills/easysociable-content/platforms/codex.md +5 -0
- package/skills/easysociable-content/platforms/hermes.md +5 -0
- package/skills/easysociable-content/platforms/openclaw.md +5 -0
- package/skills/easysociable-content/references/CLI.md +27 -0
- package/skills/easysociable-content/references/MCP.md +20 -0
- package/skills/easysociable-content/references/PAGE_PACK.md +6 -0
- package/skills/easysociable-content/references/TROUBLESHOOTING.md +11 -0
- package/skills/easysociable-content/references/VAULT.md +20 -0
- package/skills/easysociable-content/scripts/setup.mjs +156 -0
- package/skills/easysociable-content/scripts/setup.test.mjs +71 -0
- package/skills/easysociable-content/scripts/starter.mjs +53 -0
- package/skills/easysociable-content/workflows/ANALYSIS_AND_COVERAGE.md +8 -0
- package/skills/easysociable-content/workflows/CONTENT_EXPORT.md +19 -0
- package/skills/easysociable-content/workflows/DEVICE_TASKS.md +5 -0
- package/skills/easysociable-content/workflows/EXPERIMENTS.md +6 -0
- package/skills/easysociable-content/workflows/MCP_FIRST.md +10 -0
- package/skills/easysociable-content/workflows/PUBLICATION.md +5 -0
- package/skills/easysociable-content/workflows/TOPIC_DRAFT.md +5 -0
- package/skills/easysociable-content/workflows/WORKING_DRAFT.md +10 -0
- package/skills/easysociable-slideshow/SKILL.md +77 -0
- package/skills/easysociable-slideshow/references/CONTENT_PACK.md +25 -0
- package/skills/easysociable-slideshow/references/GATEWAY.md +16 -0
- package/skills/easysociable-slideshow/references/PLATFORM_CAPS.md +17 -0
- package/skills/easysociable-slideshow/references/VAULT.md +18 -0
- package/skills/easysociable-slideshow/workflows/CREATE_RUN.md +7 -0
- package/skills/easysociable-slideshow/workflows/MCP_FIRST.md +14 -0
- package/skills/easysociable-slideshow/workflows/OUT_OF_SCOPE.md +8 -0
- package/skills/easysociable-slideshow/workflows/PUBLISH_RUN.md +4 -0
- package/skills/easysociable-slideshow/workflows/RENDER_RUN.md +6 -0
- package/skills/easysociable-template/SKILL.md +106 -0
- package/skills/easysociable-template/references/AUTHORING_SOURCE.md +129 -0
- package/skills/easysociable-template/references/CATALOG_METADATA.md +221 -0
- package/skills/easysociable-template/references/DESIGN_INTELLIGENCE.md +69 -0
- package/skills/easysociable-template/references/DESIGN_V2_1.md +278 -0
- package/skills/easysociable-template/references/EXTERNAL_RESOURCES.md +52 -0
- package/skills/easysociable-template/references/GATEWAY.md +25 -0
- package/skills/easysociable-template/references/PLATFORM_SPECS.md +134 -0
- package/skills/easysociable-template/references/SLOTS.md +104 -0
- package/skills/easysociable-template/references/THEME_PRESETS.md +48 -0
- package/skills/easysociable-template/references/THEME_TOKENS.md +89 -0
- package/skills/easysociable-template/references/fixtures/surface-gradient.design-plan.json +74 -0
- package/skills/easysociable-template/references/theme-presets.json +95 -0
- package/skills/easysociable-template/scripts/generate-theme-from-hex.mjs +402 -0
- package/skills/easysociable-template/scripts/validate-authoring-source.mjs +228 -0
- package/skills/easysociable-template/scripts/validate-design-v2.mjs +318 -0
- package/skills/easysociable-template/workflows/ASSET_PLAN.md +34 -0
- package/skills/easysociable-template/workflows/AUTHORING_FROM_BRIEF.md +52 -0
- package/skills/easysociable-template/workflows/AUTHORING_FROM_IMAGE.md +52 -0
- package/skills/easysociable-template/workflows/AUTHORING_STUDIO.md +122 -0
- package/skills/easysociable-template/workflows/DESIGN_BRIEF.md +92 -0
- package/skills/easysociable-template/workflows/DESIGN_CONCEPTS.md +41 -0
- package/skills/easysociable-template/workflows/DESIGN_PHILOSOPHY.md +69 -0
- package/skills/easysociable-template/workflows/FILL_AND_RENDER.md +86 -0
- package/skills/easysociable-template/workflows/MCP_FIRST.md +9 -0
- package/skills/easysociable-template/workflows/OUT_OF_SCOPE.md +15 -0
- package/skills/easysociable-template/workflows/PALETTE_REVIEW.md +71 -0
- package/skills/easysociable-template/workflows/PUBLISH_FROM_DRAFT.md +60 -0
- package/skills/easysociable-template/workflows/SURFACE_REVIEW.md +178 -0
- package/skills/easysociable-template/workflows/THEME_FROM_BRAND.md +73 -0
- 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,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.
|