autocrew 0.3.7 → 0.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autocrew",
3
- "version": "0.3.7",
3
+ "version": "0.3.9",
4
4
  "description": "One-person content studio powered by AI — from trending topics to published posts",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,39 @@
1
+ import type { CommandDef } from "./index.js";
2
+ import { getOption } from "./index.js";
3
+
4
+ export const cmd: CommandDef = {
5
+ name: "draft",
6
+ description: "Generate a content draft from a topic (loads style + methodology + wiki)",
7
+ usage: "autocrew draft <topic-title> [--platform <platform>]",
8
+ action: async (args, runner) => {
9
+ const topicTitle = args.filter((a) => !a.startsWith("--")).join(" ");
10
+ if (!topicTitle) {
11
+ console.error("Usage: autocrew draft <topic-title> [--platform <platform>]");
12
+ console.error("Example: autocrew draft 'vibe-coding 实践者的真实工作流' --platform xiaohongshu");
13
+ process.exitCode = 1;
14
+ return;
15
+ }
16
+
17
+ const platform = getOption(args, "--platform") || "xiaohongshu";
18
+
19
+ const result = await runner.execute("autocrew_content", {
20
+ action: "draft",
21
+ topic_title: topicTitle,
22
+ platform,
23
+ });
24
+
25
+ if (!result.ok) {
26
+ console.error(`Draft failed: ${result.error || "unknown error"}`);
27
+ process.exitCode = 1;
28
+ return;
29
+ }
30
+
31
+ console.log(`Draft context loaded for: "${topicTitle}" (${platform})`);
32
+ console.log(`\nCreator: ${(result.creatorContext as any)?.industry || "not configured"}`);
33
+ console.log(`Style: ${(result.style as string)?.slice(0, 80) || "no style file"}...`);
34
+ console.log(`Wiki context: ${(result.wikiContext as string)?.slice(0, 80) || "none"}...`);
35
+ console.log(`\nWriting instructions loaded (Operating System + Two-Phase Creation).`);
36
+ console.log(`\nNext step: Generate the draft body, then save with:`);
37
+ console.log(` autocrew_content action="save" title="..." body="..." platform="${platform}"`);
38
+ },
39
+ };
@@ -99,6 +99,7 @@ import { cmd as advanceCmd } from "./advance.js";
99
99
  import { cmd as trashCmd } from "./trash.js";
100
100
  import { cmd as restoreCmd } from "./restore.js";
101
101
  import { cmd as migrateCmd } from "./migrate.js";
102
+ import { cmd as draftCmd } from "./draft.js";
102
103
 
103
104
  export const commands: CommandDef[] = [
104
105
  statusCmd,
@@ -128,4 +129,5 @@ export const commands: CommandDef[] = [
128
129
  trashCmd,
129
130
  restoreCmd,
130
131
  migrateCmd,
132
+ draftCmd,
131
133
  ];
@@ -4,7 +4,7 @@ import { getOption, resolveProjectText } from "./index.js";
4
4
  export const cmd: CommandDef = {
5
5
  name: "review",
6
6
  description: "Run full content review or auto-fix",
7
- usage: "autocrew review <content-id-or-project-slug> [--platform <p>] [--fix] [--file <path>]",
7
+ usage: "autocrew review <id-or-slug> [--platform <p>] [--fix] [--dry-run] [--file <path>]",
8
8
  action: async (args, runner) => {
9
9
  const id = args[0];
10
10
  if (!id && !args.includes("--file")) {
@@ -15,6 +15,7 @@ export const cmd: CommandDef = {
15
15
 
16
16
  const platform = getOption(args, "--platform");
17
17
  const isFix = args.includes("--fix");
18
+ const isDryRun = args.includes("--dry-run");
18
19
 
19
20
  const resolved = await resolveProjectText(id, args);
20
21
  if (!resolved) {
@@ -23,7 +24,7 @@ export const cmd: CommandDef = {
23
24
  return;
24
25
  }
25
26
 
26
- if (isFix) {
27
+ if (isFix || isDryRun) {
27
28
  const result = await runner.execute("autocrew_review", {
28
29
  action: "auto_fix",
29
30
  text: resolved.text,
@@ -36,13 +37,47 @@ export const cmd: CommandDef = {
36
37
  return;
37
38
  }
38
39
 
39
- // Write fixed text back to source file
40
40
  const fixedText = (result.autoFixedText || result.fixedText || "") as string;
41
+ console.log(` Sensitive words fixed: ${result.sensitiveWordsFixed || 0}`);
42
+ console.log(` AI traces fixed: ${result.aiFixesApplied || 0}`);
43
+
44
+ // Show diff preview
45
+ if (fixedText && fixedText !== resolved.text) {
46
+ // Simple line-by-line diff
47
+ const origLines = resolved.text.split("\n");
48
+ const fixedLines = fixedText.split("\n");
49
+ const diffLines: string[] = [];
50
+ const maxLen = Math.max(origLines.length, fixedLines.length);
51
+ for (let i = 0; i < maxLen; i++) {
52
+ const orig = origLines[i] ?? "";
53
+ const fixed = fixedLines[i] ?? "";
54
+ if (orig !== fixed) {
55
+ if (orig) diffLines.push(` - ${orig}`);
56
+ if (fixed) diffLines.push(` + ${fixed}`);
57
+ }
58
+ }
59
+ if (diffLines.length > 0) {
60
+ console.log(`\n Changes preview:`);
61
+ for (const line of diffLines.slice(0, 20)) {
62
+ console.log(line);
63
+ }
64
+ if (diffLines.length > 20) {
65
+ console.log(` ... and ${diffLines.length - 20} more lines`);
66
+ }
67
+ }
68
+ }
69
+
70
+ if (isDryRun) {
71
+ console.log(`\n (dry-run mode — no files modified)`);
72
+ return;
73
+ }
74
+
75
+ // Write fixed text back to source file
41
76
  if (fixedText && resolved.source.startsWith("file:")) {
42
77
  const fs = await import("node:fs/promises");
43
78
  const filePath = resolved.source.replace("file:", "");
44
79
  await fs.writeFile(filePath, fixedText, "utf-8");
45
- console.log(`Auto-fix complete for "${resolved.title}" — saved back to ${filePath}`);
80
+ console.log(`\n Saved back to ${filePath}`);
46
81
  } else if (fixedText && resolved.source.startsWith("pipeline:")) {
47
82
  const fs = await import("node:fs/promises");
48
83
  const { findProject } = await import("../../storage/pipeline-store.js");
@@ -51,13 +86,9 @@ export const cmd: CommandDef = {
51
86
  if (found) {
52
87
  const path = await import("node:path");
53
88
  await fs.writeFile(path.join(found.dir, "draft.md"), fixedText, "utf-8");
54
- console.log(`Auto-fix complete for "${resolved.title}" — saved back to draft.md`);
89
+ console.log(`\n Saved back to draft.md`);
55
90
  }
56
- } else {
57
- console.log(`Auto-fix complete for "${resolved.title}" (${resolved.source})`);
58
91
  }
59
- console.log(` Sensitive words fixed: ${result.sensitiveWordsFixed || 0}`);
60
- console.log(` AI traces fixed: ${result.aiFixesApplied || 0}`);
61
92
  return;
62
93
  }
63
94
 
@@ -55,9 +55,10 @@ function breakLongClauses(text: string): { text: string; count: number } {
55
55
  const chineseLength = (trimmed.match(/[\u4e00-\u9fff]/g) || []).length;
56
56
  if (chineseLength <= 40) return line;
57
57
 
58
- const replaced = line
59
- .replace(/,(?=[^,。!?]{10,})/, "。")
60
- .replace(/;(?=[^;。!?]{8,})/, "。");
58
+ // Use /g flag to apply ALL breaks in one pass (convergence guarantee)
59
+ let replaced = line;
60
+ replaced = replaced.replace(/,(?=[^,。!?]{10,})/g, "。");
61
+ replaced = replaced.replace(/;(?=[^;。!?]{8,})/g, "。");
61
62
  if (replaced !== line) {
62
63
  count += 1;
63
64
  return replaced;
@@ -108,20 +109,10 @@ function reduceWeOpenings(text: string): { text: string; count: number } {
108
109
  return { text: nextLines.join("\n"), count: changed };
109
110
  }
110
111
 
111
- function addRhythmPhraseIfNeeded(text: string): { text: string; count: number } {
112
- if (/说白了|你想啊|问题来了/.test(text)) {
113
- return { text, count: 0 };
114
- }
115
-
116
- const paragraphs = text.split(/\n{2,}/);
117
- if (paragraphs.length < 2) {
118
- return { text, count: 0 };
119
- }
120
-
121
- const next = [...paragraphs];
122
- next.splice(1, 0, "说白了,这件事拼的不是工具数量,而是表达和执行。");
123
- return { text: next.join("\n\n"), count: 1 };
124
- }
112
+ // addRhythmPhraseIfNeeded removed.
113
+ // Previously inserted a hardcoded sentence ("说白了,这件事拼的不是工具数量,而是表达和执行。")
114
+ // that was unrelated to the actual content. Humanizer should only do
115
+ // substitution and deletion — never insert new content.
125
116
 
126
117
  export function humanizeZh(options: HumanizeZhOptions): HumanizeZhResult {
127
118
  const originalText = options.text || "";
@@ -154,11 +145,7 @@ export function humanizeZh(options: HumanizeZhOptions): HumanizeZhResult {
154
145
  changes.push(`减少“我们”开头句子 × ${weOpenings.count}`);
155
146
  }
156
147
 
157
- const rhythm = addRhythmPhraseIfNeeded(humanizedText);
158
- if (rhythm.count > 0) {
159
- humanizedText = rhythm.text;
160
- changes.push("补入 1 处口语化节奏句");
161
- }
148
+ // No content insertion — humanizer only substitutes and deletes.
162
149
 
163
150
  humanizedText = normalizeWhitespace(humanizedText);
164
151
  return {
@@ -250,13 +250,21 @@ export async function executeReview(params: Record<string, unknown>) {
250
250
  const fixes: string[] = [];
251
251
  if (sensitiveWords.hitCount > 0) {
252
252
  fixes.push(`修复 ${sensitiveWords.hitCount} 个敏感词`);
253
- for (const hit of sensitiveWords.hits.slice(0, 5)) {
253
+ for (const hit of sensitiveWords.hits.slice(0, 10)) {
254
254
  const fix = hit.suggestion ? `"${hit.word}" → "${hit.suggestion}"` : `删除"${hit.word}"`;
255
- fixes.push(` - ${fix} (${hit.category})`);
255
+ // Show surrounding context for each hit
256
+ const pos = hit.positions[0];
257
+ const contextStart = Math.max(0, pos - 10);
258
+ const contextEnd = Math.min(fullText.length, pos + hit.word.length + 10);
259
+ const context = fullText.slice(contextStart, contextEnd).replace(/\n/g, " ");
260
+ fixes.push(` - ${fix} (${hit.category}) — "...${context}..."`);
256
261
  }
257
262
  }
258
263
  if (aiCheck.hasAiTraces) {
259
264
  fixes.push(`去 AI 味:${aiCheck.changeCount} 处需要修改`);
265
+ for (const change of aiCheck.changes.slice(0, 5)) {
266
+ fixes.push(` - ${change}`);
267
+ }
260
268
  }
261
269
  for (const note of qualityScore.notes) {
262
270
  fixes.push(note);
@@ -1,5 +1,10 @@
1
1
  import { Type } from "@sinclair/typebox";
2
- import { saveTopic, listTopics } from "../storage/local-store.js";
2
+ import { saveTopic as legacySaveTopic, listTopics as legacyListTopics } from "../storage/local-store.js";
3
+ import {
4
+ saveTopic as pipelineSaveTopic,
5
+ listTopics as pipelineListTopics,
6
+ type TopicCandidate,
7
+ } from "../storage/pipeline-store.js";
3
8
 
4
9
  /**
5
10
  * Core tool logic — platform-agnostic.
@@ -23,14 +28,22 @@ export async function executeTopicCreate(params: Record<string, unknown>) {
23
28
  const dataDir = (params._dataDir as string) || undefined;
24
29
 
25
30
  if (action === "list") {
26
- const topics = await listTopics(dataDir);
27
- if (topics.length === 0) {
31
+ // List from both stores, deduplicate by title
32
+ const legacyTopics = await legacyListTopics(dataDir);
33
+ const pipelineTopics = await pipelineListTopics(undefined, dataDir);
34
+
35
+ const combined = [
36
+ ...legacyTopics.map((t) => ({ ...t, _store: "legacy" })),
37
+ ...pipelineTopics.map((t) => ({ title: t.title, domain: t.domain, tags: t.tags || [], score: t.score, _store: "pipeline" })),
38
+ ];
39
+
40
+ if (combined.length === 0) {
28
41
  return { ok: true, message: "No topics yet.", topics: [] };
29
42
  }
30
- return { ok: true, topics };
43
+ return { ok: true, topics: combined };
31
44
  }
32
45
 
33
- // create
46
+ // create — write to BOTH stores for compatibility
34
47
  const title = params.title as string;
35
48
  const description = params.description as string;
36
49
  const tags = (params.tags as string[]) || [];
@@ -39,12 +52,29 @@ export async function executeTopicCreate(params: Record<string, unknown>) {
39
52
  return { ok: false, error: "title and description are required for create" };
40
53
  }
41
54
 
42
- const topic = await saveTopic({
55
+ // Save to legacy store (for backward compat)
56
+ const legacyTopic = await legacySaveTopic({
43
57
  title,
44
58
  description,
45
59
  tags,
46
60
  source: (params.source as string) || undefined,
47
61
  }, dataDir);
48
62
 
49
- return { ok: true, topic };
63
+ // Also save to pipeline store (so start command finds it)
64
+ const now = new Date().toISOString();
65
+ const pipelineTopic: TopicCandidate = {
66
+ title,
67
+ domain: tags[0] || "general",
68
+ score: { heat: 50, differentiation: 50, audienceFit: 50, overall: 50 },
69
+ formats: [],
70
+ suggestedPlatforms: [],
71
+ createdAt: now,
72
+ intelRefs: [],
73
+ angles: [description],
74
+ audienceResonance: "",
75
+ references: [],
76
+ };
77
+ await pipelineSaveTopic(pipelineTopic, dataDir);
78
+
79
+ return { ok: true, topic: legacyTopic, pipelineSynced: true };
50
80
  }