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,391 @@
1
+ /**
2
+ * WorkflowEngine — stateful workflow orchestration for AutoCrew.
3
+ *
4
+ * Runs multi-step tool pipelines with:
5
+ * - Sequential step execution via ToolRunner
6
+ * - Approval gates (pauses workflow for user confirmation)
7
+ * - Retry logic per step
8
+ * - Parameter interpolation (${stepId.field} references)
9
+ * - Persistent state to disk after every step
10
+ */
11
+
12
+ import fs from "node:fs/promises";
13
+ import path from "node:path";
14
+ import type { ToolRunner, ToolResult } from "./tool-runner.js";
15
+
16
+ // --- Interfaces ---
17
+
18
+ export interface WorkflowStep {
19
+ id: string;
20
+ name: string;
21
+ /** Tool to call */
22
+ tool: string;
23
+ /** Parameters (can reference previous step outputs via ${stepId.field}) */
24
+ params: Record<string, unknown>;
25
+ /** If true, workflow pauses here for user approval */
26
+ requiresApproval?: boolean;
27
+ /** Condition: only run if this evaluates true */
28
+ condition?: string;
29
+ /** Retry config */
30
+ retry?: { maxAttempts: number; delayMs: number };
31
+ }
32
+
33
+ export interface WorkflowDefinition {
34
+ id: string;
35
+ name: string;
36
+ description: string;
37
+ steps: WorkflowStep[];
38
+ }
39
+
40
+ export type WorkflowStatus = "pending" | "running" | "paused" | "completed" | "failed" | "cancelled";
41
+
42
+ export interface WorkflowInstance {
43
+ id: string;
44
+ definitionId: string;
45
+ status: WorkflowStatus;
46
+ currentStepIndex: number;
47
+ stepResults: Record<string, unknown>;
48
+ createdAt: string;
49
+ updatedAt: string;
50
+ error?: string;
51
+ /** Initial params passed at creation (merged into each step) */
52
+ params?: Record<string, unknown>;
53
+ }
54
+
55
+ // --- Helpers ---
56
+
57
+ function generateId(): string {
58
+ return `wf-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
59
+ }
60
+
61
+ /**
62
+ * Resolve parameter references like ${stepId.field} against stepResults.
63
+ */
64
+ function resolveParams(
65
+ params: Record<string, unknown>,
66
+ stepResults: Record<string, unknown>,
67
+ instanceParams?: Record<string, unknown>,
68
+ ): Record<string, unknown> {
69
+ const resolved: Record<string, unknown> = {};
70
+ for (const [key, value] of Object.entries(params)) {
71
+ if (typeof value === "string") {
72
+ resolved[key] = value.replace(/\$\{([^}]+)\}/g, (_match, ref: string) => {
73
+ const [stepId, ...fieldParts] = ref.split(".");
74
+ const field = fieldParts.join(".");
75
+ // Try stepResults first, then instanceParams
76
+ const stepResult = stepResults[stepId] as Record<string, unknown> | undefined;
77
+ if (stepResult && field in stepResult) {
78
+ return String(stepResult[field]);
79
+ }
80
+ if (instanceParams && stepId in instanceParams && !field) {
81
+ return String(instanceParams[stepId]);
82
+ }
83
+ return `\${${ref}}`; // Leave unresolved
84
+ });
85
+ } else {
86
+ resolved[key] = value;
87
+ }
88
+ }
89
+ return resolved;
90
+ }
91
+
92
+ /**
93
+ * Evaluate a simple condition string against stepResults.
94
+ * Supports: "stepId.field === value", "stepId.ok", "stepId.field !== value"
95
+ */
96
+ function evaluateCondition(condition: string, stepResults: Record<string, unknown>): boolean {
97
+ try {
98
+ // Simple evaluator for "stepId.field" truthiness
99
+ const trimmed = condition.trim();
100
+
101
+ // Handle "stepId.field === value"
102
+ const eqMatch = trimmed.match(/^(\S+)\s*===?\s*(.+)$/);
103
+ if (eqMatch) {
104
+ const val = resolveFieldPath(eqMatch[1], stepResults);
105
+ const expected = eqMatch[2].replace(/^["']|["']$/g, "");
106
+ return String(val) === expected;
107
+ }
108
+
109
+ // Handle "stepId.field !== value"
110
+ const neqMatch = trimmed.match(/^(\S+)\s*!==?\s*(.+)$/);
111
+ if (neqMatch) {
112
+ const val = resolveFieldPath(neqMatch[1], stepResults);
113
+ const expected = neqMatch[2].replace(/^["']|["']$/g, "");
114
+ return String(val) !== expected;
115
+ }
116
+
117
+ // Simple truthiness: "stepId.field"
118
+ const val = resolveFieldPath(trimmed, stepResults);
119
+ return !!val;
120
+ } catch {
121
+ return true; // If condition can't be evaluated, proceed
122
+ }
123
+ }
124
+
125
+ function resolveFieldPath(fieldPath: string, stepResults: Record<string, unknown>): unknown {
126
+ const [stepId, ...parts] = fieldPath.split(".");
127
+ let current: unknown = stepResults[stepId];
128
+ for (const part of parts) {
129
+ if (current && typeof current === "object" && current !== null) {
130
+ current = (current as Record<string, unknown>)[part];
131
+ } else {
132
+ return undefined;
133
+ }
134
+ }
135
+ return current;
136
+ }
137
+
138
+ function delay(ms: number): Promise<void> {
139
+ return new Promise((resolve) => setTimeout(resolve, ms));
140
+ }
141
+
142
+ // --- WorkflowEngine ---
143
+
144
+ export class WorkflowEngine {
145
+ private toolRunner: ToolRunner;
146
+ private dataDir: string;
147
+ private definitions = new Map<string, WorkflowDefinition>();
148
+
149
+ constructor(toolRunner: ToolRunner, dataDir: string) {
150
+ this.toolRunner = toolRunner;
151
+ this.dataDir = dataDir;
152
+ }
153
+
154
+ /** Register a workflow definition (template) */
155
+ registerDefinition(def: WorkflowDefinition): void {
156
+ this.definitions.set(def.id, def);
157
+ }
158
+
159
+ /** Get a registered definition */
160
+ getDefinition(id: string): WorkflowDefinition | undefined {
161
+ return this.definitions.get(id);
162
+ }
163
+
164
+ /** List all registered definitions */
165
+ listDefinitions(): WorkflowDefinition[] {
166
+ return Array.from(this.definitions.values());
167
+ }
168
+
169
+ private async workflowsDir(): Promise<string> {
170
+ const dir = path.join(this.dataDir, "workflows");
171
+ await fs.mkdir(dir, { recursive: true });
172
+ return dir;
173
+ }
174
+
175
+ private async persistInstance(instance: WorkflowInstance): Promise<void> {
176
+ const dir = await this.workflowsDir();
177
+ await fs.writeFile(
178
+ path.join(dir, `${instance.id}.json`),
179
+ JSON.stringify(instance, null, 2),
180
+ "utf-8",
181
+ );
182
+ }
183
+
184
+ private async loadInstance(id: string): Promise<WorkflowInstance | null> {
185
+ const dir = await this.workflowsDir();
186
+ try {
187
+ const raw = await fs.readFile(path.join(dir, `${id}.json`), "utf-8");
188
+ return JSON.parse(raw);
189
+ } catch {
190
+ return null;
191
+ }
192
+ }
193
+
194
+ /** Create a workflow instance from a registered definition */
195
+ async create(definitionId: string, params?: Record<string, unknown>): Promise<WorkflowInstance> {
196
+ const def = this.definitions.get(definitionId);
197
+ if (!def) {
198
+ throw new Error(`Workflow definition not found: ${definitionId}`);
199
+ }
200
+
201
+ const now = new Date().toISOString();
202
+ const instance: WorkflowInstance = {
203
+ id: generateId(),
204
+ definitionId,
205
+ status: "pending",
206
+ currentStepIndex: 0,
207
+ stepResults: {},
208
+ createdAt: now,
209
+ updatedAt: now,
210
+ params,
211
+ };
212
+
213
+ await this.persistInstance(instance);
214
+ return instance;
215
+ }
216
+
217
+ /** Start (or resume) a workflow instance */
218
+ async start(instanceId: string): Promise<WorkflowInstance> {
219
+ const instance = await this.loadInstance(instanceId);
220
+ if (!instance) {
221
+ throw new Error(`Workflow instance not found: ${instanceId}`);
222
+ }
223
+
224
+ if (instance.status === "completed" || instance.status === "cancelled") {
225
+ return instance;
226
+ }
227
+
228
+ const def = this.definitions.get(instance.definitionId);
229
+ if (!def) {
230
+ instance.status = "failed";
231
+ instance.error = `Definition not found: ${instance.definitionId}`;
232
+ instance.updatedAt = new Date().toISOString();
233
+ await this.persistInstance(instance);
234
+ return instance;
235
+ }
236
+
237
+ instance.status = "running";
238
+ instance.updatedAt = new Date().toISOString();
239
+ await this.persistInstance(instance);
240
+
241
+ return this.executeSteps(instance, def);
242
+ }
243
+
244
+ /** Approve a paused workflow step and continue execution */
245
+ async approve(instanceId: string): Promise<WorkflowInstance> {
246
+ const instance = await this.loadInstance(instanceId);
247
+ if (!instance) {
248
+ throw new Error(`Workflow instance not found: ${instanceId}`);
249
+ }
250
+
251
+ if (instance.status !== "paused") {
252
+ throw new Error(`Workflow is not paused (status: ${instance.status})`);
253
+ }
254
+
255
+ const def = this.definitions.get(instance.definitionId);
256
+ if (!def) {
257
+ instance.status = "failed";
258
+ instance.error = `Definition not found: ${instance.definitionId}`;
259
+ instance.updatedAt = new Date().toISOString();
260
+ await this.persistInstance(instance);
261
+ return instance;
262
+ }
263
+
264
+ // Move past the approval step
265
+ instance.currentStepIndex++;
266
+ instance.status = "running";
267
+ instance.updatedAt = new Date().toISOString();
268
+ await this.persistInstance(instance);
269
+
270
+ return this.executeSteps(instance, def);
271
+ }
272
+
273
+ /** Cancel a workflow */
274
+ async cancel(instanceId: string): Promise<WorkflowInstance> {
275
+ const instance = await this.loadInstance(instanceId);
276
+ if (!instance) {
277
+ throw new Error(`Workflow instance not found: ${instanceId}`);
278
+ }
279
+
280
+ instance.status = "cancelled";
281
+ instance.updatedAt = new Date().toISOString();
282
+ await this.persistInstance(instance);
283
+ return instance;
284
+ }
285
+
286
+ /** Get current status of a workflow */
287
+ async getStatus(instanceId: string): Promise<WorkflowInstance | null> {
288
+ return this.loadInstance(instanceId);
289
+ }
290
+
291
+ /** List all workflow instances */
292
+ async list(): Promise<WorkflowInstance[]> {
293
+ const dir = await this.workflowsDir();
294
+ const files = await fs.readdir(dir);
295
+ const instances: WorkflowInstance[] = [];
296
+ for (const f of files) {
297
+ if (!f.endsWith(".json")) continue;
298
+ try {
299
+ const raw = await fs.readFile(path.join(dir, f), "utf-8");
300
+ instances.push(JSON.parse(raw));
301
+ } catch {
302
+ // skip corrupt files
303
+ }
304
+ }
305
+ return instances.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
306
+ }
307
+
308
+ // --- Internal execution ---
309
+
310
+ private async executeSteps(
311
+ instance: WorkflowInstance,
312
+ def: WorkflowDefinition,
313
+ ): Promise<WorkflowInstance> {
314
+ while (instance.currentStepIndex < def.steps.length) {
315
+ if (instance.status === "cancelled") break;
316
+
317
+ const step = def.steps[instance.currentStepIndex];
318
+
319
+ // Check condition
320
+ if (step.condition && !evaluateCondition(step.condition, instance.stepResults)) {
321
+ // Skip this step
322
+ instance.currentStepIndex++;
323
+ instance.updatedAt = new Date().toISOString();
324
+ await this.persistInstance(instance);
325
+ continue;
326
+ }
327
+
328
+ // Check approval gate BEFORE executing the step
329
+ if (step.requiresApproval) {
330
+ instance.status = "paused";
331
+ instance.updatedAt = new Date().toISOString();
332
+ await this.persistInstance(instance);
333
+ return instance;
334
+ }
335
+
336
+ // Execute step with retries
337
+ const result = await this.executeStepWithRetry(step, instance);
338
+
339
+ // Store result
340
+ instance.stepResults[step.id] = result;
341
+ instance.updatedAt = new Date().toISOString();
342
+ await this.persistInstance(instance); // Persist result before advancing
343
+
344
+ if (result.ok === false) {
345
+ instance.status = "failed";
346
+ instance.error = `Step "${step.name}" failed: ${(result as ToolResult).error || "unknown error"}`;
347
+ await this.persistInstance(instance);
348
+ return instance;
349
+ }
350
+
351
+ // Advance
352
+ instance.currentStepIndex++;
353
+ await this.persistInstance(instance);
354
+ }
355
+
356
+ if (instance.status !== "cancelled") {
357
+ instance.status = "completed";
358
+ instance.updatedAt = new Date().toISOString();
359
+ await this.persistInstance(instance);
360
+ }
361
+
362
+ return instance;
363
+ }
364
+
365
+ private async executeStepWithRetry(
366
+ step: WorkflowStep,
367
+ instance: WorkflowInstance,
368
+ ): Promise<ToolResult> {
369
+ const maxAttempts = step.retry?.maxAttempts ?? 1;
370
+ const delayMs = step.retry?.delayMs ?? 0;
371
+
372
+ const resolvedParams = resolveParams(step.params, instance.stepResults, instance.params);
373
+
374
+ let lastResult: ToolResult = { ok: false, error: "No attempts made" };
375
+
376
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
377
+ lastResult = await this.toolRunner.execute(step.tool, resolvedParams);
378
+
379
+ if (lastResult.ok !== false) {
380
+ return lastResult;
381
+ }
382
+
383
+ // Don't delay after the last failed attempt
384
+ if (attempt < maxAttempts && delayMs > 0) {
385
+ await delay(delayMs);
386
+ }
387
+ }
388
+
389
+ return lastResult;
390
+ }
391
+ }