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,133 @@
1
+ /**
2
+ * AutoCrew Init — Initialize the ~/.autocrew/ data directory.
3
+ *
4
+ * Creates the directory structure and empty creator-profile.json.
5
+ * Safe to run multiple times (idempotent).
6
+ */
7
+ import fs from "node:fs/promises";
8
+ import path from "node:path";
9
+ import { initProfile, detectMissingInfo } from "../modules/profile/creator-profile.js";
10
+ import { initPipeline } from "../storage/pipeline-store.js";
11
+
12
+ function getDataDir(customDir?: string): string {
13
+ if (customDir) return customDir;
14
+ const home = process.env.HOME || process.env.USERPROFILE || "~";
15
+ return path.join(home, ".autocrew");
16
+ }
17
+
18
+ const SUBDIRS = [
19
+ "topics",
20
+ "contents",
21
+ "learnings/edits",
22
+ "assets/raw",
23
+ "sensitive-words",
24
+ "covers/templates",
25
+ "competitors",
26
+ "memory",
27
+ ];
28
+
29
+ export interface InitResult {
30
+ ok: boolean;
31
+ dataDir: string;
32
+ created: string[];
33
+ alreadyExisted: boolean;
34
+ next_step?: {
35
+ action: string;
36
+ message: string;
37
+ steps: string[];
38
+ };
39
+ }
40
+
41
+ export async function executeInit(options?: { dataDir?: string }): Promise<InitResult> {
42
+ const dataDir = getDataDir(options?.dataDir);
43
+ const created: string[] = [];
44
+
45
+ // Check if already initialized
46
+ let alreadyExisted = false;
47
+ try {
48
+ await fs.access(dataDir);
49
+ alreadyExisted = true;
50
+ } catch {
51
+ // Will create
52
+ }
53
+
54
+ // Create all subdirectories
55
+ for (const sub of SUBDIRS) {
56
+ const dir = path.join(dataDir, sub);
57
+ try {
58
+ await fs.mkdir(dir, { recursive: true });
59
+ created.push(sub);
60
+ } catch {
61
+ // Already exists — fine
62
+ }
63
+ }
64
+
65
+ // Initialize pipeline directory structure (idempotent)
66
+ await initPipeline(dataDir);
67
+
68
+ // Initialize creator-profile.json (no-op if exists)
69
+ const profile = await initProfile(dataDir);
70
+
71
+ // Create empty STYLE.md if not exists
72
+ const stylePath = path.join(dataDir, "STYLE.md");
73
+ try {
74
+ await fs.access(stylePath);
75
+ } catch {
76
+ await fs.writeFile(
77
+ stylePath,
78
+ "# Writing Style\n\n> 尚未校准。运行「风格校准」来设置你的写作风格。\n",
79
+ "utf-8",
80
+ );
81
+ created.push("STYLE.md");
82
+ }
83
+
84
+ // Create empty custom sensitive words file if not exists
85
+ const customWordsPath = path.join(dataDir, "sensitive-words", "custom.txt");
86
+ try {
87
+ await fs.access(customWordsPath);
88
+ } catch {
89
+ await fs.writeFile(customWordsPath, "# 自定义敏感词(每行一个)\n", "utf-8");
90
+ created.push("sensitive-words/custom.txt");
91
+ }
92
+
93
+ // Check profile completeness to guide next steps
94
+ const missing = detectMissingInfo(profile);
95
+
96
+ let next_step: InitResult["next_step"];
97
+
98
+ if (missing.length > 0) {
99
+ const needsOnboarding = missing.some((m) => m !== "style");
100
+ const needsCalibration = missing.includes("style") || !profile.styleCalibrated;
101
+
102
+ if (needsOnboarding) {
103
+ next_step = {
104
+ action: "onboarding",
105
+ message: "⚠️ 创作者档案不完整,请先完成初始设置。",
106
+ steps: [
107
+ "1. 询问用户:你的行业/领域是什么?",
108
+ "2. 询问用户:你主要在哪些平台发内容?(小红书/抖音/公众号/视频号)",
109
+ "3. 询问用户:你的目标受众是谁?",
110
+ "4. 将信息保存到 creator-profile.json",
111
+ ...(needsCalibration
112
+ ? [
113
+ "5. 进行风格校准:通过 A/B 选择题确定写作风格偏好",
114
+ "6. 生成 STYLE.md 并更新 creator-profile.json 的 styleCalibrated 为 true",
115
+ ]
116
+ : []),
117
+ ],
118
+ };
119
+ } else if (needsCalibration) {
120
+ next_step = {
121
+ action: "style_calibration",
122
+ message: "✨ 初始化完成!接下来进行风格校准,让写出来的内容更贴合你的调性。",
123
+ steps: [
124
+ "1. 询问用户的风格偏好(正式vs口语、专业vs大白话、长文vs短文、情感vs干货)",
125
+ "2. 根据回答生成 ~/.autocrew/STYLE.md",
126
+ "3. 更新 creator-profile.json: styleCalibrated = true",
127
+ ],
128
+ };
129
+ }
130
+ }
131
+
132
+ return { ok: true, dataDir, created, alreadyExisted, next_step };
133
+ }
@@ -0,0 +1,92 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import { loadProfile } from "../modules/profile/creator-profile.js";
3
+ import { runIntelPull } from "../modules/intel/intel-engine.js";
4
+ import { listIntel, archiveExpiredIntel } from "../storage/pipeline-store.js";
5
+
6
+ export const intelSchema = Type.Object({
7
+ action: Type.Unsafe<string>({
8
+ type: "string",
9
+ enum: ["pull", "list", "clean"],
10
+ description: "Action to perform: pull (collect intel), list (show saved intel), clean (archive expired)",
11
+ }),
12
+ domain: Type.Optional(Type.String({ description: "Filter by domain" })),
13
+ source: Type.Optional(Type.String({ description: "Filter to specific source(s), comma-separated" })),
14
+ keywords: Type.Optional(Type.Array(Type.String(), { description: "Override keywords for pull" })),
15
+ _dataDir: Type.Optional(Type.String()),
16
+ });
17
+
18
+ export async function executeIntel(params: Record<string, unknown>) {
19
+ const action = params.action as string;
20
+ const dataDir = (params._dataDir as string) || undefined;
21
+ const domain = (params.domain as string) || undefined;
22
+
23
+ switch (action) {
24
+ case "pull": {
25
+ const profile = await loadProfile(dataDir);
26
+ if (!profile) {
27
+ return { ok: false, error: "No creator profile found. Run autocrew_init first." };
28
+ }
29
+
30
+ const searchFn = params._searchFn as
31
+ | ((query: string) => Promise<Array<{ title: string; snippet: string; url: string }>>)
32
+ | undefined;
33
+ if (!searchFn) {
34
+ return { ok: false, error: "Search function not available. Ensure MCP search is configured." };
35
+ }
36
+
37
+ const keywords = (params.keywords as string[]) ?? profile.writingRules.map((r) => r.rule).slice(0, 5);
38
+ const sources = params.source ? (params.source as string).split(",").map((s) => s.trim()) : undefined;
39
+
40
+ const result = await runIntelPull({
41
+ keywords: keywords.length > 0 ? keywords : [profile.industry],
42
+ industry: profile.industry,
43
+ platforms: profile.platforms,
44
+ dataDir,
45
+ searchFn,
46
+ skipBrowser: true,
47
+ sources,
48
+ });
49
+
50
+ return {
51
+ ok: true,
52
+ action: "pull",
53
+ totalCollected: result.totalCollected,
54
+ totalSaved: result.totalSaved,
55
+ bySource: result.bySource,
56
+ errors: result.errors.length > 0 ? result.errors.slice(0, 10) : undefined,
57
+ };
58
+ }
59
+
60
+ case "list": {
61
+ const items = await listIntel(domain, dataDir);
62
+ const top50 = items.slice(0, 50).map((item) => ({
63
+ title: item.title,
64
+ domain: item.domain,
65
+ source: item.source,
66
+ relevance: item.relevance,
67
+ collectedAt: item.collectedAt,
68
+ summary: item.summary.slice(0, 120),
69
+ }));
70
+
71
+ return {
72
+ ok: true,
73
+ action: "list",
74
+ total: items.length,
75
+ showing: top50.length,
76
+ items: top50,
77
+ };
78
+ }
79
+
80
+ case "clean": {
81
+ const result = await archiveExpiredIntel(dataDir);
82
+ return {
83
+ ok: true,
84
+ action: "clean",
85
+ archived: result.archived,
86
+ };
87
+ }
88
+
89
+ default:
90
+ return { ok: false, error: `Unknown action: ${action}` };
91
+ }
92
+ }
@@ -0,0 +1,76 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs/promises";
3
+ import { Type } from "@sinclair/typebox";
4
+ import { getContent } from "../storage/local-store.js";
5
+ import { distillMemory } from "../modules/memory/distill.js";
6
+
7
+ export const memorySchema = Type.Object({
8
+ action: Type.Unsafe<"capture_feedback" | "get_memory">({
9
+ type: "string",
10
+ enum: ["capture_feedback", "get_memory"],
11
+ description: "Capture a new feedback signal into memory, or read the current MEMORY.md file.",
12
+ }),
13
+ signal_type: Type.Optional(
14
+ Type.Unsafe<"approval" | "rejection" | "edit" | "performance" | "general">({
15
+ type: "string",
16
+ enum: ["approval", "rejection", "edit", "performance", "general"],
17
+ description: "Feedback signal type for capture_feedback.",
18
+ }),
19
+ ),
20
+ content_id: Type.Optional(Type.String({ description: "Optional AutoCrew content id." })),
21
+ feedback: Type.Optional(Type.String({ description: "Freeform user feedback or performance note." })),
22
+ original_text: Type.Optional(Type.String({ description: "Original text before user edit." })),
23
+ modified_text: Type.Optional(Type.String({ description: "User-edited or final text." })),
24
+ platform: Type.Optional(Type.String({ description: "Optional platform label." })),
25
+ });
26
+
27
+ function resolveDataDir(customDir?: string): string {
28
+ if (customDir) return customDir;
29
+ const home = process.env.HOME || process.env.USERPROFILE || "~";
30
+ return path.join(home, ".autocrew");
31
+ }
32
+
33
+ export async function executeMemory(params: Record<string, unknown>) {
34
+ const action = params.action as string;
35
+ const dataDir = (params._dataDir as string) || undefined;
36
+
37
+ if (action === "get_memory") {
38
+ const memoryPath = path.join(resolveDataDir(dataDir), "MEMORY.md");
39
+ try {
40
+ const content = await fs.readFile(memoryPath, "utf-8");
41
+ return { ok: true, memoryPath, content };
42
+ } catch {
43
+ return { ok: true, memoryPath, content: "", note: "MEMORY.md 尚未创建。使用 capture_feedback 记录反馈后会自动生成。" };
44
+ }
45
+ }
46
+
47
+ if (action !== "capture_feedback") {
48
+ return { ok: false, error: `Unknown action: ${action}` };
49
+ }
50
+
51
+ const signalType = (params.signal_type as any) || "general";
52
+ let contentTitle: string | undefined;
53
+ let originalText = (params.original_text as string) || undefined;
54
+ let modifiedText = (params.modified_text as string) || undefined;
55
+ let platform = (params.platform as string) || undefined;
56
+
57
+ const contentId = params.content_id as string | undefined;
58
+ if (contentId) {
59
+ const content = await getContent(contentId, dataDir);
60
+ if (content) {
61
+ contentTitle = content.title;
62
+ platform = platform || content.platform;
63
+ originalText = originalText || content.body;
64
+ }
65
+ }
66
+
67
+ return distillMemory({
68
+ signalType,
69
+ feedback: (params.feedback as string) || undefined,
70
+ originalText,
71
+ modifiedText,
72
+ contentTitle,
73
+ platform,
74
+ dataDir,
75
+ });
76
+ }
@@ -0,0 +1,109 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import {
3
+ initPipeline,
4
+ listIntel,
5
+ listTopics,
6
+ listProjects,
7
+ startProject,
8
+ advanceProject,
9
+ addDraftVersion,
10
+ trashProject,
11
+ restoreProject,
12
+ PIPELINE_STAGES,
13
+ } from "../storage/pipeline-store.js";
14
+
15
+ export const pipelineOpsSchema = Type.Object({
16
+ action: Type.Unsafe<string>({
17
+ type: "string",
18
+ enum: ["start", "advance", "trash", "restore", "status", "version"],
19
+ description: "Pipeline action: status (overview), start (topic→project), advance (next stage), version (add draft), trash/restore",
20
+ }),
21
+ project: Type.Optional(Type.String({ description: "Project slug name" })),
22
+ platform: Type.Optional(Type.String({ description: "Target platform" })),
23
+ content: Type.Optional(Type.String({ description: "Draft content for version action" })),
24
+ note: Type.Optional(Type.String({ description: "Version note or action note" })),
25
+ _dataDir: Type.Optional(Type.String()),
26
+ });
27
+
28
+ export async function executePipelineOps(params: Record<string, unknown>) {
29
+ const action = params.action as string;
30
+ const dataDir = (params._dataDir as string) || undefined;
31
+
32
+ switch (action) {
33
+ case "status": {
34
+ await initPipeline(dataDir);
35
+
36
+ const intel = await listIntel(undefined, dataDir);
37
+ const topics = await listTopics(undefined, dataDir);
38
+
39
+ const stages: Record<string, number> = {};
40
+ for (const stage of PIPELINE_STAGES) {
41
+ if (stage === "intel") {
42
+ stages[stage] = intel.length;
43
+ } else if (stage === "topics") {
44
+ stages[stage] = topics.length;
45
+ } else {
46
+ const projects = await listProjects(stage, dataDir);
47
+ stages[stage] = projects.length;
48
+ }
49
+ }
50
+
51
+ return {
52
+ ok: true,
53
+ action: "status",
54
+ stages,
55
+ total: Object.values(stages).reduce((a, b) => a + b, 0),
56
+ };
57
+ }
58
+
59
+ case "start": {
60
+ const project = params.project as string | undefined;
61
+ if (!project) {
62
+ return { ok: false, error: "Missing 'project' — provide a topic slug to start from." };
63
+ }
64
+ const dir = await startProject(project, dataDir);
65
+ return { ok: true, action: "start", projectDir: dir };
66
+ }
67
+
68
+ case "advance": {
69
+ const project = params.project as string | undefined;
70
+ if (!project) {
71
+ return { ok: false, error: "Missing 'project' — provide the project name to advance." };
72
+ }
73
+ const dir = await advanceProject(project, dataDir);
74
+ return { ok: true, action: "advance", newDir: dir };
75
+ }
76
+
77
+ case "version": {
78
+ const project = params.project as string | undefined;
79
+ const content = params.content as string | undefined;
80
+ const note = (params.note as string) ?? "new version";
81
+ if (!project || !content) {
82
+ return { ok: false, error: "Missing 'project' and/or 'content' for version action." };
83
+ }
84
+ const file = await addDraftVersion(project, content, note, dataDir);
85
+ return { ok: true, action: "version", file };
86
+ }
87
+
88
+ case "trash": {
89
+ const project = params.project as string | undefined;
90
+ if (!project) {
91
+ return { ok: false, error: "Missing 'project' — provide the project name to trash." };
92
+ }
93
+ await trashProject(project, dataDir);
94
+ return { ok: true, action: "trash", project };
95
+ }
96
+
97
+ case "restore": {
98
+ const project = params.project as string | undefined;
99
+ if (!project) {
100
+ return { ok: false, error: "Missing 'project' — provide the project name to restore." };
101
+ }
102
+ const dir = await restoreProject(project, dataDir);
103
+ return { ok: true, action: "restore", restoredTo: dir };
104
+ }
105
+
106
+ default:
107
+ return { ok: false, error: `Unknown action: ${action}` };
108
+ }
109
+ }
@@ -0,0 +1,168 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import { WorkflowEngine } from "../runtime/workflow-engine.js";
3
+ import { getTemplates, getTemplate } from "../modules/workflow/templates.js";
4
+ import type { ToolRunner } from "../runtime/tool-runner.js";
5
+
6
+ /**
7
+ * autocrew_pipeline — workflow orchestration for content pipelines.
8
+ *
9
+ * Manages stateful multi-step workflows with approval gates,
10
+ * parameter interpolation, and persistent state.
11
+ */
12
+
13
+ export const pipelineSchema = Type.Object({
14
+ action: Type.Unsafe<"create" | "start" | "status" | "approve" | "cancel" | "list" | "templates">({
15
+ type: "string",
16
+ enum: ["create", "start", "status", "approve", "cancel", "list", "templates"],
17
+ description:
18
+ "Action: 'create' workflow from template, 'start' a workflow, 'status' check progress, 'approve' a paused step, 'cancel' a workflow, 'list' all workflows, 'templates' list available templates.",
19
+ }),
20
+ id: Type.Optional(Type.String({ description: "Workflow instance id (for start/status/approve/cancel)" })),
21
+ template: Type.Optional(
22
+ Type.String({ description: "Template id: 'xiaohongshu_full', 'quick_publish'" }),
23
+ ),
24
+ params: Type.Optional(
25
+ Type.Record(Type.String(), Type.Unknown(), {
26
+ description: "Initial parameters for the workflow (e.g. content_id for quick_publish)",
27
+ }),
28
+ ),
29
+ });
30
+
31
+ /** Singleton engine per dataDir — avoids re-creating on every call */
32
+ const engines = new Map<string, WorkflowEngine>();
33
+
34
+ function getEngine(toolRunner: ToolRunner, dataDir: string): WorkflowEngine {
35
+ let engine = engines.get(dataDir);
36
+ if (!engine) {
37
+ engine = new WorkflowEngine(toolRunner, dataDir);
38
+ // Register all built-in templates
39
+ for (const tpl of getTemplates()) {
40
+ engine.registerDefinition(tpl);
41
+ }
42
+ engines.set(dataDir, engine);
43
+ }
44
+ return engine;
45
+ }
46
+
47
+ /**
48
+ * Create the pipeline executor. Needs a ToolRunner reference for workflow step execution.
49
+ */
50
+ export function createPipelineExecutor(toolRunner: ToolRunner) {
51
+ return async function executePipeline(params: Record<string, unknown>) {
52
+ const action = params.action as string;
53
+ const dataDir = params._dataDir as string;
54
+
55
+ if (!dataDir) {
56
+ return { ok: false, error: "No _dataDir provided" };
57
+ }
58
+
59
+ const engine = getEngine(toolRunner, dataDir);
60
+
61
+ // --- Templates ---
62
+ if (action === "templates") {
63
+ const templates = getTemplates().map((t) => ({
64
+ id: t.id,
65
+ name: t.name,
66
+ description: t.description,
67
+ steps: t.steps.map((s) => `${s.name} (${s.tool})${s.requiresApproval ? " ⏸" : ""}`),
68
+ }));
69
+ return { ok: true, templates };
70
+ }
71
+
72
+ // --- List ---
73
+ if (action === "list") {
74
+ const instances = await engine.list();
75
+ return {
76
+ ok: true,
77
+ workflows: instances.map((i) => ({
78
+ id: i.id,
79
+ definitionId: i.definitionId,
80
+ status: i.status,
81
+ currentStepIndex: i.currentStepIndex,
82
+ createdAt: i.createdAt,
83
+ error: i.error,
84
+ })),
85
+ };
86
+ }
87
+
88
+ // --- Create ---
89
+ if (action === "create") {
90
+ const templateId = params.template as string;
91
+ if (!templateId) {
92
+ return { ok: false, error: "template is required for create" };
93
+ }
94
+ const tpl = getTemplate(templateId);
95
+ if (!tpl) {
96
+ return { ok: false, error: `Unknown template: ${templateId}. Use action='templates' to see available ones.` };
97
+ }
98
+ try {
99
+ const instance = await engine.create(templateId, params.params as Record<string, unknown>);
100
+ return {
101
+ ok: true,
102
+ workflow: instance,
103
+ hint: `Workflow created. Use action='start' with id='${instance.id}' to begin execution.`,
104
+ };
105
+ } catch (err: unknown) {
106
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
107
+ }
108
+ }
109
+
110
+ // --- Start ---
111
+ if (action === "start") {
112
+ const id = params.id as string;
113
+ if (!id) return { ok: false, error: "id is required" };
114
+ try {
115
+ const instance = await engine.start(id);
116
+ return { ok: true, workflow: instance };
117
+ } catch (err: unknown) {
118
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
119
+ }
120
+ }
121
+
122
+ // --- Status ---
123
+ if (action === "status") {
124
+ const id = params.id as string;
125
+ if (!id) return { ok: false, error: "id is required" };
126
+ const instance = await engine.getStatus(id);
127
+ if (!instance) {
128
+ return { ok: false, error: `Workflow ${id} not found` };
129
+ }
130
+ const def = getTemplate(instance.definitionId);
131
+ const currentStep = def?.steps[instance.currentStepIndex];
132
+ return {
133
+ ok: true,
134
+ workflow: instance,
135
+ currentStep: currentStep
136
+ ? { name: currentStep.name, tool: currentStep.tool, requiresApproval: currentStep.requiresApproval }
137
+ : null,
138
+ totalSteps: def?.steps.length ?? 0,
139
+ };
140
+ }
141
+
142
+ // --- Approve ---
143
+ if (action === "approve") {
144
+ const id = params.id as string;
145
+ if (!id) return { ok: false, error: "id is required" };
146
+ try {
147
+ const instance = await engine.approve(id);
148
+ return { ok: true, workflow: instance };
149
+ } catch (err: unknown) {
150
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
151
+ }
152
+ }
153
+
154
+ // --- Cancel ---
155
+ if (action === "cancel") {
156
+ const id = params.id as string;
157
+ if (!id) return { ok: false, error: "id is required" };
158
+ try {
159
+ const instance = await engine.cancel(id);
160
+ return { ok: true, workflow: instance };
161
+ } catch (err: unknown) {
162
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
163
+ }
164
+ }
165
+
166
+ return { ok: false, error: `Unknown action: ${action}` };
167
+ };
168
+ }