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,304 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import fs from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import {
6
+ saveTopic,
7
+ listTopics,
8
+ getTopic,
9
+ saveContent,
10
+ listContents,
11
+ getContent,
12
+ updateContent,
13
+ addAsset,
14
+ listAssets,
15
+ removeAsset,
16
+ listVersions,
17
+ getVersion,
18
+ revertToVersion,
19
+ saveCoverReview,
20
+ getCoverReview,
21
+ approveCoverVariant,
22
+ transitionStatus,
23
+ getAllowedTransitions,
24
+ createPlatformVariant,
25
+ listSiblings,
26
+ normalizeLegacyStatus,
27
+ type CoverReview,
28
+ } from "../storage/local-store.js";
29
+
30
+ let testDir: string;
31
+
32
+ beforeEach(async () => {
33
+ testDir = await fs.mkdtemp(path.join(os.tmpdir(), "autocrew-store-test-"));
34
+ });
35
+
36
+ afterEach(async () => {
37
+ await fs.rm(testDir, { recursive: true, force: true });
38
+ });
39
+
40
+ // --- Topics ---
41
+
42
+ describe("Topics", () => {
43
+ it("saveTopic creates a topic with id and timestamp", async () => {
44
+ const topic = await saveTopic({ title: "AI赚钱", description: "测试", tags: ["ai"] }, testDir);
45
+ expect(topic.id).toBeTruthy();
46
+ expect(topic.title).toBe("AI赚钱");
47
+ expect(topic.createdAt).toBeTruthy();
48
+ });
49
+
50
+ it("listTopics returns saved topics", async () => {
51
+ await saveTopic({ title: "T1", description: "d1", tags: [] }, testDir);
52
+ await saveTopic({ title: "T2", description: "d2", tags: [] }, testDir);
53
+ const topics = await listTopics(testDir);
54
+ expect(topics).toHaveLength(2);
55
+ });
56
+
57
+ it("getTopic retrieves by id", async () => {
58
+ const saved = await saveTopic({ title: "Find me", description: "d", tags: [] }, testDir);
59
+ const found = await getTopic(saved.id, testDir);
60
+ expect(found).not.toBeNull();
61
+ expect(found!.title).toBe("Find me");
62
+ });
63
+
64
+ it("getTopic returns null for nonexistent id", async () => {
65
+ const found = await getTopic("nonexistent", testDir);
66
+ expect(found).toBeNull();
67
+ });
68
+
69
+ it("listTopics returns empty array when no topics", async () => {
70
+ const topics = await listTopics(testDir);
71
+ expect(topics).toEqual([]);
72
+ });
73
+ });
74
+
75
+ // --- Content ---
76
+
77
+ describe("Content", () => {
78
+ it("saveContent creates content with id and defaults", async () => {
79
+ const content = await saveContent(
80
+ { title: "Test", body: "Hello world", tags: ["test"], status: "draft_ready" },
81
+ testDir,
82
+ );
83
+ expect(content.id).toBeTruthy();
84
+ expect(content.status).toBe("draft_ready");
85
+ expect(content.siblings).toEqual([]);
86
+ expect(content.versions.length).toBeGreaterThanOrEqual(1);
87
+ });
88
+
89
+ it("listContents returns saved content", async () => {
90
+ await saveContent({ title: "C1", body: "b1", tags: [], status: "draft_ready" }, testDir);
91
+ await saveContent({ title: "C2", body: "b2", tags: [], status: "draft_ready" }, testDir);
92
+ const items = await listContents(testDir);
93
+ expect(items).toHaveLength(2);
94
+ });
95
+
96
+ it("getContent retrieves by id", async () => {
97
+ const saved = await saveContent({ title: "Find", body: "me", tags: [], status: "draft_ready" }, testDir);
98
+ const found = await getContent(saved.id, testDir);
99
+ expect(found).not.toBeNull();
100
+ expect(found!.title).toBe("Find");
101
+ });
102
+
103
+ it("getContent returns null for nonexistent", async () => {
104
+ const found = await getContent("nope", testDir);
105
+ expect(found).toBeNull();
106
+ });
107
+
108
+ it("updateContent merges fields", async () => {
109
+ const saved = await saveContent({ title: "Original", body: "body", tags: [], status: "draft_ready" }, testDir);
110
+ const updated = await updateContent(saved.id, { title: "Updated" }, testDir);
111
+ expect(updated).not.toBeNull();
112
+ expect(updated!.title).toBe("Updated");
113
+ expect(updated!.body).toBe("body");
114
+ });
115
+
116
+ it("updateContent creates a new version when body changes", async () => {
117
+ const saved = await saveContent({ title: "V", body: "v1 body", tags: [], status: "draft_ready" }, testDir);
118
+ await updateContent(saved.id, { body: "v2 body" }, testDir);
119
+ const content = await getContent(saved.id, testDir);
120
+ expect(content!.versions.length).toBeGreaterThanOrEqual(2);
121
+ });
122
+
123
+ it("updateContent returns null for nonexistent", async () => {
124
+ const result = await updateContent("nope", { title: "x" }, testDir);
125
+ expect(result).toBeNull();
126
+ });
127
+ });
128
+
129
+ // --- Assets ---
130
+
131
+ describe("Assets", () => {
132
+ it("addAsset adds to content assets list", async () => {
133
+ const content = await saveContent({ title: "A", body: "b", tags: [], status: "draft_ready" }, testDir);
134
+ const dummyFile = path.join(testDir, "test-image.jpg");
135
+ await fs.writeFile(dummyFile, "fake image data");
136
+
137
+ const result = await addAsset(
138
+ content.id,
139
+ { filename: "test-image.jpg", type: "cover", sourcePath: dummyFile },
140
+ testDir,
141
+ );
142
+ expect(result.ok).toBe(true);
143
+
144
+ const assets = await listAssets(content.id, testDir);
145
+ expect(assets.length).toBeGreaterThanOrEqual(1);
146
+ expect(assets.some((a) => a.type === "cover")).toBe(true);
147
+ });
148
+
149
+ it("listAssets returns empty for content with no assets", async () => {
150
+ const content = await saveContent({ title: "A", body: "b", tags: [], status: "draft_ready" }, testDir);
151
+ const assets = await listAssets(content.id, testDir);
152
+ expect(assets).toEqual([]);
153
+ });
154
+
155
+ it("removeAsset removes from list", async () => {
156
+ const content = await saveContent({ title: "A", body: "b", tags: [], status: "draft_ready" }, testDir);
157
+ const dummyFile = path.join(testDir, "remove-me.png");
158
+ await fs.writeFile(dummyFile, "data");
159
+ await addAsset(content.id, { filename: "remove-me.png", type: "image", sourcePath: dummyFile }, testDir);
160
+
161
+ const removed = await removeAsset(content.id, "remove-me.png", testDir);
162
+ expect(removed).toBe(true);
163
+
164
+ const assets = await listAssets(content.id, testDir);
165
+ expect(assets.some((a) => a.filename === "remove-me.png")).toBe(false);
166
+ });
167
+ });
168
+
169
+ // --- Versions ---
170
+
171
+ describe("Versions", () => {
172
+ it("listVersions returns version history", async () => {
173
+ const content = await saveContent({ title: "V", body: "v1", tags: [], status: "draft_ready" }, testDir);
174
+ const versions = await listVersions(content.id, testDir);
175
+ expect(versions.length).toBeGreaterThanOrEqual(1);
176
+ });
177
+
178
+ it("getVersion retrieves specific version body", async () => {
179
+ const content = await saveContent({ title: "V", body: "original body", tags: [], status: "draft_ready" }, testDir);
180
+ const body = await getVersion(content.id, 1, testDir);
181
+ expect(body).toBe("original body");
182
+ });
183
+
184
+ it("revertToVersion restores old body", async () => {
185
+ const content = await saveContent({ title: "V", body: "v1 body", tags: [], status: "draft_ready" }, testDir);
186
+ await updateContent(content.id, { body: "v2 body" }, testDir);
187
+
188
+ const reverted = await revertToVersion(content.id, 1, testDir);
189
+ expect(reverted).not.toBeNull();
190
+ expect(reverted!.body).toBe("v1 body");
191
+ });
192
+ });
193
+
194
+ // --- Cover Review ---
195
+
196
+ describe("Cover Review", () => {
197
+ it("saveCoverReview + getCoverReview round-trip", async () => {
198
+ const content = await saveContent({ title: "C", body: "b", tags: [], status: "draft_ready" }, testDir);
199
+ const review: CoverReview = {
200
+ platform: "xhs",
201
+ status: "review_pending",
202
+ variants: [{ label: "a", style: "cinematic", imagePaths: { "3:4": "/tmp/a.png" } }],
203
+ };
204
+ await saveCoverReview(content.id, review, testDir);
205
+
206
+ const loaded = await getCoverReview(content.id, testDir);
207
+ expect(loaded).not.toBeNull();
208
+ expect(loaded!.platform).toBe("xhs");
209
+ expect(loaded!.variants).toHaveLength(1);
210
+ });
211
+
212
+ it("approveCoverVariant sets approvedLabel and status", async () => {
213
+ const content = await saveContent({ title: "C", body: "b", tags: [], status: "draft_ready" }, testDir);
214
+ const review: CoverReview = {
215
+ platform: "xhs",
216
+ status: "review_pending",
217
+ variants: [
218
+ { label: "a", imagePaths: {}, imagePath: "/tmp/a.png" },
219
+ { label: "b", imagePaths: {}, imagePath: "/tmp/b.png" },
220
+ ],
221
+ };
222
+ await saveCoverReview(content.id, review, testDir);
223
+
224
+ const approved = await approveCoverVariant(content.id, "b", testDir);
225
+ expect(approved).not.toBeNull();
226
+ expect(approved!.approvedLabel).toBe("b");
227
+ // approveCoverVariant sets review.status to "publish_ready"
228
+ expect(approved!.status).toBe("publish_ready");
229
+ });
230
+
231
+ it("getCoverReview returns null when no review exists", async () => {
232
+ const content = await saveContent({ title: "C", body: "b", tags: [], status: "draft_ready" }, testDir);
233
+ const review = await getCoverReview(content.id, testDir);
234
+ expect(review).toBeNull();
235
+ });
236
+ });
237
+
238
+ // --- Status Transitions ---
239
+
240
+ describe("Status Transitions", () => {
241
+ it("transitionStatus moves to allowed state", async () => {
242
+ const content = await saveContent({ title: "T", body: "b", tags: [], status: "draft_ready" }, testDir);
243
+ // draft_ready → reviewing is allowed
244
+ const result = await transitionStatus(content.id, "reviewing", undefined, testDir);
245
+ expect(result.ok).toBe(true);
246
+ const updated = await getContent(content.id, testDir);
247
+ expect(updated!.status).toBe("reviewing");
248
+ });
249
+
250
+ it("transitionStatus rejects invalid transition", async () => {
251
+ const content = await saveContent({ title: "T", body: "b", tags: [], status: "draft_ready" }, testDir);
252
+ // draft_ready → published is not allowed directly
253
+ const result = await transitionStatus(content.id, "published", undefined, testDir);
254
+ expect(result.ok).toBe(false);
255
+ expect(result.error).toBeTruthy();
256
+ });
257
+
258
+ it("getAllowedTransitions returns valid next states", () => {
259
+ const allowed = getAllowedTransitions("draft_ready");
260
+ expect(allowed).toContain("reviewing");
261
+ expect(allowed.length).toBeGreaterThan(0);
262
+ });
263
+
264
+ it("normalizeLegacyStatus maps old status names", () => {
265
+ expect(normalizeLegacyStatus("draft")).toBe("draft_ready");
266
+ expect(normalizeLegacyStatus("review")).toBe("reviewing");
267
+ expect(normalizeLegacyStatus("approved")).toBe("approved");
268
+ });
269
+ });
270
+
271
+ // --- Platform Variants + Siblings ---
272
+
273
+ describe("Platform Variants & Siblings", () => {
274
+ it("createPlatformVariant creates a sibling content from topic", async () => {
275
+ // createPlatformVariant takes topicId, not contentId
276
+ const topic = await saveTopic({ title: "Original", description: "desc", tags: [] }, testDir);
277
+ const result = await createPlatformVariant(topic.id, "douyin", undefined, testDir);
278
+ expect(result.ok).toBe(true);
279
+ expect(result.content).toBeDefined();
280
+ expect(result.content!.platform).toBe("douyin");
281
+ });
282
+
283
+ it("siblings are linked when multiple variants exist", async () => {
284
+ const topic = await saveTopic({ title: "O", description: "d", tags: [] }, testDir);
285
+ // Create first variant
286
+ const r1 = await createPlatformVariant(topic.id, "xhs", undefined, testDir);
287
+ expect(r1.ok).toBe(true);
288
+ // Create second variant
289
+ const r2 = await createPlatformVariant(topic.id, "douyin", undefined, testDir);
290
+ expect(r2.ok).toBe(true);
291
+
292
+ // Both should be siblings of each other
293
+ const siblings1 = await listSiblings(r1.content!.id, testDir);
294
+ expect(siblings1.some((s) => s.id === r2.content!.id)).toBe(true);
295
+
296
+ const siblings2 = await listSiblings(r2.content!.id, testDir);
297
+ expect(siblings2.some((s) => s.id === r1.content!.id)).toBe(true);
298
+ });
299
+
300
+ it("createPlatformVariant returns error for nonexistent topic", async () => {
301
+ const result = await createPlatformVariant("nope", "douyin", undefined, testDir);
302
+ expect(result.ok).toBe(false);
303
+ });
304
+ });