@gobing-ai/knowledge-kit 0.0.2 → 0.0.4

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 (33) hide show
  1. package/.claude-plugin/marketplace.json +15 -0
  2. package/package.json +6 -3
  3. package/plugins/generations/content-gen/package.json +15 -0
  4. package/plugins/generations/content-gen/plugin.json +7 -0
  5. package/plugins/generations/content-gen/src/agent-json.ts +12 -0
  6. package/plugins/generations/content-gen/src/index.ts +52 -0
  7. package/plugins/generations/content-gen/src/storm.ts +333 -0
  8. package/plugins/generations/content-gen/tsconfig.json +4 -0
  9. package/plugins/ingestions/karakeep-local/plugin.json +7 -0
  10. package/plugins/ingestions/karakeep-local/src/index.ts +170 -0
  11. package/plugins/kk/README.md +17 -0
  12. package/plugins/kk/agents/kk-judge-compliance.md +37 -0
  13. package/plugins/kk/agents/kk-judge-tech.md +35 -0
  14. package/plugins/kk/agents/kk-judge-tone.md +37 -0
  15. package/plugins/kk/hooks/README.md +3 -0
  16. package/plugins/kk/plugin.json +5 -0
  17. package/plugins/kk/rules/README.md +4 -0
  18. package/plugins/kk/skills/kk-judge/SKILL.md +133 -0
  19. package/plugins/kk/skills/kk-judge/references/rubrics.md +105 -0
  20. package/plugins/kk/skills/kk-judge/references/workflow-integration.md +77 -0
  21. package/plugins/kk/skills/kk-topic/SKILL.md +169 -0
  22. package/plugins/publishings/qiita-pub/package.json +16 -0
  23. package/plugins/publishings/qiita-pub/plugin.json +7 -0
  24. package/plugins/publishings/qiita-pub/src/index.ts +262 -0
  25. package/plugins/publishings/qiita-pub/tsconfig.json +4 -0
  26. package/plugins/publishings/surfdash-pub/package.json +16 -0
  27. package/plugins/publishings/surfdash-pub/plugin.json +7 -0
  28. package/plugins/publishings/surfdash-pub/src/index.ts +200 -0
  29. package/plugins/publishings/surfdash-pub/tsconfig.json +4 -0
  30. package/plugins/publishings/zenn-pub/package.json +16 -0
  31. package/plugins/publishings/zenn-pub/plugin.json +7 -0
  32. package/plugins/publishings/zenn-pub/src/index.ts +310 -0
  33. package/plugins/publishings/zenn-pub/tsconfig.json +4 -0
@@ -0,0 +1,262 @@
1
+ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
2
+ import { dirname } from 'node:path';
3
+ import { parseArgs } from 'node:util';
4
+ import { type Content, ContentSchema, type Result, ResultSchema } from '@gobing-ai/kk-core';
5
+ import { FakeFileCliTransport, type PublishTransport, type PublishTransportPayload } from '@gobing-ai/publish-harness';
6
+ import { logger } from '@gobing-ai/utils';
7
+
8
+ /** Qiita API v2 article-create endpoint (audited from wt-publish-to-qiita). */
9
+ const QIITA_ITEMS_URL = 'https://qiita.com/api/v2/items';
10
+
11
+ /**
12
+ * Converts a string into a lowercase hyphenated tag (slug) per the publish spec tags convention.
13
+ * Qiita tag names allow only alphanumeric, hyphen, and underscore, so the slugified form is
14
+ * always API-acceptable.
15
+ */
16
+ export function slugify(str: string): string {
17
+ return str
18
+ .toLowerCase()
19
+ .replace(/[^\w\s-]/g, '')
20
+ .trim()
21
+ .replace(/[\s_]+/g, '-');
22
+ }
23
+
24
+ /** Resolved Qiita article fields derived from a Content payload. */
25
+ interface QiitaArticleFields {
26
+ title: string;
27
+ tags: string[];
28
+ private: boolean;
29
+ }
30
+
31
+ /**
32
+ * Resolves Content into Qiita article fields (title, slugified tags, private flag).
33
+ * Splitting from `mapContentToQiitaMarkdown` lets `publish` resolve fields once so the
34
+ * rendered article and the API payload agree. Throws when a tag cannot be slugified:
35
+ * the Qiita API accepts only alphanumeric, hyphen, and underscore tag names.
36
+ */
37
+ function resolveQiitaFields(content: Content): QiitaArticleFields {
38
+ let title = content.title;
39
+ if (!title) {
40
+ const match = content.body.match(/^#\s+(.+)$/m);
41
+ title = match?.[1] ? match[1].trim() : 'Untitled Post';
42
+ }
43
+
44
+ const rawTags: unknown[] = Array.isArray(content.metadata?.tags)
45
+ ? content.metadata.tags
46
+ : Array.isArray(content.metadata?.keywords)
47
+ ? content.metadata.keywords
48
+ : [];
49
+ const tagStrings = rawTags.filter((t): t is string => typeof t === 'string' && t.trim().length > 0);
50
+ const sluggedTags = tagStrings.map((t: string) => slugify(t));
51
+
52
+ // A tag that slugifies to empty (non-ASCII or punctuation-only) cannot be sent to the
53
+ // Qiita API; fail loud at mapping time instead of emitting `tags: [{name: ""}]` (HTTP 400).
54
+ const emptySlugIndex = sluggedTags.findIndex((slug) => slug.length === 0);
55
+ if (emptySlugIndex >= 0) {
56
+ const invalidTag = tagStrings[emptySlugIndex] ?? '';
57
+ throw new Error(
58
+ `Invalid tag "${invalidTag}": cannot map to a Qiita tag slug (alphanumeric, hyphen, underscore only)`,
59
+ );
60
+ }
61
+
62
+ // Dedupe after slugification — the Qiita API rejects duplicate tag names in one payload.
63
+ const tags = [...new Set(sluggedTags)];
64
+
65
+ // Qiita `private: true` means limited-sharing (the inverse of the shared `published` flag).
66
+ const metadata = content.metadata;
67
+ const isPrivate =
68
+ typeof metadata?.private === 'boolean'
69
+ ? metadata.private
70
+ : typeof metadata?.published === 'boolean'
71
+ ? !metadata.published
72
+ : false;
73
+
74
+ return { title, tags, private: isPrivate };
75
+ }
76
+
77
+ /**
78
+ * Renders Qiita markdown (Qiita CLI frontmatter + body) from already-resolved article fields.
79
+ * The frontmatter carries title, tags, and the private flag; the article body follows
80
+ * verbatim per the publish spec.
81
+ */
82
+ function renderQiitaMarkdown(content: Content, fields: QiitaArticleFields): string {
83
+ const { title, tags, private: isPrivate } = fields;
84
+ const tagYaml = tags.length > 0 ? tags.map((tag) => ` - "${tag}"`).join('\n') : ' []';
85
+ const yamlLines = ['---', `title: ${JSON.stringify(title)}`, 'tags:', tagYaml, `private: ${isPrivate}`, '---'];
86
+ return `${yamlLines.join('\n')}\n\n${content.body.trim()}\n`;
87
+ }
88
+
89
+ /**
90
+ * Maps Content object into Qiita markdown with target frontmatter
91
+ * (title, tags, private flag) and the article body per the publish spec.
92
+ */
93
+ export function mapContentToQiitaMarkdown(content: Content): string {
94
+ return renderQiitaMarkdown(content, resolveQiitaFields(content));
95
+ }
96
+
97
+ /**
98
+ * Token-API transport publishing articles to Qiita via the Qiita API v2
99
+ * (`POST /api/v2/items`), mirroring the wt-publish-to-qiita API flow. The access
100
+ * token is read exclusively from the QIITA_TOKEN environment variable (spec §2.4);
101
+ * the transport stays classified `file-cli` per the E2 File/CLI audit (task 0002).
102
+ */
103
+ export class QiitaApiTransport implements PublishTransport {
104
+ public readonly kind = 'file-cli';
105
+
106
+ public async publish(payload: PublishTransportPayload): Promise<Result> {
107
+ let fields: QiitaArticleFields;
108
+ try {
109
+ fields = resolveQiitaFields(payload.content);
110
+ } catch (error: unknown) {
111
+ return ResultSchema.parse({
112
+ ok: false,
113
+ target: 'qiita',
114
+ error: `Invalid Content: ${error instanceof Error ? error.message : String(error)}`,
115
+ });
116
+ }
117
+
118
+ const token = process.env.QIITA_TOKEN;
119
+ if (!token) {
120
+ return ResultSchema.parse({
121
+ ok: false,
122
+ target: 'qiita',
123
+ error: 'QIITA_TOKEN environment variable is required: Qiita API access token (read_qiita/write_qiita scopes)',
124
+ });
125
+ }
126
+
127
+ try {
128
+ const response = await fetch(QIITA_ITEMS_URL, {
129
+ method: 'POST',
130
+ headers: {
131
+ Authorization: `Bearer ${token}`,
132
+ 'Content-Type': 'application/json',
133
+ },
134
+ body: JSON.stringify({
135
+ title: fields.title,
136
+ body: payload.content.body,
137
+ tags: fields.tags.map((name) => ({ name })),
138
+ private: fields.private,
139
+ }),
140
+ });
141
+
142
+ if (!response.ok) {
143
+ const errorText = (await response.text()).slice(0, 1000);
144
+ return ResultSchema.parse({
145
+ ok: false,
146
+ target: 'qiita',
147
+ error: `Qiita API request failed (HTTP ${response.status} ${response.statusText}): ${
148
+ errorText.trim() || 'Unknown error'
149
+ }`,
150
+ });
151
+ }
152
+
153
+ const data = (await response.json()) as { id?: unknown; url?: unknown };
154
+ if (typeof data.id !== 'string' || typeof data.url !== 'string') {
155
+ return ResultSchema.parse({
156
+ ok: false,
157
+ target: 'qiita',
158
+ error: 'Qiita API response missing required fields (id, url)',
159
+ });
160
+ }
161
+
162
+ return ResultSchema.parse({
163
+ ok: true,
164
+ target: 'qiita',
165
+ url: data.url,
166
+ id: data.id,
167
+ message: 'Published successfully to Qiita via the API v2 token transport',
168
+ });
169
+ } catch (err: unknown) {
170
+ return ResultSchema.parse({
171
+ ok: false,
172
+ target: 'qiita',
173
+ error: `Qiita transport error: ${err instanceof Error ? err.message : String(err)}`,
174
+ });
175
+ }
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Resolves default PublishTransport based on environment configuration.
181
+ */
182
+ export function getPublishTransport(): PublishTransport {
183
+ if (process.env.KNOWLEDGE_KIT_PUBLISH_TRANSPORT === 'fake') {
184
+ return new FakeFileCliTransport();
185
+ }
186
+ return new QiitaApiTransport();
187
+ }
188
+
189
+ /**
190
+ * Writes a Result JSON to the output path, creating the directory as needed.
191
+ */
192
+ async function writePublishResult(outputPath: string, result: Result): Promise<void> {
193
+ const outDir = dirname(outputPath);
194
+ if (outDir && outDir !== '.') {
195
+ await mkdir(outDir, { recursive: true });
196
+ }
197
+ await writeFile(outputPath, JSON.stringify(result, null, 2), 'utf-8');
198
+ }
199
+
200
+ /**
201
+ * Processes input Content and writes output Result via transport.
202
+ *
203
+ * Validation failure and transport failure both write a fail-loud Result
204
+ * (ok: false + error) to the output path and reject (spec §2.5).
205
+ */
206
+ export async function processPublishIO(
207
+ inputPath: string,
208
+ outputPath: string,
209
+ transport: PublishTransport = getPublishTransport(),
210
+ ): Promise<void> {
211
+ await rm(outputPath, { force: true });
212
+
213
+ let content: Content;
214
+ try {
215
+ const rawInput = await readFile(inputPath, 'utf-8');
216
+ content = ContentSchema.parse(JSON.parse(rawInput));
217
+ } catch (error: unknown) {
218
+ const message = `Invalid Content input: ${error instanceof Error ? error.message : String(error)}`;
219
+ await writePublishResult(outputPath, ResultSchema.parse({ ok: false, target: 'qiita', error: message }));
220
+ throw new Error(message);
221
+ }
222
+
223
+ const result = await transport.publish({ content });
224
+ const validatedResult = ResultSchema.parse(result);
225
+ await writePublishResult(outputPath, validatedResult);
226
+
227
+ if (!validatedResult.ok) {
228
+ throw new Error(validatedResult.error ?? 'Publish failed');
229
+ }
230
+ }
231
+
232
+ export async function main(): Promise<number> {
233
+ let values: { in?: string; out?: string };
234
+ try {
235
+ ({ values } = parseArgs({
236
+ options: {
237
+ in: { type: 'string' },
238
+ out: { type: 'string' },
239
+ },
240
+ }));
241
+ } catch (error: unknown) {
242
+ logger.error(`Invalid arguments: ${error instanceof Error ? error.message : String(error)}`);
243
+ return 1;
244
+ }
245
+
246
+ if (!values.in || !values.out) {
247
+ logger.error('Missing required arguments: --in and --out');
248
+ return 1;
249
+ }
250
+
251
+ try {
252
+ await processPublishIO(values.in, values.out);
253
+ return 0;
254
+ } catch (error: unknown) {
255
+ logger.error(`qiita-pub failed: ${error instanceof Error ? error.message : String(error)}`);
256
+ return 1;
257
+ }
258
+ }
259
+
260
+ if (import.meta.main) {
261
+ process.exit(await main());
262
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "../../../tooling/typescript/base.json",
3
+ "include": ["src", "tests"]
4
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "@gobing-ai/surfdash-pub",
3
+ "type": "module",
4
+ "private": true,
5
+ "scripts": {
6
+ "typecheck": "tsc --noEmit"
7
+ },
8
+ "dependencies": {
9
+ "@gobing-ai/kk-core": "workspace:*",
10
+ "@gobing-ai/publish-harness": "workspace:*",
11
+ "@gobing-ai/utils": "workspace:*"
12
+ },
13
+ "devDependencies": {
14
+ "@types/bun": "1.3.14"
15
+ }
16
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "surfdash-pub",
3
+ "kind": "publish",
4
+ "entry": "./src/index.ts",
5
+ "version": "1.0.0",
6
+ "description": "Publish plugin for Surfdash (Surfing SSG) invoking postsurfing CLI via FileCli transport"
7
+ }
@@ -0,0 +1,200 @@
1
+ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
2
+ import { dirname, join } from 'node:path';
3
+ import { parseArgs } from 'node:util';
4
+ import { type Content, ContentSchema, type Result, ResultSchema } from '@gobing-ai/kk-core';
5
+ import type { PublishTransport, PublishTransportPayload } from '@gobing-ai/publish-harness';
6
+ import { logger } from '@gobing-ai/utils';
7
+
8
+ /**
9
+ * Options for mapping Content into Surfing Markdown with frontmatter.
10
+ */
11
+ export interface MapContentOptions {
12
+ /** Target publishing site name or base URL. Defaults to 'surfdash'. */
13
+ targetName?: string;
14
+ }
15
+
16
+ /**
17
+ * Converts a string into a lowercase hyphenated tag (slug).
18
+ */
19
+ export function slugify(str: string): string {
20
+ return str
21
+ .toLowerCase()
22
+ .replace(/[^\w\s-]/g, '')
23
+ .trim()
24
+ .replace(/[\s_]+/g, '-');
25
+ }
26
+
27
+ /**
28
+ * Maps Content object into Surfing frontmatter and body per the publish spec.
29
+ */
30
+ export function mapContentToSurfingMarkdown(content: Content): string {
31
+ let title = content.title;
32
+ if (!title) {
33
+ const match = content.body.match(/^#\s+(.+)$/m);
34
+ title = match?.[1] ? match[1].trim() : 'Untitled Post';
35
+ }
36
+
37
+ let description: string | undefined;
38
+ if (typeof content.metadata?.description === 'string') {
39
+ description = content.metadata.description;
40
+ } else {
41
+ const paragraphs = content.body
42
+ .split(/\n\s*\n/)
43
+ .map((p) => p.replace(/^#+\s+.*$/, '').trim())
44
+ .filter((p) => p.length > 0);
45
+ if (paragraphs.length > 0) {
46
+ description = paragraphs[0];
47
+ }
48
+ }
49
+
50
+ const rawTags: unknown[] = Array.isArray(content.metadata?.tags)
51
+ ? content.metadata.tags
52
+ : Array.isArray(content.metadata?.keywords)
53
+ ? content.metadata.keywords
54
+ : [];
55
+ const tags = rawTags
56
+ .filter((t): t is string => typeof t === 'string' && t.trim().length > 0)
57
+ .map((t: string) => slugify(t));
58
+
59
+ const date = new Date().toISOString();
60
+ const firstRefUrl = content.references?.[0]?.url;
61
+ const canonicalUrl =
62
+ typeof firstRefUrl === 'string'
63
+ ? firstRefUrl
64
+ : typeof content.metadata?.canonicalUrl === 'string'
65
+ ? content.metadata.canonicalUrl
66
+ : undefined;
67
+ const published = typeof content.metadata?.published === 'boolean' ? content.metadata.published : true;
68
+
69
+ const frontmatterObj: Record<string, unknown> = {
70
+ title,
71
+ ...(description ? { description } : {}),
72
+ tags,
73
+ date,
74
+ ...(canonicalUrl ? { canonical_url: canonicalUrl } : {}),
75
+ published,
76
+ };
77
+
78
+ const yamlLines = ['---', ...Object.entries(frontmatterObj).map(([k, v]) => `${k}: ${JSON.stringify(v)}`), '---'];
79
+ return `${yamlLines.join('\n')}\n\n${content.body.trim()}\n`;
80
+ }
81
+
82
+ /**
83
+ * FileCli transport invoking postsurfing CLI.
84
+ */
85
+ export class PostsurfingFileCliTransport implements PublishTransport {
86
+ public readonly kind = 'file-cli';
87
+
88
+ public async publish(payload: PublishTransportPayload): Promise<Result> {
89
+ const markdown = mapContentToSurfingMarkdown(payload.content);
90
+ const tempDir = join(process.cwd(), '.tmp-surfdash');
91
+ await mkdir(tempDir, { recursive: true });
92
+
93
+ const titleSlug = slugify(payload.content.title ?? 'post');
94
+ const fileName = `${titleSlug || 'post'}-${Date.now()}.md`;
95
+ const filePath = join(tempDir, fileName);
96
+
97
+ try {
98
+ await writeFile(filePath, markdown, 'utf-8');
99
+ const postsurfingBin = process.env.POSTSURFING_BIN ?? 'postsurfing';
100
+
101
+ const proc = Bun.spawn([postsurfingBin, 'publish', filePath], {
102
+ stdout: 'pipe',
103
+ stderr: 'pipe',
104
+ });
105
+
106
+ const [exitCode, stdout, stderr] = await Promise.all([
107
+ proc.exited,
108
+ new Response(proc.stdout).text(),
109
+ new Response(proc.stderr).text(),
110
+ ]);
111
+
112
+ if (exitCode !== 0) {
113
+ return ResultSchema.parse({
114
+ ok: false,
115
+ target: 'surfdash',
116
+ error: `postsurfing CLI failed (exit ${exitCode}): ${stderr.trim() || stdout.trim() || 'Unknown error'}`,
117
+ });
118
+ }
119
+
120
+ return ResultSchema.parse({
121
+ ok: true,
122
+ target: 'surfdash',
123
+ url: `https://surfdash.local/posts/${fileName}`,
124
+ message: 'Published successfully to Surfdash via postsurfing CLI',
125
+ });
126
+ } catch (err: unknown) {
127
+ return ResultSchema.parse({
128
+ ok: false,
129
+ target: 'surfdash',
130
+ error: `Postsurfing transport error: ${err instanceof Error ? err.message : String(err)}`,
131
+ });
132
+ } finally {
133
+ await rm(filePath, { force: true }).catch(() => {});
134
+ }
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Resolves default PublishTransport based on environment configuration.
140
+ */
141
+ export function getPublishTransport(): PublishTransport {
142
+ if (process.env.KNOWLEDGE_KIT_PUBLISH_TRANSPORT === 'fake') {
143
+ const { FakeFileCliTransport } = require('@gobing-ai/publish-harness');
144
+ return new FakeFileCliTransport();
145
+ }
146
+ return new PostsurfingFileCliTransport();
147
+ }
148
+
149
+ /**
150
+ * Processes input Content and writes output Result via transport.
151
+ */
152
+ export async function processPublishIO(
153
+ inputPath: string,
154
+ outputPath: string,
155
+ transport: PublishTransport = getPublishTransport(),
156
+ ): Promise<void> {
157
+ await rm(outputPath, { force: true });
158
+ const rawInput = await readFile(inputPath, 'utf-8');
159
+ const content = ContentSchema.parse(JSON.parse(rawInput));
160
+
161
+ const result = await transport.publish({ content });
162
+ const validatedResult = ResultSchema.parse(result);
163
+
164
+ const outDir = dirname(outputPath);
165
+ if (outDir && outDir !== '.') {
166
+ await mkdir(outDir, { recursive: true });
167
+ }
168
+
169
+ await writeFile(outputPath, JSON.stringify(validatedResult, null, 2), 'utf-8');
170
+
171
+ if (!validatedResult.ok) {
172
+ throw new Error(validatedResult.error ?? 'Publish failed');
173
+ }
174
+ }
175
+
176
+ export async function main(): Promise<number> {
177
+ const { values } = parseArgs({
178
+ options: {
179
+ in: { type: 'string' },
180
+ out: { type: 'string' },
181
+ },
182
+ });
183
+
184
+ if (!values.in || !values.out) {
185
+ logger.error('Missing required arguments: --in and --out');
186
+ return 1;
187
+ }
188
+
189
+ try {
190
+ await processPublishIO(values.in, values.out);
191
+ return 0;
192
+ } catch (error: unknown) {
193
+ logger.error(`surfdash-pub failed: ${error instanceof Error ? error.message : String(error)}`);
194
+ return 1;
195
+ }
196
+ }
197
+
198
+ if (import.meta.main) {
199
+ process.exit(await main());
200
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "../../../tooling/typescript/base.json",
3
+ "include": ["src", "tests"]
4
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "@gobing-ai/zenn-pub",
3
+ "type": "module",
4
+ "private": true,
5
+ "scripts": {
6
+ "typecheck": "tsc --noEmit"
7
+ },
8
+ "dependencies": {
9
+ "@gobing-ai/kk-core": "workspace:*",
10
+ "@gobing-ai/publish-harness": "workspace:*",
11
+ "@gobing-ai/utils": "workspace:*"
12
+ },
13
+ "devDependencies": {
14
+ "@types/bun": "1.3.14"
15
+ }
16
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "zenn-pub",
3
+ "kind": "publish",
4
+ "entry": "./src/index.ts",
5
+ "version": "1.0.0",
6
+ "description": "Publish plugin for Zenn (zenn.dev) invoking Zenn CLI via FileCli transport"
7
+ }