@gobing-ai/knowledge-kit 0.0.21 → 0.0.22

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 (59) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/dist/index.js +161 -142
  3. package/package.json +1 -1
  4. package/plugins/generations/content-gen/dist/index.js +20 -3
  5. package/plugins/generations/core-facts-gen/dist/index.js +20 -3
  6. package/plugins/generations/daily-article-gen/dist/index.js +20 -3
  7. package/plugins/generations/dailynews-gen/dist/index.js +20 -3
  8. package/plugins/generations/episode-plan-gen/dist/index.js +20 -3
  9. package/plugins/generations/image-gen/dist/index.js +147 -69
  10. package/plugins/generations/image-gen/src/presets.ts +2 -2
  11. package/plugins/generations/image-gen/src/providers/agnes.ts +6 -2
  12. package/plugins/generations/image-gen/src/providers/azure.ts +6 -2
  13. package/plugins/generations/image-gen/src/providers/dashscope.ts +10 -3
  14. package/plugins/generations/image-gen/src/providers/google.ts +70 -29
  15. package/plugins/generations/image-gen/src/providers/huggingface.ts +3 -0
  16. package/plugins/generations/image-gen/src/providers/jimeng.ts +9 -2
  17. package/plugins/generations/image-gen/src/providers/minimax.ts +10 -2
  18. package/plugins/generations/image-gen/src/providers/openai.ts +9 -3
  19. package/plugins/generations/image-gen/src/providers/openrouter.ts +16 -4
  20. package/plugins/generations/image-gen/src/providers/replicate.ts +13 -3
  21. package/plugins/generations/image-gen/src/providers/seedream.ts +4 -1
  22. package/plugins/generations/image-gen/src/providers/timeouts.ts +14 -0
  23. package/plugins/generations/image-gen/src/providers/types.ts +10 -0
  24. package/plugins/generations/image-gen/src/providers/zai.ts +4 -1
  25. package/plugins/generations/news-report-gen/dist/index.js +20 -3
  26. package/plugins/generations/voice-gen/dist/index.js +36 -10
  27. package/plugins/generations/voice-gen/src/voicebox-client.ts +18 -4
  28. package/plugins/generations/voice-gen/src/voicebox-server.ts +2 -1
  29. package/plugins/ingestions/aihot-ingest/dist/index.js +25 -5
  30. package/plugins/ingestions/aihot-ingest/src/client.ts +5 -0
  31. package/plugins/ingestions/horizon-ingest/dist/index.js +20 -3
  32. package/plugins/ingestions/last30days-ingest/dist/index.js +20 -3
  33. package/plugins/ingestions/web-search/dist/index.js +22 -3
  34. package/plugins/ingestions/web-search/src/index.ts +4 -0
  35. package/plugins/kk/plugin.json +1 -1
  36. package/plugins/kk/scripts/kk-workflow-stages.ts +35 -7
  37. package/plugins/kk/scripts/render-md.ts +7 -1
  38. package/plugins/publishings/emdash-pub/dist/index.js +76 -19
  39. package/plugins/publishings/emdash-pub/src/index.ts +4 -0
  40. package/plugins/publishings/infoq-pub/dist/index.js +74 -21
  41. package/plugins/publishings/juejin-pub/dist/index.js +74 -21
  42. package/plugins/publishings/medium-pub/dist/index.js +77 -19
  43. package/plugins/publishings/medium-pub/src/index.ts +5 -0
  44. package/plugins/publishings/podcast-pub/dist/index.js +130 -34
  45. package/plugins/publishings/podcast-pub/src/index.ts +20 -5
  46. package/plugins/publishings/podcast-pub/src/map.ts +11 -0
  47. package/plugins/publishings/podcast-pub/src/show-notes.ts +66 -13
  48. package/plugins/publishings/qiita-pub/dist/index.js +75 -18
  49. package/plugins/publishings/qiita-pub/src/index.ts +4 -0
  50. package/plugins/publishings/substack-pub/dist/index.js +74 -21
  51. package/plugins/publishings/surfdash-pub/dist/index.js +99 -62
  52. package/plugins/publishings/surfdash-pub/src/index.ts +60 -19
  53. package/plugins/publishings/wechatmp-pub/dist/index.js +79 -22
  54. package/plugins/publishings/wechatmp-pub/src/index.ts +12 -3
  55. package/plugins/publishings/x-pub/dist/index.js +56 -37
  56. package/plugins/publishings/x-pub/src/index.ts +4 -0
  57. package/plugins/publishings/xhs-pub/dist/index.js +74 -21
  58. package/plugins/publishings/zenn-pub/dist/index.js +103 -25
  59. package/plugins/publishings/zenn-pub/src/index.ts +43 -9
@@ -21781,6 +21781,22 @@ var ResultSchema = exports_external.object({
21781
21781
  error: exports_external.string().optional(),
21782
21782
  metadata: exports_external.record(exports_external.string(), exports_external.unknown()).optional()
21783
21783
  });
21784
+ var TargetOutcomeSchema = exports_external.object({
21785
+ target: exports_external.string(),
21786
+ ok: exports_external.boolean(),
21787
+ status: exports_external.enum(["published", "failed", "skipped"]),
21788
+ url: exports_external.string().optional(),
21789
+ id: exports_external.string().optional(),
21790
+ error: exports_external.string().optional(),
21791
+ durationMs: exports_external.number().optional(),
21792
+ metadata: exports_external.record(exports_external.string(), exports_external.unknown()).optional()
21793
+ });
21794
+ var AggregateResultSchema = exports_external.object({
21795
+ ok: exports_external.boolean(),
21796
+ summary: exports_external.object({ total: exports_external.number(), succeeded: exports_external.number(), failed: exports_external.number(), skipped: exports_external.number() }),
21797
+ targets: exports_external.array(TargetOutcomeSchema),
21798
+ metadata: exports_external.record(exports_external.string(), exports_external.unknown()).optional()
21799
+ });
21784
21800
  var DocListSchema = exports_external.array(DocSchema);
21785
21801
  var omniVoiceGenContentSchema = ContentSchema.extend({
21786
21802
  format: exports_external.literal("audio"),
@@ -21809,14 +21825,15 @@ var PluginKindSchema = exports_external.enum(PLUGIN_KINDS);
21809
21825
  // ../../packages/kk-core/src/manifest.ts
21810
21826
  init_zod();
21811
21827
  var OperationIOSchema = exports_external.enum(["doc-list", "markdown"]);
21812
- var PluginManifestSchema = exports_external.object({
21828
+ var PluginOperationSchema = exports_external.object({ input: OperationIOSchema, output: OperationIOSchema });
21829
+ var PluginManifestSchema = exports_external.looseObject({
21813
21830
  name: exports_external.string().min(1),
21814
21831
  kind: PluginKindSchema,
21815
21832
  entry: exports_external.string().min(1),
21816
21833
  version: exports_external.string().optional(),
21817
21834
  description: exports_external.string().optional(),
21818
- operations: exports_external.record(exports_external.string(), exports_external.object({ input: OperationIOSchema, output: OperationIOSchema })).optional()
21819
- }).passthrough();
21835
+ operations: exports_external.record(exports_external.string(), PluginOperationSchema).optional()
21836
+ });
21820
21837
  // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.4.39/node_modules/@gobing-ai/ts-runtime/dist/index.js
21821
21838
  init_file_system_node();
21822
21839
  init_config();
@@ -21843,42 +21860,78 @@ function echoError(message, target = defaultStderrTarget ?? processStream("stder
21843
21860
  if (false) {}
21844
21861
  // ../../packages/publish-harness/src/index.ts
21845
21862
  import { homedir } from "os";
21846
- import { join } from "path";
21863
+ import { join, resolve } from "path";
21864
+
21865
+ class FakeTransportCore {
21866
+ callLogs = [];
21867
+ nextResult;
21868
+ setNextResult(result) {
21869
+ this.nextResult = result;
21870
+ }
21871
+ async publish(payload, defaultResult) {
21872
+ this.callLogs.push({
21873
+ timestamp: new Date().toISOString(),
21874
+ payload
21875
+ });
21876
+ return this.nextResult ?? defaultResult;
21877
+ }
21878
+ getCallLogs() {
21879
+ return this.callLogs;
21880
+ }
21881
+ reset() {
21882
+ this.callLogs = [];
21883
+ this.nextResult = undefined;
21884
+ }
21885
+ }
21886
+
21887
+ class FakeFileCliTransport {
21888
+ kind = "file-cli";
21889
+ core = new FakeTransportCore;
21890
+ setNextResult(result) {
21891
+ this.core.setNextResult(result);
21892
+ }
21893
+ async publish(payload) {
21894
+ return this.core.publish(payload, {
21895
+ ok: true,
21896
+ target: "fake-file-cli",
21897
+ url: "https://example.com/published-post",
21898
+ id: "fake-id-123",
21899
+ message: "Published offline via FakeFileCliTransport"
21900
+ });
21901
+ }
21902
+ getCallLogs() {
21903
+ return this.core.getCallLogs();
21904
+ }
21905
+ reset() {
21906
+ this.core.reset();
21907
+ }
21908
+ }
21909
+
21847
21910
  class FakeBrowserTransport {
21848
21911
  kind = "browser";
21849
21912
  driver;
21850
- callLogs;
21851
- nextResult;
21913
+ core = new FakeTransportCore;
21852
21914
  constructor(driver = "agent") {
21853
21915
  this.driver = driver;
21854
- this.callLogs = [];
21855
21916
  }
21856
21917
  setNextResult(result) {
21857
- this.nextResult = result;
21918
+ this.core.setNextResult(result);
21858
21919
  }
21859
21920
  async publish(payload) {
21860
- this.callLogs.push({
21861
- timestamp: new Date().toISOString(),
21862
- payload
21863
- });
21864
- if (this.nextResult) {
21865
- return this.nextResult;
21866
- }
21867
- return {
21921
+ return this.core.publish(payload, {
21868
21922
  ok: true,
21869
21923
  target: "fake-browser",
21870
21924
  url: "https://example.com/browser-draft",
21871
21925
  id: "fake-browser-id-123",
21872
21926
  message: "Published offline via FakeBrowserTransport",
21873
21927
  metadata: { driver: this.driver }
21874
- };
21928
+ });
21875
21929
  }
21876
21930
  getCallLogs() {
21877
- return this.callLogs;
21931
+ return this.core.getCallLogs();
21878
21932
  }
21879
21933
  reset() {
21880
- this.callLogs = [];
21881
- this.nextResult = undefined;
21934
+ this.core.reset();
21882
21935
  }
21883
21936
  }
21884
21937
  function resolveBrowserProfileDir(channel, override) {
@@ -21887,7 +21940,7 @@ function resolveBrowserProfileDir(channel, override) {
21887
21940
  if (trimmed.startsWith("~/")) {
21888
21941
  return join(homedir(), trimmed.slice(2));
21889
21942
  }
21890
- return trimmed;
21943
+ return resolve(trimmed);
21891
21944
  }
21892
21945
  return join(homedir(), ".config", "kk", "browser-profiles", channel);
21893
21946
  }
@@ -21776,78 +21776,78 @@ __export(exports_src, {
21776
21776
  FakeBrowserTransport: () => FakeBrowserTransport
21777
21777
  });
21778
21778
  import { homedir } from "os";
21779
- import { join } from "path";
21779
+ import { join, resolve } from "path";
21780
21780
 
21781
- class FakeFileCliTransport {
21782
- kind = "file-cli";
21783
- callLogs;
21781
+ class FakeTransportCore {
21782
+ callLogs = [];
21784
21783
  nextResult;
21785
- constructor() {
21786
- this.callLogs = [];
21787
- }
21788
21784
  setNextResult(result) {
21789
21785
  this.nextResult = result;
21790
21786
  }
21791
- async publish(payload) {
21787
+ async publish(payload, defaultResult) {
21792
21788
  this.callLogs.push({
21793
21789
  timestamp: new Date().toISOString(),
21794
21790
  payload
21795
21791
  });
21796
- if (this.nextResult) {
21797
- return this.nextResult;
21798
- }
21799
- return {
21792
+ return this.nextResult ?? defaultResult;
21793
+ }
21794
+ getCallLogs() {
21795
+ return this.callLogs;
21796
+ }
21797
+ reset() {
21798
+ this.callLogs = [];
21799
+ this.nextResult = undefined;
21800
+ }
21801
+ }
21802
+
21803
+ class FakeFileCliTransport {
21804
+ kind = "file-cli";
21805
+ core = new FakeTransportCore;
21806
+ setNextResult(result) {
21807
+ this.core.setNextResult(result);
21808
+ }
21809
+ async publish(payload) {
21810
+ return this.core.publish(payload, {
21800
21811
  ok: true,
21801
21812
  target: "fake-file-cli",
21802
21813
  url: "https://example.com/published-post",
21803
21814
  id: "fake-id-123",
21804
21815
  message: "Published offline via FakeFileCliTransport"
21805
- };
21816
+ });
21806
21817
  }
21807
21818
  getCallLogs() {
21808
- return this.callLogs;
21819
+ return this.core.getCallLogs();
21809
21820
  }
21810
21821
  reset() {
21811
- this.callLogs = [];
21812
- this.nextResult = undefined;
21822
+ this.core.reset();
21813
21823
  }
21814
21824
  }
21815
21825
 
21816
21826
  class FakeBrowserTransport {
21817
21827
  kind = "browser";
21818
21828
  driver;
21819
- callLogs;
21820
- nextResult;
21829
+ core = new FakeTransportCore;
21821
21830
  constructor(driver = "agent") {
21822
21831
  this.driver = driver;
21823
- this.callLogs = [];
21824
21832
  }
21825
21833
  setNextResult(result) {
21826
- this.nextResult = result;
21834
+ this.core.setNextResult(result);
21827
21835
  }
21828
21836
  async publish(payload) {
21829
- this.callLogs.push({
21830
- timestamp: new Date().toISOString(),
21831
- payload
21832
- });
21833
- if (this.nextResult) {
21834
- return this.nextResult;
21835
- }
21836
- return {
21837
+ return this.core.publish(payload, {
21837
21838
  ok: true,
21838
21839
  target: "fake-browser",
21839
21840
  url: "https://example.com/browser-draft",
21840
21841
  id: "fake-browser-id-123",
21841
21842
  message: "Published offline via FakeBrowserTransport",
21842
21843
  metadata: { driver: this.driver }
21843
- };
21844
+ });
21844
21845
  }
21845
21846
  getCallLogs() {
21846
- return this.callLogs;
21847
+ return this.core.getCallLogs();
21847
21848
  }
21848
21849
  reset() {
21849
- this.callLogs = [];
21850
- this.nextResult = undefined;
21850
+ this.core.reset();
21851
21851
  }
21852
21852
  }
21853
21853
  function resolveBrowserProfileDir(channel, override) {
@@ -21856,12 +21856,12 @@ function resolveBrowserProfileDir(channel, override) {
21856
21856
  if (trimmed.startsWith("~/")) {
21857
21857
  return join(homedir(), trimmed.slice(2));
21858
21858
  }
21859
- return trimmed;
21859
+ return resolve(trimmed);
21860
21860
  }
21861
21861
  return join(homedir(), ".config", "kk", "browser-profiles", channel);
21862
21862
  }
21863
21863
  async function trySelectors(page, selectors, options = {}) {
21864
- const timeout = options.timeout ?? 5000;
21864
+ const timeout = options.timeout ?? 2000;
21865
21865
  const visible = options.visible ?? true;
21866
21866
  for (const selector of selectors) {
21867
21867
  try {
@@ -21871,7 +21871,7 @@ async function trySelectors(page, selectors, options = {}) {
21871
21871
  continue;
21872
21872
  }
21873
21873
  if (visible) {
21874
- const isVisible = await locator.isVisible({ timeout: Math.min(timeout, 2000) }).catch(() => false);
21874
+ const isVisible = await locator.isVisible({ timeout }).catch(() => false);
21875
21875
  if (!isVisible) {
21876
21876
  continue;
21877
21877
  }
@@ -21902,7 +21902,7 @@ var init_src = () => {};
21902
21902
  // ../../plugins/publishings/surfdash-pub/src/index.ts
21903
21903
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
21904
21904
  import { copyFile, cp, writeFile } from "fs/promises";
21905
- import { dirname as dirname2, isAbsolute, join as join2, resolve, sep } from "path";
21905
+ import { dirname as dirname2, extname, isAbsolute, join as join2, resolve as resolve2, sep } from "path";
21906
21906
  import { parseArgs } from "util";
21907
21907
 
21908
21908
  // ../../packages/kk-core/src/kinds.ts
@@ -21939,6 +21939,22 @@ var ResultSchema = exports_external.object({
21939
21939
  error: exports_external.string().optional(),
21940
21940
  metadata: exports_external.record(exports_external.string(), exports_external.unknown()).optional()
21941
21941
  });
21942
+ var TargetOutcomeSchema = exports_external.object({
21943
+ target: exports_external.string(),
21944
+ ok: exports_external.boolean(),
21945
+ status: exports_external.enum(["published", "failed", "skipped"]),
21946
+ url: exports_external.string().optional(),
21947
+ id: exports_external.string().optional(),
21948
+ error: exports_external.string().optional(),
21949
+ durationMs: exports_external.number().optional(),
21950
+ metadata: exports_external.record(exports_external.string(), exports_external.unknown()).optional()
21951
+ });
21952
+ var AggregateResultSchema = exports_external.object({
21953
+ ok: exports_external.boolean(),
21954
+ summary: exports_external.object({ total: exports_external.number(), succeeded: exports_external.number(), failed: exports_external.number(), skipped: exports_external.number() }),
21955
+ targets: exports_external.array(TargetOutcomeSchema),
21956
+ metadata: exports_external.record(exports_external.string(), exports_external.unknown()).optional()
21957
+ });
21942
21958
  var DocListSchema = exports_external.array(DocSchema);
21943
21959
  var omniVoiceGenContentSchema = ContentSchema.extend({
21944
21960
  format: exports_external.literal("audio"),
@@ -21967,14 +21983,15 @@ var PluginKindSchema = exports_external.enum(PLUGIN_KINDS);
21967
21983
  // ../../packages/kk-core/src/manifest.ts
21968
21984
  init_zod();
21969
21985
  var OperationIOSchema = exports_external.enum(["doc-list", "markdown"]);
21970
- var PluginManifestSchema = exports_external.object({
21986
+ var PluginOperationSchema = exports_external.object({ input: OperationIOSchema, output: OperationIOSchema });
21987
+ var PluginManifestSchema = exports_external.looseObject({
21971
21988
  name: exports_external.string().min(1),
21972
21989
  kind: PluginKindSchema,
21973
21990
  entry: exports_external.string().min(1),
21974
21991
  version: exports_external.string().optional(),
21975
21992
  description: exports_external.string().optional(),
21976
- operations: exports_external.record(exports_external.string(), exports_external.object({ input: OperationIOSchema, output: OperationIOSchema })).optional()
21977
- }).passthrough();
21993
+ operations: exports_external.record(exports_external.string(), PluginOperationSchema).optional()
21994
+ });
21978
21995
  // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.4.39/node_modules/@gobing-ai/ts-runtime/dist/index.js
21979
21996
  init_file_system_node();
21980
21997
  init_config();
@@ -22003,6 +22020,7 @@ if (false) {}
22003
22020
  var TARGET = "surfdash";
22004
22021
  var OPERATIONS = ["publish", "scaffold-locale", "sync"];
22005
22022
  var SCAFFOLD_LOCALES = ["en", "ja"];
22023
+ var COVER_EXTS = new Set([".png", ".jpg", ".jpeg", ".webp", ".gif"]);
22006
22024
  function slugify2(str) {
22007
22025
  return str.toLowerCase().replace(/[^\w\s-]/g, "").trim().replace(/[\s_]+/g, "-");
22008
22026
  }
@@ -22046,6 +22064,13 @@ ${content.body.trim()}
22046
22064
  function fail(error51) {
22047
22065
  return ResultSchema.parse({ ok: false, target: TARGET, error: error51 });
22048
22066
  }
22067
+ async function writePublishResult(outputPath, result) {
22068
+ const outDir = dirname2(outputPath);
22069
+ if (outDir && outDir !== ".") {
22070
+ await createNodeFileSystem().ensureDir(outDir);
22071
+ }
22072
+ await createNodeFileSystem().writeFile(outputPath, JSON.stringify(result, null, 2));
22073
+ }
22049
22074
  function resolveOperation(options) {
22050
22075
  const raw = options.operation ?? "publish";
22051
22076
  if (typeof raw !== "string" || !OPERATIONS.includes(raw)) {
@@ -22065,8 +22090,8 @@ function resolvePostPath(options) {
22065
22090
  return { postPath };
22066
22091
  }
22067
22092
  function postPathOutsideRoot(postPath, surfdashRoot) {
22068
- const root = resolve(surfdashRoot);
22069
- if (!resolve(postPath).startsWith(root + sep)) {
22093
+ const root = resolve2(surfdashRoot);
22094
+ if (!resolve2(postPath).startsWith(root + sep)) {
22070
22095
  return `options.postPath must resolve inside the surfdash checkout (${root}): ${postPath}`;
22071
22096
  }
22072
22097
  return;
@@ -22090,7 +22115,7 @@ function deriveLocaleDraftPath(postPath, locale) {
22090
22115
  }
22091
22116
  function resolveSurfdashRoot() {
22092
22117
  const postsurfingBin = process.env.POSTSURFING_BIN ?? "postsurfing";
22093
- return process.env.SURFDASH_ROOT ?? (postsurfingBin.includes("/") ? resolve(dirname2(dirname2(postsurfingBin))) : undefined);
22118
+ return process.env.SURFDASH_ROOT ?? (postsurfingBin.includes("/") ? resolve2(dirname2(dirname2(postsurfingBin))) : undefined);
22094
22119
  }
22095
22120
  async function spawnCli(cmd, cwd) {
22096
22121
  const proc = Bun.spawn(cmd, {
@@ -22206,16 +22231,13 @@ async function scaffoldLocale(options) {
22206
22231
  function normalizeLocaleFrontmatter(markdown) {
22207
22232
  const lines = markdown.split(`
22208
22233
  `);
22209
- const inFrontmatter = lines[0]?.trim() === "---";
22210
- for (let i = 1;inFrontmatter && i < lines.length; i++) {
22211
- if (lines[i]?.trim() === "---")
22212
- break;
22213
- if (/^canonical\s*:/.test(lines[i] ?? ""))
22214
- lines[i] = "";
22215
- else if (/^draft\s*:/.test(lines[i] ?? ""))
22216
- lines[i] = "draft: false";
22217
- }
22218
- return lines.filter((line) => line !== "").join(`
22234
+ if (lines[0]?.trim() !== "---")
22235
+ return markdown;
22236
+ const close = lines.findIndex((l, i) => i > 0 && l.trim() === "---");
22237
+ if (close < 0)
22238
+ return markdown;
22239
+ const head = lines.slice(1, close).filter((l) => !/^canonical\s*:/.test(l)).map((l) => /^draft\s*:/.test(l) ? "draft: false" : l);
22240
+ return [lines[0], ...head, ...lines.slice(close)].join(`
22219
22241
  `);
22220
22242
  }
22221
22243
  async function syncPost(options) {
@@ -22266,14 +22288,25 @@ async function publishPost(payload) {
22266
22288
  const dateStamp = runDate || today;
22267
22289
  const titleSlug = slugify2(payload.content.title ?? "");
22268
22290
  const slug = titleSlug ? `${dateStamp}-${titleSlug}` : `${dateStamp}-post`;
22269
- const postDir = join2(surfdashRoot, "content", "posts", "articles", locale, slug);
22291
+ if (runDate !== "" && !/^\d{8}$/.test(runDate)) {
22292
+ return fail(`metadata.runDate must be YYYYMMDD, got '${runDate}'`);
22293
+ }
22294
+ const articlesRoot = resolve2(surfdashRoot, "content", "posts", "articles");
22295
+ const localeRoot = resolve2(articlesRoot, locale);
22296
+ const postDir = resolve2(localeRoot, slug);
22297
+ if (dirname2(localeRoot) !== articlesRoot || dirname2(postDir) !== localeRoot) {
22298
+ return fail(`post path escapes ${articlesRoot}`);
22299
+ }
22300
+ const coverPath = payload.content.metadata?.coverImage;
22301
+ if (typeof coverPath === "string" && existsSync2(coverPath) && !COVER_EXTS.has(extname(coverPath).toLowerCase())) {
22302
+ return fail(`metadata.coverImage must be one of ${[...COVER_EXTS].join(", ")}`);
22303
+ }
22270
22304
  await createNodeFileSystem().ensureDir(postDir);
22271
22305
  let imageFrontmatter;
22272
- const coverPath = payload.content.metadata?.coverImage;
22273
22306
  if (typeof coverPath === "string" && existsSync2(coverPath)) {
22274
22307
  const assetsDir = join2(postDir, "assets");
22275
22308
  await createNodeFileSystem().ensureDir(assetsDir);
22276
- const coverName = `cover${coverPath.slice(coverPath.lastIndexOf("."))}`;
22309
+ const coverName = `cover${extname(coverPath).toLowerCase()}`;
22277
22310
  await copyFile(coverPath, join2(assetsDir, coverName));
22278
22311
  imageFrontmatter = `./assets/${coverName}`;
22279
22312
  }
@@ -22342,16 +22375,20 @@ function getPublishTransport() {
22342
22375
  async function processPublishIO(inputPath, outputPath, transport = getPublishTransport()) {
22343
22376
  await createNodeFileSystem().deleteFile(outputPath);
22344
22377
  const rawInput = await createNodeFileSystem().readFile(inputPath);
22345
- const raw = JSON.parse(rawInput);
22346
- const content = ContentSchema.parse(raw);
22378
+ let raw;
22379
+ let content;
22380
+ try {
22381
+ raw = JSON.parse(rawInput);
22382
+ content = ContentSchema.parse(raw);
22383
+ } catch (error51) {
22384
+ const message = `Invalid Content input: ${error51 instanceof Error ? error51.message : String(error51)}`;
22385
+ await writePublishResult(outputPath, fail(message));
22386
+ throw new Error(message);
22387
+ }
22347
22388
  const options = raw.options !== undefined && typeof raw.options === "object" && raw.options !== null ? raw.options : undefined;
22348
22389
  const result = await transport.publish({ content, options });
22349
22390
  const validatedResult = ResultSchema.parse(result);
22350
- const outDir = dirname2(outputPath);
22351
- if (outDir && outDir !== ".") {
22352
- await createNodeFileSystem().ensureDir(outDir);
22353
- }
22354
- await createNodeFileSystem().writeFile(outputPath, JSON.stringify(validatedResult, null, 2));
22391
+ await writePublishResult(outputPath, validatedResult);
22355
22392
  if (!validatedResult.ok) {
22356
22393
  throw new Error(validatedResult.error ?? "Publish failed");
22357
22394
  }
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
2
  import { copyFile, cp, writeFile } from 'node:fs/promises';
3
- import { dirname, isAbsolute, join, resolve, sep } from 'node:path';
3
+ import { dirname, extname, isAbsolute, join, resolve, sep } from 'node:path';
4
4
  import { parseArgs } from 'node:util';
5
5
  import { type Content, ContentSchema, type Result, ResultSchema } from '@gobing-ai/kk-core';
6
6
  import type { PublishTransport, PublishTransportPayload } from '@gobing-ai/publish-harness';
@@ -18,6 +18,9 @@ type Operation = (typeof OPERATIONS)[number];
18
18
  const SCAFFOLD_LOCALES = ['en', 'ja'] as const;
19
19
  type ScaffoldLocale = (typeof SCAFFOLD_LOCALES)[number];
20
20
 
21
+ /** Cover-image extensions accepted from `metadata.coverImage` (R4): anything else is rejected, not copied. */
22
+ const COVER_EXTS = new Set(['.png', '.jpg', '.jpeg', '.webp', '.gif']);
23
+
21
24
  /** Target-specific options carried on the invoke payload's `options` channel. */
22
25
  interface SurfdashPubOptions {
23
26
  operation?: unknown;
@@ -113,6 +116,19 @@ function fail(error: string): Result {
113
116
  return ResultSchema.parse({ ok: false, target: TARGET, error });
114
117
  }
115
118
 
119
+ /**
120
+ * Writes a Result JSON to `outputPath`, creating the directory as needed. Shared by the success
121
+ * path and the invalid-input path so a malformed payload still produces the spec §2.5 `ok:false`
122
+ * Result artifact (R7).
123
+ */
124
+ async function writePublishResult(outputPath: string, result: Result): Promise<void> {
125
+ const outDir = dirname(outputPath);
126
+ if (outDir && outDir !== '.') {
127
+ await createNodeFileSystem().ensureDir(outDir);
128
+ }
129
+ await createNodeFileSystem().writeFile(outputPath, JSON.stringify(result, null, 2));
130
+ }
131
+
116
132
  /** Parses and routes `options.operation`; unknown or misplaced values fail loud. */
117
133
  function resolveOperation(options: SurfdashPubOptions): { operation: Operation } | { error: string } {
118
134
  const raw = options.operation ?? 'publish';
@@ -359,14 +375,19 @@ async function scaffoldLocale(options: SurfdashPubOptions): Promise<Result> {
359
375
  * for the agent prompt; safe because sync always runs after the agent's edit.
360
376
  */
361
377
  export function normalizeLocaleFrontmatter(markdown: string): string {
378
+ // Frontmatter-only rewrite (R3). The previous implementation filtered empty lines across the
379
+ // WHOLE document, deleting every blank line in the body — that is the sync corruption. Only
380
+ // the head between the opening and closing `---` is rewritten here; `split`/`join` on '\n'
381
+ // round-trips the rest of the document byte-for-byte.
362
382
  const lines = markdown.split('\n');
363
- const inFrontmatter = lines[0]?.trim() === '---';
364
- for (let i = 1; inFrontmatter && i < lines.length; i++) {
365
- if (lines[i]?.trim() === '---') break;
366
- if (/^canonical\s*:/.test(lines[i] ?? '')) lines[i] = '';
367
- else if (/^draft\s*:/.test(lines[i] ?? '')) lines[i] = 'draft: false';
368
- }
369
- return lines.filter((line) => line !== '').join('\n');
383
+ if (lines[0]?.trim() !== '---') return markdown;
384
+ const close = lines.findIndex((l, i) => i > 0 && l.trim() === '---');
385
+ if (close < 0) return markdown;
386
+ const head = lines
387
+ .slice(1, close)
388
+ .filter((l) => !/^canonical\s*:/.test(l))
389
+ .map((l) => (/^draft\s*:/.test(l) ? 'draft: false' : l));
390
+ return [lines[0], ...head, ...lines.slice(close)].join('\n');
370
391
  }
371
392
 
372
393
  async function syncPost(options: SurfdashPubOptions): Promise<Result> {
@@ -435,17 +456,32 @@ async function publishPost(payload: PublishTransportPayload): Promise<Result> {
435
456
  const dateStamp = runDate || today;
436
457
  const titleSlug = slugify(payload.content.title ?? '');
437
458
  const slug = titleSlug ? `${dateStamp}-${titleSlug}` : `${dateStamp}-post`;
438
- const postDir = join(surfdashRoot, 'content', 'posts', 'articles', locale, slug);
459
+ // Validate before touching disk (R4): a `metadata.runDate` or slug that escapes the articles
460
+ // tree would write a post outside it, and a non-image `coverImage` would be copied into a post
461
+ // that pushPostGit then publishes to a public repo.
462
+ if (runDate !== '' && !/^\d{8}$/.test(runDate)) {
463
+ return fail(`metadata.runDate must be YYYYMMDD, got '${runDate}'`);
464
+ }
465
+ const articlesRoot = resolve(surfdashRoot, 'content', 'posts', 'articles');
466
+ const localeRoot = resolve(articlesRoot, locale);
467
+ const postDir = resolve(localeRoot, slug);
468
+ if (dirname(localeRoot) !== articlesRoot || dirname(postDir) !== localeRoot) {
469
+ return fail(`post path escapes ${articlesRoot}`);
470
+ }
471
+ const coverPath = payload.content.metadata?.coverImage;
472
+ if (typeof coverPath === 'string' && existsSync(coverPath) && !COVER_EXTS.has(extname(coverPath).toLowerCase())) {
473
+ return fail(`metadata.coverImage must be one of ${[...COVER_EXTS].join(', ')}`);
474
+ }
475
+
439
476
  await createNodeFileSystem().ensureDir(postDir);
440
477
 
441
478
  // Cover image: metadata.coverImage (absolute local path) is copied into the post's
442
479
  // assets dir and referenced relatively — the surfdash PostSchema accepts ./assets/<file>.
443
480
  let imageFrontmatter: string | undefined;
444
- const coverPath = payload.content.metadata?.coverImage;
445
481
  if (typeof coverPath === 'string' && existsSync(coverPath)) {
446
482
  const assetsDir = join(postDir, 'assets');
447
483
  await createNodeFileSystem().ensureDir(assetsDir);
448
- const coverName = `cover${coverPath.slice(coverPath.lastIndexOf('.'))}`;
484
+ const coverName = `cover${extname(coverPath).toLowerCase()}`;
449
485
  await copyFile(coverPath, join(assetsDir, coverName));
450
486
  imageFrontmatter = `./assets/${coverName}`;
451
487
  }
@@ -552,8 +588,18 @@ export async function processPublishIO(
552
588
  ): Promise<void> {
553
589
  await createNodeFileSystem().deleteFile(outputPath);
554
590
  const rawInput = await createNodeFileSystem().readFile(inputPath);
555
- const raw = JSON.parse(rawInput) as Record<string, unknown>;
556
- const content = ContentSchema.parse(raw);
591
+ // A malformed payload must yield the spec §2.5 `ok:false` Result and a non-zero exit, not an
592
+ // uncaught SyntaxError (R7 — the other publishers were built from a template with this catch).
593
+ let raw: Record<string, unknown>;
594
+ let content: Content;
595
+ try {
596
+ raw = JSON.parse(rawInput) as Record<string, unknown>;
597
+ content = ContentSchema.parse(raw);
598
+ } catch (error: unknown) {
599
+ const message = `Invalid Content input: ${error instanceof Error ? error.message : String(error)}`;
600
+ await writePublishResult(outputPath, fail(message));
601
+ throw new Error(message);
602
+ }
557
603
  const options =
558
604
  raw.options !== undefined && typeof raw.options === 'object' && raw.options !== null
559
605
  ? (raw.options as Record<string, unknown>)
@@ -562,12 +608,7 @@ export async function processPublishIO(
562
608
  const result = await transport.publish({ content, options });
563
609
  const validatedResult = ResultSchema.parse(result);
564
610
 
565
- const outDir = dirname(outputPath);
566
- if (outDir && outDir !== '.') {
567
- await createNodeFileSystem().ensureDir(outDir);
568
- }
569
-
570
- await createNodeFileSystem().writeFile(outputPath, JSON.stringify(validatedResult, null, 2));
611
+ await writePublishResult(outputPath, validatedResult);
571
612
 
572
613
  if (!validatedResult.ok) {
573
614
  throw new Error(validatedResult.error ?? 'Publish failed');