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.
- package/package.json +1 -1
- package/skills/content-review/SKILL.md +56 -6
- package/skills/feature-triage/SKILL.md +335 -0
- package/skills/knowledge-sync/SKILL.md +137 -0
- package/skills/onboarding/SKILL.md +6 -3
- package/skills/setup/SKILL.md +8 -3
- package/skills/spawn-writer/SKILL.md +11 -1
- package/skills/teardown/SKILL.md +254 -44
- package/skills/write-script/SKILL.md +154 -1
- package/src/modules/intel/integration.test.ts +9 -7
- package/src/modules/profile/creator-profile.test.ts +51 -0
- package/src/modules/profile/creator-profile.ts +36 -6
- package/src/modules/wiki/wiki.test.ts +213 -0
- package/src/storage/pipeline-store.test.ts +118 -11
- package/src/storage/pipeline-store.ts +175 -20
- package/src/tools/content-save.ts +8 -10
- package/src/tools/intel.test.ts +61 -0
- package/src/tools/intel.ts +111 -3
- package/src/tools/registry.ts +2 -1
|
@@ -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: [
|
|
216
|
-
current: "draft
|
|
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
|
-
|
|
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
|
-
`
|
|
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
|
+
});
|
package/src/tools/intel.ts
CHANGED
|
@@ -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:
|
|
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
|
}
|
package/src/tools/registry.ts
CHANGED
|
@@ -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),
|
|
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
|
});
|