@gobing-ai/knowledge-kit 0.0.7 → 0.0.8
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/dist/index.js +21639 -11931
- package/package.json +4 -1
- package/plugins/generations/content-gen/package.json +2 -1
- package/plugins/generations/content-gen/src/index.ts +9 -8
- package/plugins/generations/content-gen/src/storm.ts +12 -4
- package/plugins/generations/voice-gen/package.json +17 -0
- package/plugins/generations/voice-gen/plugin.json +6 -0
- package/plugins/generations/voice-gen/src/concat.ts +218 -0
- package/plugins/generations/voice-gen/src/index.ts +213 -0
- package/plugins/generations/voice-gen/src/voicebox-client.ts +223 -0
- package/plugins/generations/voice-gen/src/voicescript.ts +365 -0
- package/plugins/generations/voice-gen/tsconfig.json +8 -0
- package/plugins/ingestions/karakeep-local/package.json +17 -0
- package/plugins/ingestions/karakeep-local/src/index.ts +31 -26
- package/plugins/ingestions/karakeep-local/tsconfig.json +4 -0
- package/plugins/ingestions/web-search/package.json +4 -1
- package/plugins/ingestions/web-search/src/index.ts +137 -15
- package/plugins/kk/README.md +9 -3
- package/plugins/kk/commands/workflow-run.md +100 -28
- package/plugins/kk/config.example.yaml +34 -0
- package/plugins/kk/scripts/render-md.ts +8 -3
- package/plugins/kk/skills/{judge → content-judge}/SKILL.md +13 -14
- package/plugins/kk/skills/{judge → content-judge}/references/workflow-integration.md +13 -11
- package/plugins/kk/skills/itc-generating/SKILL.md +147 -0
- package/plugins/kk/skills/itc-generating/references/generic-craft.md +80 -0
- package/plugins/kk/skills/itc-generating/references/platform-english.md +72 -0
- package/plugins/kk/skills/itc-generating/references/platform-wechat.md +60 -0
- package/plugins/kk/skills/itc-generating/references/skill-authoring.md +62 -0
- package/plugins/kk/skills/storm-research/SKILL.md +10 -3
- package/plugins/kk/workflows/judge-gated-publish-example.yaml +101 -0
- package/plugins/kk/workflows/kk-ingest-generate-publish.yaml +72 -0
- package/plugins/kk/workflows/kk-itc.yaml +285 -0
- package/plugins/kk/workflows/kk-solo-podcast.yaml +374 -0
- package/plugins/kk/workflows/validate-voicescript.ts +226 -0
- package/plugins/publishings/emdash-pub/package.json +17 -0
- package/plugins/publishings/emdash-pub/plugin.json +7 -0
- package/plugins/publishings/emdash-pub/src/index.ts +450 -0
- package/plugins/publishings/emdash-pub/tsconfig.json +4 -0
- package/plugins/publishings/qiita-pub/package.json +2 -1
- package/plugins/publishings/qiita-pub/src/index.ts +9 -9
- package/plugins/publishings/surfdash-pub/package.json +2 -1
- package/plugins/publishings/surfdash-pub/src/index.ts +16 -11
- package/plugins/publishings/zenn-pub/package.json +2 -1
- package/plugins/publishings/zenn-pub/src/index.ts +11 -11
- package/plugins/kk/agents/judge-compliance.md +0 -37
- package/plugins/kk/agents/judge-tech.md +0 -35
- package/plugins/kk/agents/judge-tone.md +0 -37
- /package/plugins/kk/skills/{judge → content-judge}/references/rubrics.md +0 -0
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
import { dirname } from 'node:path';
|
|
2
|
+
import { parseArgs } from 'node:util';
|
|
3
|
+
import { type Content, ContentSchema, type Result, ResultSchema } from '@gobing-ai/kk-core';
|
|
4
|
+
import { FakeFileCliTransport, type PublishTransport, type PublishTransportPayload } from '@gobing-ai/publish-harness';
|
|
5
|
+
import { createNodeFileSystem } from '@gobing-ai/ts-runtime';
|
|
6
|
+
import { echoError } from '@gobing-ai/ts-utils';
|
|
7
|
+
|
|
8
|
+
/** EmDash create envelope per docs/design/emdash-pub.md §3 (REST create). */
|
|
9
|
+
export interface EmDashCreateEnvelope {
|
|
10
|
+
data: {
|
|
11
|
+
title: string;
|
|
12
|
+
content: PortableTextBlock[];
|
|
13
|
+
excerpt?: string;
|
|
14
|
+
};
|
|
15
|
+
slug: string;
|
|
16
|
+
status: 'draft' | 'published';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** One inline run of Portable Text; `marks` may carry `strong`/`em`/`link-N`. */
|
|
20
|
+
export interface PortableTextSpan {
|
|
21
|
+
_type: 'span';
|
|
22
|
+
text: string;
|
|
23
|
+
marks?: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Portable Text block; `_type: 'code'` blocks carry `language` + `code` instead of children. */
|
|
27
|
+
export interface PortableTextBlock {
|
|
28
|
+
_type: 'block' | 'code';
|
|
29
|
+
style?: 'normal' | 'h2' | 'h3' | 'h4' | 'blockquote';
|
|
30
|
+
children?: PortableTextSpan[];
|
|
31
|
+
markDefs?: Array<{ _type: 'link'; href: string; _key: string }>;
|
|
32
|
+
listItem?: 'bullet' | 'number';
|
|
33
|
+
language?: string;
|
|
34
|
+
code?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Converts a string into a lowercase hyphenated slug per the publish spec tags convention.
|
|
39
|
+
*/
|
|
40
|
+
export function slugify(str: string): string {
|
|
41
|
+
return str
|
|
42
|
+
.toLowerCase()
|
|
43
|
+
.replace(/[^\w\s-]/g, '')
|
|
44
|
+
.trim()
|
|
45
|
+
.replace(/[\s_]+/g, '-');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Unsupported markdown line detectors (fail loud — never silently drop a construct). */
|
|
49
|
+
const H1_RE = /^#\s/;
|
|
50
|
+
const TABLE_RE = /\|/;
|
|
51
|
+
const HTML_RE = /<[a-zA-Z/!]/;
|
|
52
|
+
const IMAGE_RE = /!\[/;
|
|
53
|
+
|
|
54
|
+
/** Shared mutable state threaded through inline parsing (per-block link key counter). */
|
|
55
|
+
interface InlineState {
|
|
56
|
+
linkIndex: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Parses inline markdown (`**strong**`, `*em*`/`_em_`, `[text](url)`) into Portable Text
|
|
61
|
+
* spans and link markDefs. v1 scope per docs/design/emdash-pub.md §5; nesting is not supported.
|
|
62
|
+
*/
|
|
63
|
+
function parseInline(
|
|
64
|
+
text: string,
|
|
65
|
+
children: PortableTextSpan[],
|
|
66
|
+
markDefs: EmDashCreateEnvelope['data']['content'][number]['markDefs'],
|
|
67
|
+
state: InlineState,
|
|
68
|
+
): void {
|
|
69
|
+
const INLINE = /\*\*([^*]+)\*\*|\*([^*]+)\*|_([^_]+)_|\[([^\]]+)\]\(([^)\s]+)\)/;
|
|
70
|
+
let rest = text;
|
|
71
|
+
const push = (value: string, marks?: string[]): void => {
|
|
72
|
+
if (value.length === 0) return;
|
|
73
|
+
children.push(
|
|
74
|
+
marks && marks.length > 0 ? { _type: 'span', text: value, marks } : { _type: 'span', text: value },
|
|
75
|
+
);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
while (rest.length > 0) {
|
|
79
|
+
const match = rest.match(INLINE);
|
|
80
|
+
if (!match) {
|
|
81
|
+
push(rest);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const index = match.index ?? 0;
|
|
85
|
+
push(rest.slice(0, index));
|
|
86
|
+
if (match[1] !== undefined) {
|
|
87
|
+
push(match[1], ['strong']);
|
|
88
|
+
} else if (match[2] !== undefined || match[3] !== undefined) {
|
|
89
|
+
push(match[2] ?? match[3] ?? '', ['em']);
|
|
90
|
+
} else if (match[4] !== undefined && match[5] !== undefined) {
|
|
91
|
+
const key = `link-${state.linkIndex}`;
|
|
92
|
+
state.linkIndex += 1;
|
|
93
|
+
push(match[4], [key]);
|
|
94
|
+
if (markDefs) markDefs.push({ _type: 'link', href: match[5], _key: key });
|
|
95
|
+
}
|
|
96
|
+
rest = rest.slice(index + match[0].length);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Builds a text block, parsing inline marks into children/markDefs. */
|
|
101
|
+
function textBlock(style: NonNullable<PortableTextBlock['style']>, text: string): PortableTextBlock {
|
|
102
|
+
const children: PortableTextSpan[] = [];
|
|
103
|
+
const markDefs: PortableTextBlock['markDefs'] = [];
|
|
104
|
+
parseInline(text, children, markDefs, { linkIndex: 0 });
|
|
105
|
+
const block: PortableTextBlock = { _type: 'block', style, children };
|
|
106
|
+
if (markDefs && markDefs.length > 0) block.markDefs = markDefs;
|
|
107
|
+
return block;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Maps markdown into Portable Text blocks (v1 construct set per docs/design/emdash-pub.md §5):
|
|
112
|
+
* paragraphs, ATX `##`–`####` headings, blockquotes, `- ` / `* ` bullets, `1. ` numbered
|
|
113
|
+
* lists, and fenced ` ```lang ` code. Unsupported constructs — leftover `# ` h1, pipe tables,
|
|
114
|
+
* raw HTML, and `![` images — throw instead of being dropped.
|
|
115
|
+
*/
|
|
116
|
+
export function markdownToPortableText(markdown: string): PortableTextBlock[] {
|
|
117
|
+
const blocks: PortableTextBlock[] = [];
|
|
118
|
+
const lines = markdown.split('\n');
|
|
119
|
+
let paragraph: string[] = [];
|
|
120
|
+
let codeLanguage: string | undefined;
|
|
121
|
+
let codeLines: string[] = [];
|
|
122
|
+
|
|
123
|
+
const flushParagraph = (): void => {
|
|
124
|
+
const text = paragraph.join('\n').trim();
|
|
125
|
+
paragraph = [];
|
|
126
|
+
if (text.length > 0) blocks.push(textBlock('normal', text));
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
for (const rawLine of lines) {
|
|
130
|
+
const line = rawLine.trimEnd();
|
|
131
|
+
const trimmed = line.trimStart();
|
|
132
|
+
|
|
133
|
+
// Inside a fenced code block: consume verbatim until the closing fence.
|
|
134
|
+
if (codeLanguage !== undefined) {
|
|
135
|
+
if (/^```/.test(trimmed)) {
|
|
136
|
+
const block: PortableTextBlock = { _type: 'code', code: codeLines.join('\n') };
|
|
137
|
+
if (codeLanguage) block.language = codeLanguage;
|
|
138
|
+
blocks.push(block);
|
|
139
|
+
codeLanguage = undefined;
|
|
140
|
+
codeLines = [];
|
|
141
|
+
} else {
|
|
142
|
+
codeLines.push(line);
|
|
143
|
+
}
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (/^```/.test(trimmed)) {
|
|
148
|
+
flushParagraph();
|
|
149
|
+
codeLanguage = trimmed.slice(3).trim();
|
|
150
|
+
codeLines = [];
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Unsupported constructs fail loud (code-block content is exempt — it is verbatim).
|
|
155
|
+
if (H1_RE.test(trimmed)) throw new Error('Unsupported markdown construct: h1');
|
|
156
|
+
if (IMAGE_RE.test(trimmed)) throw new Error('Unsupported markdown construct: image');
|
|
157
|
+
if (TABLE_RE.test(trimmed)) throw new Error('Unsupported markdown construct: table');
|
|
158
|
+
if (HTML_RE.test(trimmed)) throw new Error('Unsupported markdown construct: html');
|
|
159
|
+
|
|
160
|
+
if (trimmed.length === 0) {
|
|
161
|
+
flushParagraph();
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (/^#{2,4}\s/.test(trimmed)) {
|
|
165
|
+
flushParagraph();
|
|
166
|
+
const level = trimmed.match(/^#+/)?.[0].length ?? 2;
|
|
167
|
+
const style = (level === 2 ? 'h2' : level === 3 ? 'h3' : 'h4') as 'h2' | 'h3' | 'h4';
|
|
168
|
+
blocks.push(textBlock(style, trimmed.replace(/^#+\s+/, '').trim()));
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (/^>/.test(trimmed)) {
|
|
172
|
+
flushParagraph();
|
|
173
|
+
blocks.push(textBlock('blockquote', trimmed.replace(/^>\s?/, '').trim()));
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (/^[-*]\s/.test(trimmed)) {
|
|
177
|
+
flushParagraph();
|
|
178
|
+
const block = textBlock('normal', trimmed.replace(/^[-*]\s+/, '').trim());
|
|
179
|
+
block.listItem = 'bullet';
|
|
180
|
+
blocks.push(block);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (/^\d+\.\s/.test(trimmed)) {
|
|
184
|
+
flushParagraph();
|
|
185
|
+
const block = textBlock('normal', trimmed.replace(/^\d+\.\s+/, '').trim());
|
|
186
|
+
block.listItem = 'number';
|
|
187
|
+
blocks.push(block);
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
paragraph.push(trimmed);
|
|
191
|
+
}
|
|
192
|
+
flushParagraph();
|
|
193
|
+
return blocks;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Strips trailing slashes from a site origin so URL joins do not double up. */
|
|
197
|
+
function stripTrailingSlashes(url: string): string {
|
|
198
|
+
return url.replace(/\/+$/, '');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** First non-empty paragraph of a markdown body, skipping heading lines (for the excerpt). */
|
|
202
|
+
function firstParagraph(markdown: string): string | undefined {
|
|
203
|
+
const lines = markdown.split('\n');
|
|
204
|
+
const parts: string[] = [];
|
|
205
|
+
for (const line of lines) {
|
|
206
|
+
const trimmed = line.trim();
|
|
207
|
+
if (trimmed.length === 0) {
|
|
208
|
+
if (parts.length > 0) return parts.join(' ').trim();
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (/^#{1,6}\s/.test(trimmed)) continue;
|
|
212
|
+
parts.push(trimmed);
|
|
213
|
+
}
|
|
214
|
+
const result = parts.join(' ').trim();
|
|
215
|
+
return result.length > 0 ? result : undefined;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Maps Content into the EmDash create envelope per docs/design/emdash-pub.md §5:
|
|
220
|
+
* title from `content.title` or the first ATX `# ` heading (else throw), slug from
|
|
221
|
+
* `metadata.slug` or `slugify(title)` (empty → throw), excerpt from `metadata.description`
|
|
222
|
+
* or the first paragraph (omitted when empty), status published only when
|
|
223
|
+
* `metadata.published === true`. When the title is consumed from the body, that `# ` line
|
|
224
|
+
* is dropped before the body is mapped.
|
|
225
|
+
*/
|
|
226
|
+
export function mapContentToEmDashCreate(content: Content): EmDashCreateEnvelope {
|
|
227
|
+
const metadata = content.metadata ?? {};
|
|
228
|
+
let title = typeof content.title === 'string' ? content.title.trim() : '';
|
|
229
|
+
let body = content.body;
|
|
230
|
+
|
|
231
|
+
if (!title) {
|
|
232
|
+
const match = body.match(/^#\s+(.+)$/m);
|
|
233
|
+
if (!match?.[1]?.trim()) {
|
|
234
|
+
throw new Error('Cannot resolve title: set Content.title or start the body with an ATX # heading');
|
|
235
|
+
}
|
|
236
|
+
title = match[1].trim();
|
|
237
|
+
body = body.replace(/^#\s+.*$/m, '');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const metaSlug = typeof metadata.slug === 'string' ? metadata.slug.trim() : '';
|
|
241
|
+
const slug = metaSlug || slugify(title);
|
|
242
|
+
if (!slug) {
|
|
243
|
+
throw new Error('Cannot derive slug from title');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
let excerpt: string | undefined;
|
|
247
|
+
const description = typeof metadata.description === 'string' ? metadata.description.trim() : '';
|
|
248
|
+
if (description) {
|
|
249
|
+
excerpt = description;
|
|
250
|
+
} else {
|
|
251
|
+
excerpt = firstParagraph(body);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const status = metadata.published === true ? 'published' : 'draft';
|
|
255
|
+
|
|
256
|
+
const envelope: EmDashCreateEnvelope = {
|
|
257
|
+
data: { title, content: markdownToPortableText(body.trim()) },
|
|
258
|
+
slug,
|
|
259
|
+
status,
|
|
260
|
+
};
|
|
261
|
+
if (excerpt) envelope.data.excerpt = excerpt;
|
|
262
|
+
return envelope;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Token-API transport creating an EmDash post via `POST {origin}/_emdash/api/content/{collection}`
|
|
267
|
+
* with `Authorization: Bearer {token}`. Reads `EMDASH_URL` / `EMDASH_TOKEN` / `EMDASH_COLLECTION`
|
|
268
|
+
* (default `posts`) from the environment (spec §2.4, D3/D4); classified `file-cli` per E2
|
|
269
|
+
* (ADR-008). Fail-loud Result on missing env, mapping failure, network error, non-2xx,
|
|
270
|
+
* `success: false`, or missing `data.item.id` — never leaking the token.
|
|
271
|
+
*/
|
|
272
|
+
export class EmDashApiTransport implements PublishTransport {
|
|
273
|
+
public readonly kind = 'file-cli';
|
|
274
|
+
|
|
275
|
+
public async publish(payload: PublishTransportPayload): Promise<Result> {
|
|
276
|
+
const baseUrl = process.env.EMDASH_URL?.trim();
|
|
277
|
+
if (!baseUrl) {
|
|
278
|
+
return ResultSchema.parse({
|
|
279
|
+
ok: false,
|
|
280
|
+
target: 'emdash',
|
|
281
|
+
error: 'EMDASH_URL environment variable is required',
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const token = process.env.EMDASH_TOKEN?.trim();
|
|
286
|
+
if (!token) {
|
|
287
|
+
return ResultSchema.parse({
|
|
288
|
+
ok: false,
|
|
289
|
+
target: 'emdash',
|
|
290
|
+
error: 'EMDASH_TOKEN environment variable is required',
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
let envelope: EmDashCreateEnvelope;
|
|
295
|
+
try {
|
|
296
|
+
envelope = mapContentToEmDashCreate(payload.content);
|
|
297
|
+
} catch (error: unknown) {
|
|
298
|
+
return ResultSchema.parse({
|
|
299
|
+
ok: false,
|
|
300
|
+
target: 'emdash',
|
|
301
|
+
error: `Invalid Content: ${error instanceof Error ? error.message : String(error)}`,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const collection = process.env.EMDASH_COLLECTION?.trim() || 'posts';
|
|
306
|
+
const endpoint = `${stripTrailingSlashes(baseUrl)}/_emdash/api/content/${encodeURIComponent(collection)}`;
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
const response = await fetch(endpoint, {
|
|
310
|
+
method: 'POST',
|
|
311
|
+
headers: {
|
|
312
|
+
Authorization: `Bearer ${token}`,
|
|
313
|
+
'Content-Type': 'application/json',
|
|
314
|
+
},
|
|
315
|
+
body: JSON.stringify(envelope),
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
let body: { success?: unknown; error?: { code?: unknown }; data?: { item?: { id?: unknown } } } | undefined;
|
|
319
|
+
try {
|
|
320
|
+
body = (await response.json()) as typeof body;
|
|
321
|
+
} catch {
|
|
322
|
+
body = undefined;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (!response.ok || body?.success !== true || typeof body?.data?.item?.id !== 'string') {
|
|
326
|
+
const statusPart = `HTTP ${response.status}`;
|
|
327
|
+
const codePart = body?.error && typeof body.error.code === 'string' ? `, code ${body.error.code}` : '';
|
|
328
|
+
const detail = body ? JSON.stringify(body).slice(0, 1000) : 'Unknown error';
|
|
329
|
+
return ResultSchema.parse({
|
|
330
|
+
ok: false,
|
|
331
|
+
target: 'emdash',
|
|
332
|
+
error: `EmDash API request failed (${statusPart}${codePart}): ${detail}`,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const id = body.data.item.id;
|
|
337
|
+
const result: Result = {
|
|
338
|
+
ok: true,
|
|
339
|
+
target: 'emdash',
|
|
340
|
+
id,
|
|
341
|
+
message: 'Published successfully to EmDash via the REST token transport',
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
const publicPath = process.env.EMDASH_PUBLIC_POST_PATH;
|
|
345
|
+
if (publicPath !== undefined) {
|
|
346
|
+
if (!publicPath.includes('{slug}')) {
|
|
347
|
+
return ResultSchema.parse({
|
|
348
|
+
ok: false,
|
|
349
|
+
target: 'emdash',
|
|
350
|
+
error: 'EMDASH_PUBLIC_POST_PATH environment variable must contain {slug}',
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
result.url = `${stripTrailingSlashes(baseUrl)}${publicPath.replaceAll('{slug}', envelope.slug)}`;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return ResultSchema.parse(result);
|
|
357
|
+
} catch (err: unknown) {
|
|
358
|
+
return ResultSchema.parse({
|
|
359
|
+
ok: false,
|
|
360
|
+
target: 'emdash',
|
|
361
|
+
error: `EmDash transport error: ${err instanceof Error ? err.message : String(err)}`,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Resolves default PublishTransport based on environment configuration.
|
|
369
|
+
*/
|
|
370
|
+
export function getPublishTransport(): PublishTransport {
|
|
371
|
+
if (process.env.KNOWLEDGE_KIT_PUBLISH_TRANSPORT === 'fake') {
|
|
372
|
+
return new FakeFileCliTransport();
|
|
373
|
+
}
|
|
374
|
+
return new EmDashApiTransport();
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Writes a Result JSON to the output path, creating the directory as needed.
|
|
379
|
+
*/
|
|
380
|
+
async function writePublishResult(outputPath: string, result: Result): Promise<void> {
|
|
381
|
+
const outDir = dirname(outputPath);
|
|
382
|
+
if (outDir && outDir !== '.') {
|
|
383
|
+
await createNodeFileSystem().ensureDir(outDir);
|
|
384
|
+
}
|
|
385
|
+
await createNodeFileSystem().writeFile(outputPath, JSON.stringify(result, null, 2));
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Processes input Content and writes output Result via transport.
|
|
390
|
+
*
|
|
391
|
+
* Validation failure and transport failure both write a fail-loud Result
|
|
392
|
+
* (ok: false + error) to the output path and reject (spec §2.5).
|
|
393
|
+
*/
|
|
394
|
+
export async function processPublishIO(
|
|
395
|
+
inputPath: string,
|
|
396
|
+
outputPath: string,
|
|
397
|
+
transport: PublishTransport = getPublishTransport(),
|
|
398
|
+
): Promise<void> {
|
|
399
|
+
await createNodeFileSystem().deleteFile(outputPath);
|
|
400
|
+
|
|
401
|
+
let content: Content;
|
|
402
|
+
try {
|
|
403
|
+
const rawInput = await createNodeFileSystem().readFile(inputPath);
|
|
404
|
+
content = ContentSchema.parse(JSON.parse(rawInput));
|
|
405
|
+
} catch (error: unknown) {
|
|
406
|
+
const message = `Invalid Content input: ${error instanceof Error ? error.message : String(error)}`;
|
|
407
|
+
await writePublishResult(outputPath, ResultSchema.parse({ ok: false, target: 'emdash', error: message }));
|
|
408
|
+
throw new Error(message);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const result = await transport.publish({ content });
|
|
412
|
+
const validatedResult = ResultSchema.parse(result);
|
|
413
|
+
await writePublishResult(outputPath, validatedResult);
|
|
414
|
+
|
|
415
|
+
if (!validatedResult.ok) {
|
|
416
|
+
throw new Error(validatedResult.error ?? 'Publish failed');
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
export async function main(): Promise<number> {
|
|
421
|
+
let values: { in?: string; out?: string };
|
|
422
|
+
try {
|
|
423
|
+
({ values } = parseArgs({
|
|
424
|
+
options: {
|
|
425
|
+
in: { type: 'string' },
|
|
426
|
+
out: { type: 'string' },
|
|
427
|
+
},
|
|
428
|
+
}));
|
|
429
|
+
} catch (error: unknown) {
|
|
430
|
+
echoError(`Invalid arguments: ${error instanceof Error ? error.message : String(error)}`);
|
|
431
|
+
return 1;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (!values.in || !values.out) {
|
|
435
|
+
echoError('Missing required arguments: --in and --out');
|
|
436
|
+
return 1;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
try {
|
|
440
|
+
await processPublishIO(values.in, values.out);
|
|
441
|
+
return 0;
|
|
442
|
+
} catch (error: unknown) {
|
|
443
|
+
echoError(`emdash-pub failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
444
|
+
return 1;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (import.meta.main) {
|
|
449
|
+
process.exit(await main());
|
|
450
|
+
}
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
"dependencies": {
|
|
9
9
|
"@gobing-ai/kk-core": "workspace:*",
|
|
10
10
|
"@gobing-ai/publish-harness": "workspace:*",
|
|
11
|
-
"@gobing-ai/
|
|
11
|
+
"@gobing-ai/ts-runtime": "catalog:",
|
|
12
|
+
"@gobing-ai/ts-utils": "catalog:"
|
|
12
13
|
},
|
|
13
14
|
"devDependencies": {
|
|
14
15
|
"@types/bun": "1.3.14"
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
2
1
|
import { dirname } from 'node:path';
|
|
3
2
|
import { parseArgs } from 'node:util';
|
|
4
3
|
import { type Content, ContentSchema, type Result, ResultSchema } from '@gobing-ai/kk-core';
|
|
5
4
|
import { FakeFileCliTransport, type PublishTransport, type PublishTransportPayload } from '@gobing-ai/publish-harness';
|
|
6
|
-
import {
|
|
5
|
+
import { createNodeFileSystem } from '@gobing-ai/ts-runtime';
|
|
6
|
+
import { echoError } from '@gobing-ai/ts-utils';
|
|
7
7
|
|
|
8
8
|
/** Qiita API v2 article-create endpoint (audited from wt-publish-to-qiita). */
|
|
9
9
|
const QIITA_ITEMS_URL = 'https://qiita.com/api/v2/items';
|
|
@@ -192,9 +192,9 @@ export function getPublishTransport(): PublishTransport {
|
|
|
192
192
|
async function writePublishResult(outputPath: string, result: Result): Promise<void> {
|
|
193
193
|
const outDir = dirname(outputPath);
|
|
194
194
|
if (outDir && outDir !== '.') {
|
|
195
|
-
await
|
|
195
|
+
await createNodeFileSystem().ensureDir(outDir);
|
|
196
196
|
}
|
|
197
|
-
await writeFile(outputPath, JSON.stringify(result, null, 2)
|
|
197
|
+
await createNodeFileSystem().writeFile(outputPath, JSON.stringify(result, null, 2));
|
|
198
198
|
}
|
|
199
199
|
|
|
200
200
|
/**
|
|
@@ -208,11 +208,11 @@ export async function processPublishIO(
|
|
|
208
208
|
outputPath: string,
|
|
209
209
|
transport: PublishTransport = getPublishTransport(),
|
|
210
210
|
): Promise<void> {
|
|
211
|
-
await
|
|
211
|
+
await createNodeFileSystem().deleteFile(outputPath);
|
|
212
212
|
|
|
213
213
|
let content: Content;
|
|
214
214
|
try {
|
|
215
|
-
const rawInput = await readFile(inputPath
|
|
215
|
+
const rawInput = await createNodeFileSystem().readFile(inputPath);
|
|
216
216
|
content = ContentSchema.parse(JSON.parse(rawInput));
|
|
217
217
|
} catch (error: unknown) {
|
|
218
218
|
const message = `Invalid Content input: ${error instanceof Error ? error.message : String(error)}`;
|
|
@@ -239,12 +239,12 @@ export async function main(): Promise<number> {
|
|
|
239
239
|
},
|
|
240
240
|
}));
|
|
241
241
|
} catch (error: unknown) {
|
|
242
|
-
|
|
242
|
+
echoError(`Invalid arguments: ${error instanceof Error ? error.message : String(error)}`);
|
|
243
243
|
return 1;
|
|
244
244
|
}
|
|
245
245
|
|
|
246
246
|
if (!values.in || !values.out) {
|
|
247
|
-
|
|
247
|
+
echoError('Missing required arguments: --in and --out');
|
|
248
248
|
return 1;
|
|
249
249
|
}
|
|
250
250
|
|
|
@@ -252,7 +252,7 @@ export async function main(): Promise<number> {
|
|
|
252
252
|
await processPublishIO(values.in, values.out);
|
|
253
253
|
return 0;
|
|
254
254
|
} catch (error: unknown) {
|
|
255
|
-
|
|
255
|
+
echoError(`qiita-pub failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
256
256
|
return 1;
|
|
257
257
|
}
|
|
258
258
|
}
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
"dependencies": {
|
|
9
9
|
"@gobing-ai/kk-core": "workspace:*",
|
|
10
10
|
"@gobing-ai/publish-harness": "workspace:*",
|
|
11
|
-
"@gobing-ai/
|
|
11
|
+
"@gobing-ai/ts-runtime": "catalog:",
|
|
12
|
+
"@gobing-ai/ts-utils": "catalog:"
|
|
12
13
|
},
|
|
13
14
|
"devDependencies": {
|
|
14
15
|
"@types/bun": "1.3.14"
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
2
1
|
import { dirname, join } from 'node:path';
|
|
3
2
|
import { parseArgs } from 'node:util';
|
|
4
3
|
import { type Content, ContentSchema, type Result, ResultSchema } from '@gobing-ai/kk-core';
|
|
5
4
|
import type { PublishTransport, PublishTransportPayload } from '@gobing-ai/publish-harness';
|
|
6
|
-
import {
|
|
5
|
+
import { createNodeFileSystem } from '@gobing-ai/ts-runtime';
|
|
6
|
+
import { echoError } from '@gobing-ai/ts-utils';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Options for mapping Content into Surfing Markdown with frontmatter.
|
|
@@ -88,14 +88,14 @@ export class PostsurfingFileCliTransport implements PublishTransport {
|
|
|
88
88
|
public async publish(payload: PublishTransportPayload): Promise<Result> {
|
|
89
89
|
const markdown = mapContentToSurfingMarkdown(payload.content);
|
|
90
90
|
const tempDir = join(process.cwd(), '.tmp-surfdash');
|
|
91
|
-
await
|
|
91
|
+
await createNodeFileSystem().ensureDir(tempDir);
|
|
92
92
|
|
|
93
93
|
const titleSlug = slugify(payload.content.title ?? 'post');
|
|
94
94
|
const fileName = `${titleSlug || 'post'}-${Date.now()}.md`;
|
|
95
95
|
const filePath = join(tempDir, fileName);
|
|
96
96
|
|
|
97
97
|
try {
|
|
98
|
-
await writeFile(filePath, markdown
|
|
98
|
+
await createNodeFileSystem().writeFile(filePath, markdown);
|
|
99
99
|
const postsurfingBin = process.env.POSTSURFING_BIN ?? 'postsurfing';
|
|
100
100
|
|
|
101
101
|
const proc = Bun.spawn([postsurfingBin, 'publish', filePath], {
|
|
@@ -130,7 +130,12 @@ export class PostsurfingFileCliTransport implements PublishTransport {
|
|
|
130
130
|
error: `Postsurfing transport error: ${err instanceof Error ? err.message : String(err)}`,
|
|
131
131
|
});
|
|
132
132
|
} finally {
|
|
133
|
-
|
|
133
|
+
// Best-effort temp cleanup: a failure here must not mask the publish result.
|
|
134
|
+
try {
|
|
135
|
+
await createNodeFileSystem().deleteFile(filePath);
|
|
136
|
+
} catch {
|
|
137
|
+
// ignore
|
|
138
|
+
}
|
|
134
139
|
}
|
|
135
140
|
}
|
|
136
141
|
}
|
|
@@ -154,8 +159,8 @@ export async function processPublishIO(
|
|
|
154
159
|
outputPath: string,
|
|
155
160
|
transport: PublishTransport = getPublishTransport(),
|
|
156
161
|
): Promise<void> {
|
|
157
|
-
await
|
|
158
|
-
const rawInput = await readFile(inputPath
|
|
162
|
+
await createNodeFileSystem().deleteFile(outputPath);
|
|
163
|
+
const rawInput = await createNodeFileSystem().readFile(inputPath);
|
|
159
164
|
const content = ContentSchema.parse(JSON.parse(rawInput));
|
|
160
165
|
|
|
161
166
|
const result = await transport.publish({ content });
|
|
@@ -163,10 +168,10 @@ export async function processPublishIO(
|
|
|
163
168
|
|
|
164
169
|
const outDir = dirname(outputPath);
|
|
165
170
|
if (outDir && outDir !== '.') {
|
|
166
|
-
await
|
|
171
|
+
await createNodeFileSystem().ensureDir(outDir);
|
|
167
172
|
}
|
|
168
173
|
|
|
169
|
-
await writeFile(outputPath, JSON.stringify(validatedResult, null, 2)
|
|
174
|
+
await createNodeFileSystem().writeFile(outputPath, JSON.stringify(validatedResult, null, 2));
|
|
170
175
|
|
|
171
176
|
if (!validatedResult.ok) {
|
|
172
177
|
throw new Error(validatedResult.error ?? 'Publish failed');
|
|
@@ -182,7 +187,7 @@ export async function main(): Promise<number> {
|
|
|
182
187
|
});
|
|
183
188
|
|
|
184
189
|
if (!values.in || !values.out) {
|
|
185
|
-
|
|
190
|
+
echoError('Missing required arguments: --in and --out');
|
|
186
191
|
return 1;
|
|
187
192
|
}
|
|
188
193
|
|
|
@@ -190,7 +195,7 @@ export async function main(): Promise<number> {
|
|
|
190
195
|
await processPublishIO(values.in, values.out);
|
|
191
196
|
return 0;
|
|
192
197
|
} catch (error: unknown) {
|
|
193
|
-
|
|
198
|
+
echoError(`surfdash-pub failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
194
199
|
return 1;
|
|
195
200
|
}
|
|
196
201
|
}
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
"dependencies": {
|
|
9
9
|
"@gobing-ai/kk-core": "workspace:*",
|
|
10
10
|
"@gobing-ai/publish-harness": "workspace:*",
|
|
11
|
-
"@gobing-ai/
|
|
11
|
+
"@gobing-ai/ts-runtime": "catalog:",
|
|
12
|
+
"@gobing-ai/ts-utils": "catalog:"
|
|
12
13
|
},
|
|
13
14
|
"devDependencies": {
|
|
14
15
|
"@types/bun": "1.3.14"
|