@gobing-ai/knowledge-kit 0.0.2 โ 0.0.3
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/.claude-plugin/marketplace.json +15 -0
- package/package.json +6 -3
- package/plugins/generations/content-gen/package.json +15 -0
- package/plugins/generations/content-gen/plugin.json +7 -0
- package/plugins/generations/content-gen/src/agent-json.ts +12 -0
- package/plugins/generations/content-gen/src/index.ts +52 -0
- package/plugins/generations/content-gen/src/storm.ts +333 -0
- package/plugins/generations/content-gen/tsconfig.json +4 -0
- package/plugins/ingestions/karakeep-local/plugin.json +7 -0
- package/plugins/ingestions/karakeep-local/src/index.ts +170 -0
- package/plugins/kk/README.md +17 -0
- package/plugins/kk/agents/kk-judge-compliance.md +37 -0
- package/plugins/kk/agents/kk-judge-tech.md +35 -0
- package/plugins/kk/agents/kk-judge-tone.md +37 -0
- package/plugins/kk/hooks/README.md +3 -0
- package/plugins/kk/plugin.json +5 -0
- package/plugins/kk/rules/README.md +4 -0
- package/plugins/kk/skills/kk-judge/SKILL.md +133 -0
- package/plugins/kk/skills/kk-judge/references/rubrics.md +105 -0
- package/plugins/kk/skills/kk-judge/references/workflow-integration.md +77 -0
- package/plugins/kk/skills/kk-topic/SKILL.md +169 -0
- package/plugins/publishings/qiita-pub/package.json +16 -0
- package/plugins/publishings/qiita-pub/plugin.json +7 -0
- package/plugins/publishings/qiita-pub/src/index.ts +262 -0
- package/plugins/publishings/qiita-pub/tsconfig.json +4 -0
- package/plugins/publishings/surfdash-pub/package.json +16 -0
- package/plugins/publishings/surfdash-pub/plugin.json +7 -0
- package/plugins/publishings/surfdash-pub/src/index.ts +200 -0
- package/plugins/publishings/surfdash-pub/tsconfig.json +4 -0
- package/plugins/publishings/zenn-pub/package.json +16 -0
- package/plugins/publishings/zenn-pub/plugin.json +7 -0
- package/plugins/publishings/zenn-pub/src/index.ts +310 -0
- package/plugins/publishings/zenn-pub/tsconfig.json +4 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { parseArgs } from 'node:util';
|
|
5
|
+
import { type Content, ContentSchema, type Result, ResultSchema } from '@gobing-ai/kk-core';
|
|
6
|
+
import { FakeFileCliTransport, type PublishTransport, type PublishTransportPayload } from '@gobing-ai/publish-harness';
|
|
7
|
+
import { logger } from '@gobing-ai/utils';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Converts a string into a lowercase hyphenated tag (slug) per the publish spec tags convention.
|
|
11
|
+
*/
|
|
12
|
+
export function slugify(str: string): string {
|
|
13
|
+
return str
|
|
14
|
+
.toLowerCase()
|
|
15
|
+
.replace(/[^\w\s-]/g, '')
|
|
16
|
+
.trim()
|
|
17
|
+
.replace(/[\s_]+/g, '-');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Generates a Zenn article slug from a title.
|
|
22
|
+
*
|
|
23
|
+
* Zenn slugs must be lowercase letters, numbers, hyphens, and underscores,
|
|
24
|
+
* with a minimum length of 12 characters (audited from wt-publish-to-zenn).
|
|
25
|
+
*/
|
|
26
|
+
export function generateZennSlug(title: string): string {
|
|
27
|
+
let slug = title
|
|
28
|
+
.toLowerCase()
|
|
29
|
+
.replace(/[^\w\s-]/g, '-')
|
|
30
|
+
.replace(/\s+/g, '-')
|
|
31
|
+
.replace(/-+/g, '-')
|
|
32
|
+
.trim();
|
|
33
|
+
slug = slug.replace(/^-+|-+$/g, '');
|
|
34
|
+
if (slug.length < 12) {
|
|
35
|
+
// Timestamp suffix keeps generated slugs unique; an all-punctuation title
|
|
36
|
+
// collapses to an empty base, so drop the separator hyphen and pad to the
|
|
37
|
+
// 12-char minimum โ generated slugs must always satisfy ZENN_SLUG_PATTERN.
|
|
38
|
+
slug = `${slug}-${Date.now().toString(36)}`.replace(/^-+/, '');
|
|
39
|
+
if (slug.length < 12) {
|
|
40
|
+
slug = slug.padEnd(12, '0');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (slug.length > 100) {
|
|
44
|
+
slug = slug.substring(0, 100);
|
|
45
|
+
}
|
|
46
|
+
return slug;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const ZENN_SLUG_PATTERN = /^[a-z0-9-_]{12,100}$/;
|
|
50
|
+
|
|
51
|
+
/** Resolved Zenn article fields derived from a Content payload. */
|
|
52
|
+
interface ZennArticleFields {
|
|
53
|
+
title: string;
|
|
54
|
+
slug: string;
|
|
55
|
+
type: 'tech' | 'idea';
|
|
56
|
+
emoji: string;
|
|
57
|
+
topics: string[];
|
|
58
|
+
published: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Resolves Content into Zenn article fields, failing loud on invalid Zenn-specific values.
|
|
63
|
+
*/
|
|
64
|
+
function resolveArticleFields(content: Content): ZennArticleFields {
|
|
65
|
+
let title = content.title;
|
|
66
|
+
if (!title) {
|
|
67
|
+
const match = content.body.match(/^#\s+(.+)$/m);
|
|
68
|
+
title = match?.[1] ? match[1].trim() : 'Untitled Post';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const metadata = content.metadata;
|
|
72
|
+
const rawType = metadata?.type;
|
|
73
|
+
let type: 'tech' | 'idea' = 'tech';
|
|
74
|
+
if (rawType !== undefined) {
|
|
75
|
+
if (rawType !== 'tech' && rawType !== 'idea') {
|
|
76
|
+
throw new Error(`Invalid Zenn article type: ${String(rawType)}. Must be 'tech' or 'idea'.`);
|
|
77
|
+
}
|
|
78
|
+
type = rawType;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const emoji = typeof metadata?.emoji === 'string' && metadata.emoji ? metadata.emoji : '๐';
|
|
82
|
+
|
|
83
|
+
const rawTags: unknown[] = Array.isArray(metadata?.tags)
|
|
84
|
+
? metadata.tags
|
|
85
|
+
: Array.isArray(metadata?.keywords)
|
|
86
|
+
? metadata.keywords
|
|
87
|
+
: [];
|
|
88
|
+
const topics = rawTags
|
|
89
|
+
.filter((t): t is string => typeof t === 'string' && t.trim().length > 0)
|
|
90
|
+
.map((t: string) => slugify(t));
|
|
91
|
+
|
|
92
|
+
const published = typeof metadata?.published === 'boolean' ? metadata.published : true;
|
|
93
|
+
|
|
94
|
+
let slug: string;
|
|
95
|
+
if (typeof metadata?.slug === 'string' && metadata.slug.trim()) {
|
|
96
|
+
slug = metadata.slug.trim();
|
|
97
|
+
if (!ZENN_SLUG_PATTERN.test(slug)) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
`Invalid Zenn slug: "${slug}". Must be lowercase letters, numbers, hyphens, underscores only (12-100 chars)`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
slug = generateZennSlug(title);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return { title, slug, type, emoji, topics, published };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Renders Zenn markdown (frontmatter + body) from already-resolved article fields.
|
|
111
|
+
* Splitting from `mapContentToZennMarkdown` lets `publish` resolve fields once
|
|
112
|
+
* so the slug stays stable across the CLI invocation and the article file write.
|
|
113
|
+
*/
|
|
114
|
+
function renderZennMarkdown(content: Content, fields: ZennArticleFields): string {
|
|
115
|
+
const { title, type, emoji, topics, published, slug } = fields;
|
|
116
|
+
|
|
117
|
+
const frontmatter: Record<string, unknown> = { title, type, emoji, topics, published, slug };
|
|
118
|
+
const yamlLines = [
|
|
119
|
+
'---',
|
|
120
|
+
...Object.entries(frontmatter).map(([key, value]) => {
|
|
121
|
+
if (Array.isArray(value)) {
|
|
122
|
+
return `${key}: [${value.map((v) => JSON.stringify(v)).join(', ')}]`;
|
|
123
|
+
}
|
|
124
|
+
if (typeof value === 'boolean') {
|
|
125
|
+
return `${key}: ${value}`;
|
|
126
|
+
}
|
|
127
|
+
return `${key}: ${JSON.stringify(value)}`;
|
|
128
|
+
}),
|
|
129
|
+
'---',
|
|
130
|
+
];
|
|
131
|
+
return `${yamlLines.join('\n')}\n\n${content.body.trim()}\n`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Maps Content object into Zenn markdown with target frontmatter
|
|
136
|
+
* (title, type, emoji, topics, published, slug) per the publish spec.
|
|
137
|
+
*/
|
|
138
|
+
export function mapContentToZennMarkdown(content: Content): string {
|
|
139
|
+
return renderZennMarkdown(content, resolveArticleFields(content));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* FileCli transport invoking the Zenn CLI through child processes in the
|
|
144
|
+
* configured Zenn/GitHub repository, mirroring the wt-publish-to-zenn flow.
|
|
145
|
+
*/
|
|
146
|
+
export class ZennCliFileCliTransport implements PublishTransport {
|
|
147
|
+
public readonly kind = 'file-cli';
|
|
148
|
+
|
|
149
|
+
public async publish(payload: PublishTransportPayload): Promise<Result> {
|
|
150
|
+
const content = payload.content;
|
|
151
|
+
const fields = resolveArticleFields(content);
|
|
152
|
+
const { title, slug, type, emoji, published } = fields;
|
|
153
|
+
const markdown = renderZennMarkdown(content, fields);
|
|
154
|
+
|
|
155
|
+
const repo = process.env.ZENN_REPO;
|
|
156
|
+
if (!repo) {
|
|
157
|
+
return ResultSchema.parse({
|
|
158
|
+
ok: false,
|
|
159
|
+
target: 'zenn',
|
|
160
|
+
error: 'ZENN_REPO environment variable is required: path to the Zenn/GitHub repository',
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
const repoPath = repo.startsWith('~') ? join(homedir(), repo.slice(2)) : repo;
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
await mkdir(repoPath, { recursive: true });
|
|
167
|
+
|
|
168
|
+
const cliArgs = ['--yes', 'zenn', 'new:article', '--slug', slug, '--title', title, '--type', type];
|
|
169
|
+
if (emoji) {
|
|
170
|
+
cliArgs.push('--emoji', emoji);
|
|
171
|
+
}
|
|
172
|
+
const [createCode, , createStderr] = await this.runCli('npx', cliArgs, repoPath);
|
|
173
|
+
if (createCode !== 0) {
|
|
174
|
+
return ResultSchema.parse({
|
|
175
|
+
ok: false,
|
|
176
|
+
target: 'zenn',
|
|
177
|
+
error: `Zenn CLI failed (exit ${createCode}): ${createStderr.trim() || 'Unknown error'}`,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const articleDir = join(repoPath, 'articles');
|
|
182
|
+
await mkdir(articleDir, { recursive: true });
|
|
183
|
+
const articlePath = join(articleDir, `${slug}.md`);
|
|
184
|
+
await writeFile(articlePath, markdown, 'utf-8');
|
|
185
|
+
|
|
186
|
+
if (published) {
|
|
187
|
+
const gitSteps: string[][] = [
|
|
188
|
+
['add', `articles/${slug}.md`],
|
|
189
|
+
['commit', '-m', `Add article: ${title}`],
|
|
190
|
+
['push'],
|
|
191
|
+
];
|
|
192
|
+
for (const gitArgs of gitSteps) {
|
|
193
|
+
const [code, , stderr] = await this.runCli('git', gitArgs, repoPath);
|
|
194
|
+
if (code !== 0) {
|
|
195
|
+
return ResultSchema.parse({
|
|
196
|
+
ok: false,
|
|
197
|
+
target: 'zenn',
|
|
198
|
+
error: `git ${gitArgs.join(' ')} failed (exit ${code}): ${stderr.trim() || 'Unknown error'}`,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return ResultSchema.parse({
|
|
205
|
+
ok: true,
|
|
206
|
+
target: 'zenn',
|
|
207
|
+
url: `https://zenn.dev/articles/${slug}`,
|
|
208
|
+
id: slug,
|
|
209
|
+
message: published
|
|
210
|
+
? 'Published to Zenn via Zenn CLI (article committed and pushed to GitHub)'
|
|
211
|
+
: 'Zenn draft article created (published: false; commit and push to deploy)',
|
|
212
|
+
});
|
|
213
|
+
} catch (err: unknown) {
|
|
214
|
+
return ResultSchema.parse({
|
|
215
|
+
ok: false,
|
|
216
|
+
target: 'zenn',
|
|
217
|
+
error: `Zenn transport error: ${err instanceof Error ? err.message : String(err)}`,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private async runCli(cmd: string, args: string[], cwd: string): Promise<[number, string, string]> {
|
|
223
|
+
const proc = Bun.spawn([cmd, ...args], { cwd, env: process.env, stdout: 'pipe', stderr: 'pipe' });
|
|
224
|
+
const [exitCode, stdout, stderr] = await Promise.all([
|
|
225
|
+
proc.exited,
|
|
226
|
+
new Response(proc.stdout).text(),
|
|
227
|
+
new Response(proc.stderr).text(),
|
|
228
|
+
]);
|
|
229
|
+
return [exitCode, stdout, stderr];
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Resolves default PublishTransport based on environment configuration.
|
|
235
|
+
*/
|
|
236
|
+
export function getPublishTransport(): PublishTransport {
|
|
237
|
+
if (process.env.KNOWLEDGE_KIT_PUBLISH_TRANSPORT === 'fake') {
|
|
238
|
+
return new FakeFileCliTransport();
|
|
239
|
+
}
|
|
240
|
+
return new ZennCliFileCliTransport();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Writes a Result JSON to the output path, creating the directory as needed.
|
|
245
|
+
*/
|
|
246
|
+
async function writePublishResult(outputPath: string, result: Result): Promise<void> {
|
|
247
|
+
const outDir = dirname(outputPath);
|
|
248
|
+
if (outDir && outDir !== '.') {
|
|
249
|
+
await mkdir(outDir, { recursive: true });
|
|
250
|
+
}
|
|
251
|
+
await writeFile(outputPath, JSON.stringify(result, null, 2), 'utf-8');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Processes input Content and writes output Result via transport.
|
|
256
|
+
*
|
|
257
|
+
* Validation failure and transport failure both write a fail-loud Result
|
|
258
|
+
* (ok: false + error) to the output path and reject (spec ยง2.5).
|
|
259
|
+
*/
|
|
260
|
+
export async function processPublishIO(
|
|
261
|
+
inputPath: string,
|
|
262
|
+
outputPath: string,
|
|
263
|
+
transport: PublishTransport = getPublishTransport(),
|
|
264
|
+
): Promise<void> {
|
|
265
|
+
await rm(outputPath, { force: true });
|
|
266
|
+
|
|
267
|
+
let content: Content;
|
|
268
|
+
try {
|
|
269
|
+
const rawInput = await readFile(inputPath, 'utf-8');
|
|
270
|
+
content = ContentSchema.parse(JSON.parse(rawInput));
|
|
271
|
+
} catch (error: unknown) {
|
|
272
|
+
const message = `Invalid Content input: ${error instanceof Error ? error.message : String(error)}`;
|
|
273
|
+
await writePublishResult(outputPath, ResultSchema.parse({ ok: false, target: 'zenn', error: message }));
|
|
274
|
+
throw new Error(message);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const result = await transport.publish({ content });
|
|
278
|
+
const validatedResult = ResultSchema.parse(result);
|
|
279
|
+
await writePublishResult(outputPath, validatedResult);
|
|
280
|
+
|
|
281
|
+
if (!validatedResult.ok) {
|
|
282
|
+
throw new Error(validatedResult.error ?? 'Publish failed');
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export async function main(): Promise<number> {
|
|
287
|
+
const { values } = parseArgs({
|
|
288
|
+
options: {
|
|
289
|
+
in: { type: 'string' },
|
|
290
|
+
out: { type: 'string' },
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
if (!values.in || !values.out) {
|
|
295
|
+
logger.error('Missing required arguments: --in and --out');
|
|
296
|
+
return 1;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
try {
|
|
300
|
+
await processPublishIO(values.in, values.out);
|
|
301
|
+
return 0;
|
|
302
|
+
} catch (error: unknown) {
|
|
303
|
+
logger.error(`zenn-pub failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
304
|
+
return 1;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (import.meta.main) {
|
|
309
|
+
process.exit(await main());
|
|
310
|
+
}
|