autocrew 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 (165) hide show
  1. package/HAMLETDEER.md +562 -0
  2. package/LICENSE +21 -0
  3. package/README.md +190 -0
  4. package/README_CN.md +190 -0
  5. package/adapters/openclaw/index.ts +68 -0
  6. package/bin/autocrew.mjs +23 -0
  7. package/bin/autocrew.ts +13 -0
  8. package/openclaw.plugin.json +36 -0
  9. package/package.json +74 -0
  10. package/skills/_writing-style/SKILL.md +68 -0
  11. package/skills/audience-profiler/SKILL.md +241 -0
  12. package/skills/content-attribution/SKILL.md +128 -0
  13. package/skills/content-review/SKILL.md +257 -0
  14. package/skills/cover-generator/SKILL.md +93 -0
  15. package/skills/humanizer-zh/SKILL.md +75 -0
  16. package/skills/intel-digest/SKILL.md +57 -0
  17. package/skills/intel-pull/SKILL.md +74 -0
  18. package/skills/manage-pipeline/SKILL.md +63 -0
  19. package/skills/memory-distill/SKILL.md +89 -0
  20. package/skills/onboarding/SKILL.md +117 -0
  21. package/skills/pipeline-status/SKILL.md +51 -0
  22. package/skills/platform-rewrite/SKILL.md +125 -0
  23. package/skills/pre-publish/SKILL.md +142 -0
  24. package/skills/publish-content/SKILL.md +500 -0
  25. package/skills/remix-content/SKILL.md +77 -0
  26. package/skills/research/SKILL.md +127 -0
  27. package/skills/setup/SKILL.md +353 -0
  28. package/skills/spawn-batch-writer/SKILL.md +66 -0
  29. package/skills/spawn-planner/SKILL.md +72 -0
  30. package/skills/spawn-writer/SKILL.md +60 -0
  31. package/skills/teardown/SKILL.md +144 -0
  32. package/skills/title-craft/SKILL.md +234 -0
  33. package/skills/topic-ideas/SKILL.md +105 -0
  34. package/skills/video-timeline/SKILL.md +117 -0
  35. package/skills/write-script/SKILL.md +232 -0
  36. package/skills/xhs-cover-review/SKILL.md +48 -0
  37. package/src/adapters/browser/browser-cdp.ts +260 -0
  38. package/src/adapters/browser/browser-relay.ts +236 -0
  39. package/src/adapters/browser/gateway-client.ts +148 -0
  40. package/src/adapters/browser/types.ts +36 -0
  41. package/src/adapters/image/gemini.ts +219 -0
  42. package/src/adapters/research/tikhub.ts +19 -0
  43. package/src/cli/banner.ts +18 -0
  44. package/src/cli/bootstrap.ts +33 -0
  45. package/src/cli/commands/adapt.ts +28 -0
  46. package/src/cli/commands/advance.ts +28 -0
  47. package/src/cli/commands/assets.ts +24 -0
  48. package/src/cli/commands/audit.ts +18 -0
  49. package/src/cli/commands/contents.ts +18 -0
  50. package/src/cli/commands/cover.ts +58 -0
  51. package/src/cli/commands/events.ts +17 -0
  52. package/src/cli/commands/humanize.ts +27 -0
  53. package/src/cli/commands/index.ts +80 -0
  54. package/src/cli/commands/init.ts +28 -0
  55. package/src/cli/commands/intel.ts +55 -0
  56. package/src/cli/commands/learn.ts +34 -0
  57. package/src/cli/commands/memory.ts +18 -0
  58. package/src/cli/commands/migrate.ts +24 -0
  59. package/src/cli/commands/open.ts +21 -0
  60. package/src/cli/commands/pipelines.ts +18 -0
  61. package/src/cli/commands/pre-publish.ts +27 -0
  62. package/src/cli/commands/profile.ts +31 -0
  63. package/src/cli/commands/research.ts +36 -0
  64. package/src/cli/commands/restore.ts +28 -0
  65. package/src/cli/commands/review.ts +61 -0
  66. package/src/cli/commands/start.ts +28 -0
  67. package/src/cli/commands/status.ts +14 -0
  68. package/src/cli/commands/templates.ts +15 -0
  69. package/src/cli/commands/topics.ts +18 -0
  70. package/src/cli/commands/trash.ts +28 -0
  71. package/src/cli/commands/upgrade.ts +48 -0
  72. package/src/cli/commands/versions.ts +24 -0
  73. package/src/cli/index.ts +40 -0
  74. package/src/data/sensitive-words-builtin.json +114 -0
  75. package/src/data/source-presets.yaml +54 -0
  76. package/src/e2e.test.ts +596 -0
  77. package/src/modules/auth/cookie-manager.ts +113 -0
  78. package/src/modules/cards/template-engine.ts +74 -0
  79. package/src/modules/cards/templates/comparison-table.ts +71 -0
  80. package/src/modules/cards/templates/data-chart.ts +76 -0
  81. package/src/modules/cards/templates/flow-chart.ts +49 -0
  82. package/src/modules/cards/templates/key-points.ts +59 -0
  83. package/src/modules/cover/prompt-builder.test.ts +157 -0
  84. package/src/modules/cover/prompt-builder.ts +212 -0
  85. package/src/modules/cover/ratio-adapter.test.ts +122 -0
  86. package/src/modules/cover/ratio-adapter.ts +104 -0
  87. package/src/modules/filter/sensitive-words.test.ts +72 -0
  88. package/src/modules/filter/sensitive-words.ts +212 -0
  89. package/src/modules/humanizer/zh.test.ts +75 -0
  90. package/src/modules/humanizer/zh.ts +175 -0
  91. package/src/modules/intel/collector.ts +19 -0
  92. package/src/modules/intel/collectors/competitor.test.ts +71 -0
  93. package/src/modules/intel/collectors/competitor.ts +65 -0
  94. package/src/modules/intel/collectors/rss.test.ts +56 -0
  95. package/src/modules/intel/collectors/rss.ts +70 -0
  96. package/src/modules/intel/collectors/trends.test.ts +80 -0
  97. package/src/modules/intel/collectors/trends.ts +107 -0
  98. package/src/modules/intel/collectors/web-search.test.ts +85 -0
  99. package/src/modules/intel/collectors/web-search.ts +81 -0
  100. package/src/modules/intel/integration.test.ts +203 -0
  101. package/src/modules/intel/intel-engine.test.ts +103 -0
  102. package/src/modules/intel/intel-engine.ts +96 -0
  103. package/src/modules/intel/source-config.test.ts +113 -0
  104. package/src/modules/intel/source-config.ts +131 -0
  105. package/src/modules/learnings/diff-tracker.test.ts +144 -0
  106. package/src/modules/learnings/diff-tracker.ts +189 -0
  107. package/src/modules/learnings/rule-distiller.ts +141 -0
  108. package/src/modules/memory/distill.ts +208 -0
  109. package/src/modules/migrate/legacy-migrate.test.ts +169 -0
  110. package/src/modules/migrate/legacy-migrate.ts +229 -0
  111. package/src/modules/pro/api-client.ts +192 -0
  112. package/src/modules/pro/gate.test.ts +110 -0
  113. package/src/modules/pro/gate.ts +104 -0
  114. package/src/modules/profile/creator-profile.test.ts +178 -0
  115. package/src/modules/profile/creator-profile.ts +248 -0
  116. package/src/modules/publish/douyin-api.ts +34 -0
  117. package/src/modules/publish/wechat-mp.ts +320 -0
  118. package/src/modules/publish/xiaohongshu-api.ts +127 -0
  119. package/src/modules/research/free-engine.ts +360 -0
  120. package/src/modules/timeline/markup-generator.ts +63 -0
  121. package/src/modules/timeline/parser.ts +275 -0
  122. package/src/modules/workflow/templates.ts +124 -0
  123. package/src/modules/writing/platform-rewrite.ts +190 -0
  124. package/src/modules/writing/title-hashtag.ts +385 -0
  125. package/src/runtime/context.test.ts +97 -0
  126. package/src/runtime/context.ts +129 -0
  127. package/src/runtime/events.test.ts +83 -0
  128. package/src/runtime/events.ts +104 -0
  129. package/src/runtime/hooks.ts +174 -0
  130. package/src/runtime/tool-runner.test.ts +204 -0
  131. package/src/runtime/tool-runner.ts +282 -0
  132. package/src/runtime/workflow-engine.test.ts +455 -0
  133. package/src/runtime/workflow-engine.ts +391 -0
  134. package/src/server/index.ts +409 -0
  135. package/src/server/start.ts +39 -0
  136. package/src/storage/local-store.test.ts +304 -0
  137. package/src/storage/local-store.ts +704 -0
  138. package/src/storage/pipeline-store.test.ts +363 -0
  139. package/src/storage/pipeline-store.ts +698 -0
  140. package/src/tools/asset.ts +96 -0
  141. package/src/tools/content-save.ts +276 -0
  142. package/src/tools/cover-review.ts +221 -0
  143. package/src/tools/humanize.ts +54 -0
  144. package/src/tools/init.ts +133 -0
  145. package/src/tools/intel.ts +92 -0
  146. package/src/tools/memory.ts +76 -0
  147. package/src/tools/pipeline-ops.ts +109 -0
  148. package/src/tools/pipeline.ts +168 -0
  149. package/src/tools/pre-publish.ts +232 -0
  150. package/src/tools/publish.ts +183 -0
  151. package/src/tools/registry.ts +198 -0
  152. package/src/tools/research.ts +304 -0
  153. package/src/tools/review.ts +305 -0
  154. package/src/tools/rewrite.ts +165 -0
  155. package/src/tools/status.ts +30 -0
  156. package/src/tools/timeline.ts +234 -0
  157. package/src/tools/topic-create.ts +50 -0
  158. package/src/types/providers.ts +69 -0
  159. package/src/types/timeline.test.ts +147 -0
  160. package/src/types/timeline.ts +83 -0
  161. package/src/utils/retry.test.ts +97 -0
  162. package/src/utils/retry.ts +85 -0
  163. package/templates/AGENTS.md +99 -0
  164. package/templates/SOUL.md +31 -0
  165. package/templates/TOOLS.md +76 -0
@@ -0,0 +1,165 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import { getContent, saveContent, updateContent } from "../storage/local-store.js";
3
+ import { adaptPlatformDraft, type SupportedPlatform } from "../modules/writing/platform-rewrite.js";
4
+ import { generateForPlatform } from "../modules/writing/title-hashtag.js";
5
+
6
+ export const rewriteSchema = Type.Object({
7
+ action: Type.Unsafe<"adapt_platform" | "batch_adapt">({
8
+ type: "string",
9
+ enum: ["adapt_platform", "batch_adapt"],
10
+ description:
11
+ "Action. 'adapt_platform' adapts to one platform, 'batch_adapt' adapts to multiple platforms at once.",
12
+ }),
13
+ content_id: Type.Optional(Type.String({ description: "Existing AutoCrew content id to adapt." })),
14
+ title: Type.Optional(Type.String({ description: "Source title when adapting raw text directly." })),
15
+ body: Type.Optional(Type.String({ description: "Source body when adapting raw text directly." })),
16
+ tags: Type.Optional(Type.Array(Type.String({ description: "Optional tags" }))),
17
+ target_platform: Type.Optional(
18
+ Type.Unsafe<"xiaohongshu" | "douyin" | "wechat_mp" | "wechat_video" | "bilibili">({
19
+ type: "string",
20
+ enum: ["xiaohongshu", "douyin", "wechat_mp", "wechat_video", "bilibili"],
21
+ description: "Target platform for adapt_platform action.",
22
+ }),
23
+ ),
24
+ target_platforms: Type.Optional(
25
+ Type.Array(
26
+ Type.Unsafe<"xiaohongshu" | "douyin" | "wechat_mp" | "wechat_video" | "bilibili">({
27
+ type: "string",
28
+ enum: ["xiaohongshu", "douyin", "wechat_mp", "wechat_video", "bilibili"],
29
+ }),
30
+ { description: "Target platforms for batch_adapt action." },
31
+ ),
32
+ ),
33
+ save_as_draft: Type.Optional(Type.Boolean({ description: "Save the adapted result as a new AutoCrew draft." })),
34
+ });
35
+
36
+ /**
37
+ * Resolve source content from params (content_id or raw title+body).
38
+ */
39
+ async function resolveSource(params: Record<string, unknown>) {
40
+ const dataDir = (params._dataDir as string) || undefined;
41
+ let title = (params.title as string) || "";
42
+ let body = (params.body as string) || "";
43
+ let tags = (params.tags as string[]) || [];
44
+ const contentId = params.content_id as string | undefined;
45
+ let topicId: string | undefined;
46
+
47
+ if (contentId) {
48
+ const content = await getContent(contentId, dataDir);
49
+ if (!content) return { ok: false as const, error: `Content ${contentId} not found` };
50
+ title = content.title;
51
+ body = content.body;
52
+ tags = content.tags || tags;
53
+ topicId = content.topicId;
54
+ }
55
+
56
+ if (!title || !body) return { ok: false as const, error: "content_id or title + body is required" };
57
+
58
+ return { ok: true as const, title, body, tags, contentId, topicId, dataDir };
59
+ }
60
+
61
+ /**
62
+ * Adapt a single platform: rewrite + generate title/hashtag + optionally save.
63
+ */
64
+ async function adaptOne(
65
+ title: string,
66
+ body: string,
67
+ tags: string[],
68
+ platform: SupportedPlatform,
69
+ opts: { saveAsDraft?: boolean; topicId?: string; siblingIds?: string[]; dataDir?: string },
70
+ ) {
71
+ const adapted = adaptPlatformDraft({ title, body, tags, targetPlatform: platform });
72
+
73
+ // Generate title variants + hashtags
74
+ const titleResult = generateForPlatform(adapted.title, platform, { tags });
75
+ const hashtags = titleResult.hashtags.map((h) => h.tag);
76
+
77
+ const result: Record<string, unknown> = {
78
+ ...adapted,
79
+ titleVariants: titleResult.titles,
80
+ hashtags,
81
+ };
82
+
83
+ if (opts.saveAsDraft) {
84
+ const saved = await saveContent(
85
+ {
86
+ title: adapted.title,
87
+ body: adapted.body,
88
+ platform: adapted.platform,
89
+ status: "draft",
90
+ tags,
91
+ hashtags,
92
+ topicId: opts.topicId,
93
+ siblings: opts.siblingIds || [],
94
+ } as any,
95
+ opts.dataDir,
96
+ );
97
+ result.content = saved;
98
+ }
99
+
100
+ return result;
101
+ }
102
+
103
+ export async function executeRewrite(params: Record<string, unknown>) {
104
+ const action = params.action as string;
105
+
106
+ // --- adapt_platform (single) ---
107
+ if (action === "adapt_platform") {
108
+ const src = await resolveSource(params);
109
+ if (!src.ok) return src;
110
+
111
+ const platform = params.target_platform as SupportedPlatform;
112
+ if (!platform) return { ok: false, error: "target_platform is required for adapt_platform" };
113
+
114
+ return adaptOne(src.title, src.body, src.tags, platform, {
115
+ saveAsDraft: Boolean(params.save_as_draft),
116
+ topicId: src.topicId,
117
+ dataDir: src.dataDir,
118
+ });
119
+ }
120
+
121
+ // --- batch_adapt (multiple platforms) ---
122
+ if (action === "batch_adapt") {
123
+ const src = await resolveSource(params);
124
+ if (!src.ok) return src;
125
+
126
+ const platforms = params.target_platforms as SupportedPlatform[] | undefined;
127
+ if (!platforms || platforms.length === 0) {
128
+ return { ok: false, error: "target_platforms is required for batch_adapt" };
129
+ }
130
+
131
+ const results: Record<string, unknown>[] = [];
132
+ const savedIds: string[] = [];
133
+
134
+ for (const platform of platforms) {
135
+ const result = await adaptOne(src.title, src.body, src.tags, platform, {
136
+ saveAsDraft: Boolean(params.save_as_draft),
137
+ topicId: src.topicId,
138
+ dataDir: src.dataDir,
139
+ });
140
+ results.push(result);
141
+ const savedContent = result.content as { id: string } | undefined;
142
+ if (savedContent?.id) savedIds.push(savedContent.id);
143
+ }
144
+
145
+ // Build sibling relationships among all saved drafts (+ source if it exists)
146
+ if (params.save_as_draft && savedIds.length > 1) {
147
+ const allIds = src.contentId ? [src.contentId, ...savedIds] : savedIds;
148
+ for (const id of allIds) {
149
+ const siblingIds = allIds.filter((s) => s !== id);
150
+ await updateContent(id, { siblings: siblingIds }, src.dataDir);
151
+ }
152
+ }
153
+
154
+ return {
155
+ ok: true,
156
+ action: "batch_adapt",
157
+ sourceContentId: src.contentId || null,
158
+ platforms: platforms,
159
+ results,
160
+ siblingIds: savedIds,
161
+ };
162
+ }
163
+
164
+ return { ok: false, error: `Unknown action: ${action}` };
165
+ }
@@ -0,0 +1,30 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import { listTopics, listContents } from "../storage/local-store.js";
3
+
4
+ export const statusSchema = Type.Object({
5
+ verbose: Type.Optional(Type.Boolean({ description: "Show detailed counts" })),
6
+ });
7
+
8
+ export async function executeStatus(params: Record<string, unknown>) {
9
+ const dataDir = (params._dataDir as string) || undefined;
10
+
11
+ const topics = await listTopics(dataDir);
12
+ const contents = await listContents(dataDir);
13
+
14
+ const byStatus = {
15
+ draft: contents.filter((c) => c.status === "draft").length,
16
+ review: contents.filter((c) => c.status === "review").length,
17
+ approved: contents.filter((c) => c.status === "approved").length,
18
+ published: contents.filter((c) => c.status === "published").length,
19
+ };
20
+
21
+ return {
22
+ ok: true,
23
+ version: "0.1.0",
24
+ topics: topics.length,
25
+ contents: contents.length,
26
+ contentsByStatus: byStatus,
27
+ latestTopic: topics[0]?.title || null,
28
+ latestContent: contents[0]?.title || null,
29
+ };
30
+ }
@@ -0,0 +1,234 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { parseMarkedScript } from "../modules/timeline/parser.js";
5
+ import type {
6
+ VideoPreset,
7
+ AspectRatio,
8
+ SegmentStatus,
9
+ Timeline,
10
+ } from "../types/timeline.js";
11
+
12
+ export const timelineSchema = Type.Object({
13
+ action: Type.Unsafe<"generate" | "get" | "update_segment" | "confirm_all">({
14
+ type: "string",
15
+ enum: ["generate", "get", "update_segment", "confirm_all"],
16
+ description:
17
+ "generate: parse marked script into timeline.json. get: retrieve timeline. update_segment: update segment status/asset. confirm_all: confirm all ready segments.",
18
+ }),
19
+ content_id: Type.String({ description: "Content project ID" }),
20
+ preset: Type.Optional(
21
+ Type.Unsafe<VideoPreset>({
22
+ type: "string",
23
+ enum: ["knowledge-explainer", "tutorial"],
24
+ }),
25
+ ),
26
+ aspect_ratio: Type.Optional(
27
+ Type.Unsafe<AspectRatio>({
28
+ type: "string",
29
+ enum: ["9:16", "16:9", "3:4", "1:1", "4:3"],
30
+ }),
31
+ ),
32
+ segment_id: Type.Optional(
33
+ Type.String({ description: "Segment ID for update_segment" }),
34
+ ),
35
+ status: Type.Optional(
36
+ Type.Unsafe<SegmentStatus>({
37
+ type: "string",
38
+ enum: ["pending", "generating", "ready", "confirmed", "failed"],
39
+ }),
40
+ ),
41
+ asset_path: Type.Optional(
42
+ Type.String({ description: "Path to generated asset file" }),
43
+ ),
44
+ _dataDir: Type.Optional(Type.String()),
45
+ });
46
+
47
+ function contentDir(dataDir: string, contentId: string): string {
48
+ return join(dataDir, "contents", contentId);
49
+ }
50
+
51
+ function timelinePath(dataDir: string, contentId: string): string {
52
+ return join(contentDir(dataDir, contentId), "timeline.json");
53
+ }
54
+
55
+ async function loadTimeline(
56
+ dataDir: string,
57
+ contentId: string,
58
+ ): Promise<Timeline | null> {
59
+ try {
60
+ const raw = await readFile(timelinePath(dataDir, contentId), "utf-8");
61
+ return JSON.parse(raw) as Timeline;
62
+ } catch {
63
+ return null;
64
+ }
65
+ }
66
+
67
+ async function saveTimeline(
68
+ dataDir: string,
69
+ contentId: string,
70
+ timeline: Timeline,
71
+ ): Promise<void> {
72
+ const dir = contentDir(dataDir, contentId);
73
+ await mkdir(dir, { recursive: true });
74
+ await writeFile(
75
+ timelinePath(dataDir, contentId),
76
+ JSON.stringify(timeline, null, 2),
77
+ "utf-8",
78
+ );
79
+ }
80
+
81
+ export async function executeTimeline(
82
+ params: Record<string, unknown>,
83
+ ): Promise<Record<string, unknown>> {
84
+ const action = params.action as string;
85
+ const contentId = params.content_id as string;
86
+ const dataDir = (params._dataDir as string) || "";
87
+
88
+ if (!contentId) {
89
+ return { ok: false, error: "content_id is required" };
90
+ }
91
+
92
+ if (action === "generate") {
93
+ const preset = (params.preset as VideoPreset) || "knowledge-explainer";
94
+ const aspectRatio = (params.aspect_ratio as AspectRatio) || "9:16";
95
+
96
+ let script: string;
97
+ try {
98
+ script = await readFile(
99
+ join(contentDir(dataDir, contentId), "draft.md"),
100
+ "utf-8",
101
+ );
102
+ } catch {
103
+ return {
104
+ ok: false,
105
+ error: `draft.md not found for content ${contentId}`,
106
+ };
107
+ }
108
+
109
+ const timeline = parseMarkedScript(script, {
110
+ contentId,
111
+ preset,
112
+ aspectRatio,
113
+ });
114
+
115
+ await saveTimeline(dataDir, contentId, timeline);
116
+
117
+ return {
118
+ ok: true,
119
+ content_id: contentId,
120
+ tts_count: timeline.tracks.tts.length,
121
+ visual_count: timeline.tracks.visual.length,
122
+ timeline,
123
+ };
124
+ }
125
+
126
+ if (action === "get") {
127
+ const timeline = await loadTimeline(dataDir, contentId);
128
+ if (!timeline) {
129
+ return {
130
+ ok: false,
131
+ error: `timeline.json not found for content ${contentId}`,
132
+ };
133
+ }
134
+ return { ok: true, content_id: contentId, timeline };
135
+ }
136
+
137
+ if (action === "update_segment") {
138
+ const segmentId = params.segment_id as string;
139
+ if (!segmentId) {
140
+ return { ok: false, error: "segment_id is required for update_segment" };
141
+ }
142
+
143
+ const timeline = await loadTimeline(dataDir, contentId);
144
+ if (!timeline) {
145
+ return {
146
+ ok: false,
147
+ error: `timeline.json not found for content ${contentId}`,
148
+ };
149
+ }
150
+
151
+ const newStatus = params.status as SegmentStatus | undefined;
152
+ const assetPath = params.asset_path as string | undefined;
153
+
154
+ // Search across all track arrays
155
+ let found = false;
156
+
157
+ const newText = params.text as string | undefined;
158
+
159
+ for (const seg of timeline.tracks.tts) {
160
+ if (seg.id === segmentId) {
161
+ if (newStatus) seg.status = newStatus;
162
+ if (assetPath !== undefined) seg.asset = assetPath;
163
+ if (newText !== undefined) seg.text = newText;
164
+ found = true;
165
+ break;
166
+ }
167
+ }
168
+
169
+ if (!found) {
170
+ for (const seg of timeline.tracks.visual) {
171
+ if (seg.id === segmentId) {
172
+ if (newStatus) seg.status = newStatus;
173
+ if (assetPath !== undefined) seg.asset = assetPath;
174
+ found = true;
175
+ break;
176
+ }
177
+ }
178
+ }
179
+
180
+ if (!found) {
181
+ return { ok: false, error: `Segment ${segmentId} not found` };
182
+ }
183
+
184
+ await saveTimeline(dataDir, contentId, timeline);
185
+
186
+ // Sync text edits back to draft.md so regenerating timeline won't lose changes
187
+ if (newText !== undefined) {
188
+ const draftText = timeline.tracks.tts.map((s) => s.text).join("\n\n");
189
+ await writeFile(
190
+ join(contentDir(dataDir, contentId), "draft.md"),
191
+ draftText,
192
+ "utf-8",
193
+ );
194
+ }
195
+
196
+ return { ok: true, content_id: contentId, segment_id: segmentId };
197
+ }
198
+
199
+ if (action === "confirm_all") {
200
+ const timeline = await loadTimeline(dataDir, contentId);
201
+ if (!timeline) {
202
+ return {
203
+ ok: false,
204
+ error: `timeline.json not found for content ${contentId}`,
205
+ };
206
+ }
207
+
208
+ let confirmed = 0;
209
+
210
+ for (const seg of timeline.tracks.tts) {
211
+ if (seg.status === "ready") {
212
+ seg.status = "confirmed";
213
+ confirmed++;
214
+ }
215
+ }
216
+
217
+ for (const seg of timeline.tracks.visual) {
218
+ if (seg.status === "ready") {
219
+ seg.status = "confirmed";
220
+ confirmed++;
221
+ }
222
+ }
223
+
224
+ if (timeline.tracks.subtitle.status === "ready") {
225
+ timeline.tracks.subtitle.status = "confirmed";
226
+ confirmed++;
227
+ }
228
+
229
+ await saveTimeline(dataDir, contentId, timeline);
230
+ return { ok: true, content_id: contentId, confirmed_count: confirmed };
231
+ }
232
+
233
+ return { ok: false, error: `Unknown action: ${action}` };
234
+ }
@@ -0,0 +1,50 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import { saveTopic, listTopics } from "../storage/local-store.js";
3
+
4
+ /**
5
+ * Core tool logic — platform-agnostic.
6
+ * Wrapped by index.ts (OpenClaw) and mcp/server.ts (Claude Code).
7
+ */
8
+
9
+ export const topicCreateSchema = Type.Object({
10
+ action: Type.Unsafe<"create" | "list">({
11
+ type: "string",
12
+ enum: ["create", "list"],
13
+ description: "Action: 'create' to save a new topic, 'list' to show all topics.",
14
+ }),
15
+ title: Type.Optional(Type.String({ description: "Topic title (required for create)" })),
16
+ description: Type.Optional(Type.String({ description: "Topic description (required for create)" })),
17
+ tags: Type.Optional(Type.Array(Type.String(), { description: "Topic tags (required for create)" })),
18
+ source: Type.Optional(Type.String({ description: "Where this topic idea came from" })),
19
+ });
20
+
21
+ export async function executeTopicCreate(params: Record<string, unknown>) {
22
+ const action = (params.action as string) || "create";
23
+ const dataDir = (params._dataDir as string) || undefined;
24
+
25
+ if (action === "list") {
26
+ const topics = await listTopics(dataDir);
27
+ if (topics.length === 0) {
28
+ return { ok: true, message: "No topics yet.", topics: [] };
29
+ }
30
+ return { ok: true, topics };
31
+ }
32
+
33
+ // create
34
+ const title = params.title as string;
35
+ const description = params.description as string;
36
+ const tags = (params.tags as string[]) || [];
37
+
38
+ if (!title || !description) {
39
+ return { ok: false, error: "title and description are required for create" };
40
+ }
41
+
42
+ const topic = await saveTopic({
43
+ title,
44
+ description,
45
+ tags,
46
+ source: (params.source as string) || undefined,
47
+ }, dataDir);
48
+
49
+ return { ok: true, topic };
50
+ }
@@ -0,0 +1,69 @@
1
+ import type { AspectRatio, Timeline } from "./timeline.js";
2
+
3
+ export interface Voice {
4
+ id: string;
5
+ name: string;
6
+ language: string;
7
+ }
8
+
9
+ export interface VoiceConfig {
10
+ voiceId: string;
11
+ speed?: number;
12
+ pitch?: number;
13
+ }
14
+
15
+ export interface AudioAsset {
16
+ path: string;
17
+ duration: number;
18
+ format: "mp3" | "wav" | "ogg";
19
+ }
20
+
21
+ export interface VideoConfig {
22
+ aspectRatio: AspectRatio;
23
+ duration: number;
24
+ style?: string;
25
+ }
26
+
27
+ export interface VideoAsset {
28
+ path: string;
29
+ duration: number;
30
+ format: "mp4" | "webm" | "mov";
31
+ }
32
+
33
+ export interface VideoFile {
34
+ path: string;
35
+ duration: number;
36
+ format: string;
37
+ }
38
+
39
+ export interface ProjectFile {
40
+ path: string;
41
+ format: ExportFormat;
42
+ }
43
+
44
+ export type ExportFormat = "jianying" | "davinci" | "fcpx";
45
+ export type AssetMap = Record<string, string>;
46
+
47
+ export interface TTSProvider {
48
+ name: string;
49
+ generate(text: string, voice: VoiceConfig): Promise<AudioAsset>;
50
+ estimateDuration(text: string): number;
51
+ listVoices(): Promise<Voice[]>;
52
+ }
53
+
54
+ export interface VideoProvider {
55
+ name: string;
56
+ generate(prompt: string, config: VideoConfig): Promise<VideoAsset>;
57
+ supportedRatios(): AspectRatio[];
58
+ }
59
+
60
+ export interface CompositorProvider {
61
+ name: string;
62
+ compose(timeline: Timeline, assets: AssetMap): Promise<VideoFile>;
63
+ export(
64
+ timeline: Timeline,
65
+ assets: AssetMap,
66
+ format: ExportFormat,
67
+ ): Promise<ProjectFile>;
68
+ supportedFormats(): ExportFormat[];
69
+ }
@@ -0,0 +1,147 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import type {
3
+ Timeline,
4
+ TTSSegment,
5
+ VisualSegment,
6
+ SubtitleTrack,
7
+ SubtitleConfig,
8
+ SegmentStatus,
9
+ VideoPreset,
10
+ AspectRatio,
11
+ SubtitleTemplate,
12
+ SubtitlePosition,
13
+ VisualType,
14
+ CardTemplate,
15
+ } from "./timeline.js";
16
+
17
+ describe("Timeline types", () => {
18
+ it("creates a valid Timeline with all fields", () => {
19
+ const tts: TTSSegment = {
20
+ id: "tts-001",
21
+ text: "Welcome to the tutorial",
22
+ estimatedDuration: 3.5,
23
+ start: 0,
24
+ asset: null,
25
+ status: "pending",
26
+ };
27
+
28
+ const visual: VisualSegment = {
29
+ id: "vis-001",
30
+ layer: 1,
31
+ type: "broll",
32
+ prompt: "city skyline at sunset",
33
+ linkedTts: ["tts-001"],
34
+ asset: null,
35
+ status: "pending",
36
+ };
37
+
38
+ const subtitle: SubtitleTrack = {
39
+ asset: null,
40
+ status: "pending",
41
+ };
42
+
43
+ const timeline: Timeline = {
44
+ version: "2.0",
45
+ contentId: "content-abc",
46
+ preset: "knowledge-explainer",
47
+ aspectRatio: "9:16",
48
+ subtitle: {
49
+ template: "modern-outline",
50
+ position: "bottom",
51
+ },
52
+ tracks: {
53
+ tts: [tts],
54
+ visual: [visual],
55
+ subtitle,
56
+ },
57
+ };
58
+
59
+ expect(timeline.version).toBe("2.0");
60
+ expect(timeline.contentId).toBe("content-abc");
61
+ expect(timeline.preset).toBe("knowledge-explainer");
62
+ expect(timeline.aspectRatio).toBe("9:16");
63
+ expect(timeline.subtitle.template).toBe("modern-outline");
64
+ expect(timeline.subtitle.position).toBe("bottom");
65
+ expect(timeline.tracks.tts).toHaveLength(1);
66
+ expect(timeline.tracks.visual).toHaveLength(1);
67
+ expect(timeline.tracks.subtitle.status).toBe("pending");
68
+ });
69
+
70
+ it("creates a card visual with template, data, and opacity", () => {
71
+ const card: VisualSegment = {
72
+ id: "vis-card-001",
73
+ layer: 2,
74
+ type: "card",
75
+ template: "comparison-table",
76
+ data: {
77
+ headers: ["Feature", "Plan A", "Plan B"],
78
+ rows: [["Price", "$10", "$20"]],
79
+ },
80
+ linkedTts: ["tts-001", "tts-002"],
81
+ opacity: 0.85,
82
+ asset: "cards/comparison.png",
83
+ status: "ready",
84
+ };
85
+
86
+ expect(card.type).toBe("card");
87
+ expect(card.template).toBe("comparison-table");
88
+ expect(card.data).toBeDefined();
89
+ expect(card.opacity).toBe(0.85);
90
+ expect(card.asset).toBe("cards/comparison.png");
91
+ expect(card.linkedTts).toEqual(["tts-001", "tts-002"]);
92
+ });
93
+
94
+ it("covers all valid SegmentStatus values", () => {
95
+ const statuses: SegmentStatus[] = [
96
+ "pending",
97
+ "generating",
98
+ "ready",
99
+ "confirmed",
100
+ "failed",
101
+ ];
102
+
103
+ expect(statuses).toHaveLength(5);
104
+
105
+ for (const status of statuses) {
106
+ const segment: TTSSegment = {
107
+ id: "tts-test",
108
+ text: "test",
109
+ estimatedDuration: 1,
110
+ start: 0,
111
+ asset: null,
112
+ status,
113
+ };
114
+ expect(segment.status).toBe(status);
115
+ }
116
+ });
117
+
118
+ it("covers all VideoPreset values", () => {
119
+ const presets: VideoPreset[] = ["knowledge-explainer", "tutorial"];
120
+ expect(presets).toHaveLength(2);
121
+ });
122
+
123
+ it("covers all AspectRatio values", () => {
124
+ const ratios: AspectRatio[] = ["9:16", "16:9", "3:4", "1:1", "4:3"];
125
+ expect(ratios).toHaveLength(5);
126
+ });
127
+
128
+ it("covers all SubtitleTemplate values", () => {
129
+ const templates: SubtitleTemplate[] = [
130
+ "modern-outline",
131
+ "karaoke-highlight",
132
+ "minimal-fade",
133
+ "bold-top",
134
+ ];
135
+ expect(templates).toHaveLength(4);
136
+ });
137
+
138
+ it("covers all CardTemplate values", () => {
139
+ const templates: CardTemplate[] = [
140
+ "comparison-table",
141
+ "key-points",
142
+ "flow-chart",
143
+ "data-chart",
144
+ ];
145
+ expect(templates).toHaveLength(4);
146
+ });
147
+ });