autocrew 0.1.0 → 0.2.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.
@@ -205,6 +205,9 @@ export async function executeContentSave(params: Record<string, unknown>) {
205
205
  await fs.mkdir(path.join(projectDir, "references"), { recursive: true });
206
206
 
207
207
  const now = new Date().toISOString();
208
+ // Semantics: draft.md = live current working file (always latest),
209
+ // draft-v{N}.md = immutable snapshots of content that has been REPLACED by a revision.
210
+ // On initial save there are no snapshots yet — only draft.md exists.
208
211
  const meta: ProjectMeta = {
209
212
  title,
210
213
  domain: "",
@@ -212,8 +215,8 @@ export async function executeContentSave(params: Record<string, unknown>) {
212
215
  createdAt: now,
213
216
  sourceTopic: "",
214
217
  intelRefs: [],
215
- versions: [{ file: "draft-v1.md", createdAt: now, note: "initial draft" }],
216
- current: "draft-v1.md",
218
+ versions: [],
219
+ current: "draft.md",
217
220
  history: [{ stage: "drafting", entered: now }],
218
221
  platforms: platform ? [{ format: platform, status: "drafting" }] : [],
219
222
  hypothesis: (params.hypothesis as string) || undefined,
@@ -228,14 +231,9 @@ export async function executeContentSave(params: Record<string, unknown>) {
228
231
  yaml.dump(meta, { lineWidth: -1 }),
229
232
  "utf-8",
230
233
  );
231
- await fs.writeFile(
232
- path.join(projectDir, "draft-v1.md"),
233
- body,
234
- "utf-8",
235
- );
236
234
  await fs.writeFile(
237
235
  path.join(projectDir, "draft.md"),
238
- `# ${title}\n\n${body}\n`,
236
+ body,
239
237
  "utf-8",
240
238
  );
241
239
 
@@ -264,9 +262,9 @@ export async function executeContentSave(params: Record<string, unknown>) {
264
262
  openCommand: `open "${projectDir}"`,
265
263
  message: [
266
264
  `📄 内容已保存到 pipeline:`,
267
- ` 草稿:${projectDir}/draft.md`,
265
+ ` 草稿:${projectDir}/draft.md (当前活动文件)`,
268
266
  ` 元数据:${projectDir}/meta.yaml`,
269
- ` 版本:${projectDir}/draft-v1.md`,
267
+ ` 历史快照:修改后会在 ${projectDir}/draft-v{N}.md 生成`,
270
268
  ` 自动去AI味:${humanizeResult?.ok ? "✅ 已处理" : "⚠️ 跳过"}`,
271
269
  ``,
272
270
  `打开文件夹:open "${projectDir}"`,
@@ -0,0 +1,61 @@
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 { executeIntel } from "./intel.js";
6
+ import { listIntel } from "../storage/pipeline-store.js";
7
+
8
+ let testDir: string;
9
+
10
+ beforeEach(async () => {
11
+ testDir = await fs.mkdtemp(path.join(os.tmpdir(), "autocrew-intel-test-"));
12
+ // Create a minimal creator profile so loadProfile doesn't fail
13
+ await fs.writeFile(
14
+ path.join(testDir, "creator-profile.json"),
15
+ JSON.stringify({
16
+ industry: "tech",
17
+ platforms: ["xiaohongshu"],
18
+ writingRules: [],
19
+ styleCalibrated: false,
20
+ }),
21
+ "utf-8",
22
+ );
23
+ });
24
+
25
+ afterEach(async () => {
26
+ await fs.rm(testDir, { recursive: true, force: true });
27
+ });
28
+
29
+ describe("executeIntel – ingest action", () => {
30
+ it("ingests text and saves as intel item", async () => {
31
+ const result = (await executeIntel({
32
+ action: "ingest",
33
+ text: "AI video editing tools are transforming short-form content creation workflows for solo creators.",
34
+ domain: "ai-tools",
35
+ tags: ["ai", "video"],
36
+ _dataDir: testDir,
37
+ })) as Record<string, unknown>;
38
+
39
+ expect(result.ok).toBe(true);
40
+ expect(result.action).toBe("ingest");
41
+ expect(result.mode).toBe("text");
42
+ expect(result.saved).toBe(true);
43
+
44
+ // Verify the item was actually saved
45
+ const items = await listIntel("ai-tools", testDir);
46
+ expect(items.length).toBe(1);
47
+ expect(items[0].source).toBe("manual");
48
+ expect(items[0].tags).toContain("ai");
49
+ });
50
+
51
+ it("rejects ingest without url or text", async () => {
52
+ const result = (await executeIntel({
53
+ action: "ingest",
54
+ domain: "ai-tools",
55
+ _dataDir: testDir,
56
+ })) as Record<string, unknown>;
57
+
58
+ expect(result.ok).toBe(false);
59
+ expect(result.error).toMatch(/ingest requires/i);
60
+ });
61
+ });
@@ -1,17 +1,26 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
1
3
  import { Type } from "@sinclair/typebox";
2
4
  import { loadProfile } from "../modules/profile/creator-profile.js";
3
5
  import { runIntelPull } from "../modules/intel/intel-engine.js";
4
- import { listIntel, archiveExpiredIntel } from "../storage/pipeline-store.js";
6
+ import { listIntel, archiveExpiredIntel, saveIntel, type IntelItem } from "../storage/pipeline-store.js";
5
7
 
6
8
  export const intelSchema = Type.Object({
7
9
  action: Type.Unsafe<string>({
8
10
  type: "string",
9
- enum: ["pull", "list", "clean"],
10
- description: "Action to perform: pull (collect intel), list (show saved intel), clean (archive expired)",
11
+ enum: ["pull", "list", "clean", "ingest"],
12
+ description:
13
+ "Action to perform: pull (collect intel), list (show saved intel), clean (archive expired), ingest (manually add url/text/memory)",
11
14
  }),
12
15
  domain: Type.Optional(Type.String({ description: "Filter by domain" })),
13
16
  source: Type.Optional(Type.String({ description: "Filter to specific source(s), comma-separated" })),
14
17
  keywords: Type.Optional(Type.Array(Type.String(), { description: "Override keywords for pull" })),
18
+ url: Type.Optional(Type.String({ description: "URL to ingest as intel (ingest action)" })),
19
+ text: Type.Optional(Type.String({ description: "Raw text to ingest as intel (ingest action)" })),
20
+ memory_paths: Type.Optional(
21
+ Type.Array(Type.String(), { description: "Paths to memory dirs to harvest (ingest action)" }),
22
+ ),
23
+ tags: Type.Optional(Type.Array(Type.String(), { description: "Tags for ingested intel" })),
15
24
  _dataDir: Type.Optional(Type.String()),
16
25
  });
17
26
 
@@ -54,6 +63,10 @@ export async function executeIntel(params: Record<string, unknown>) {
54
63
  totalSaved: result.totalSaved,
55
64
  bySource: result.bySource,
56
65
  errors: result.errors.length > 0 ? result.errors.slice(0, 10) : undefined,
66
+ _triggerSync: result.totalSaved > 0,
67
+ _syncHint: result.totalSaved > 0
68
+ ? "新增情报已入库。请运行 knowledge-sync 同步知识库。"
69
+ : undefined,
57
70
  };
58
71
  }
59
72
 
@@ -86,6 +99,101 @@ export async function executeIntel(params: Record<string, unknown>) {
86
99
  };
87
100
  }
88
101
 
102
+ case "ingest": {
103
+ const url = params.url as string | undefined;
104
+ const text = params.text as string | undefined;
105
+ const memoryPaths = params.memory_paths as string[] | undefined;
106
+ const tags = (params.tags as string[]) ?? [];
107
+ const now = new Date().toISOString();
108
+
109
+ if (text) {
110
+ const item: IntelItem = {
111
+ title: text.slice(0, 60).replace(/\n/g, " "),
112
+ domain: domain ?? "general",
113
+ source: "manual",
114
+ collectedAt: now,
115
+ relevance: 70,
116
+ tags,
117
+ expiresAfter: 365,
118
+ summary: text,
119
+ keyPoints: [],
120
+ topicPotential: "",
121
+ };
122
+ const filePath = await saveIntel(item, dataDir);
123
+ return { ok: true, action: "ingest", mode: "text", saved: true, filePath, _triggerSync: true, _syncHint: "新素材已入库。请运行 knowledge-sync 同步知识库。" };
124
+ }
125
+
126
+ if (url) {
127
+ let hostname: string;
128
+ try {
129
+ hostname = new URL(url).hostname;
130
+ } catch {
131
+ hostname = url.slice(0, 40);
132
+ }
133
+ const item: IntelItem = {
134
+ title: `Ingested from ${hostname}`,
135
+ domain: domain ?? "general",
136
+ source: "manual",
137
+ sourceUrl: url,
138
+ collectedAt: now,
139
+ relevance: 60,
140
+ tags,
141
+ expiresAfter: 365,
142
+ summary: `Placeholder for content from ${url}. Fetch and summarize separately.`,
143
+ keyPoints: [],
144
+ topicPotential: "",
145
+ };
146
+ const filePath = await saveIntel(item, dataDir);
147
+ return { ok: true, action: "ingest", mode: "url", saved: true, filePath, _triggerSync: true, _syncHint: "新素材已入库。请运行 knowledge-sync 同步知识库。" };
148
+ }
149
+
150
+ if (memoryPaths && memoryPaths.length > 0) {
151
+ let scanned = 0;
152
+ let extracted = 0;
153
+ let skipped = 0;
154
+ const savedPaths: string[] = [];
155
+
156
+ for (const memDir of memoryPaths) {
157
+ let files: string[];
158
+ try {
159
+ files = await fs.readdir(memDir);
160
+ } catch {
161
+ skipped++;
162
+ continue;
163
+ }
164
+ for (const f of files) {
165
+ if (!f.endsWith(".md") || f === "MEMORY.md") continue;
166
+ scanned++;
167
+ const filePath = path.join(memDir, f);
168
+ const content = await fs.readFile(filePath, "utf-8");
169
+ if (content.length < 100) {
170
+ skipped++;
171
+ continue;
172
+ }
173
+ const item: IntelItem = {
174
+ title: f.replace(/\.md$/, "").replace(/[-_]/g, " "),
175
+ domain: domain ?? "general",
176
+ source: "manual",
177
+ collectedAt: now,
178
+ relevance: 50,
179
+ tags: [...tags, "memory-harvest"],
180
+ expiresAfter: 730,
181
+ summary: content.slice(0, 500),
182
+ keyPoints: [],
183
+ topicPotential: "",
184
+ };
185
+ const saved = await saveIntel(item, dataDir);
186
+ savedPaths.push(saved);
187
+ extracted++;
188
+ }
189
+ }
190
+
191
+ return { ok: true, action: "ingest", mode: "memory", scanned, extracted, skipped, savedPaths, _triggerSync: extracted > 0, _syncHint: extracted > 0 ? "新素材已入库。请运行 knowledge-sync 同步知识库。" : undefined };
192
+ }
193
+
194
+ return { ok: false, error: "ingest requires url, text, or memory_paths" };
195
+ }
196
+
89
197
  default:
90
198
  return { ok: false, error: `Unknown action: ${action}` };
91
199
  }
@@ -145,7 +145,8 @@ export function registerAllTools(runner: ToolRunner): void {
145
145
  name: "autocrew_intel",
146
146
  label: "AutoCrew 灵感源",
147
147
  description:
148
- "Inspiration source pipeline. Actions: pull (collect from web/RSS/trends), list (show saved inspiration), clean (archive expired).",
148
+ "Inspiration source pipeline. Actions: pull (collect from web/RSS/trends), " +
149
+ "list (show saved), clean (archive expired), ingest (manually add url/text/memory).",
149
150
  parameters: intelSchema,
150
151
  execute: executeIntel,
151
152
  });