@gobing-ai/knowledge-kit 0.1.8 → 0.1.10

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 (78) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/dist/board/collect/index.js +725 -1101
  3. package/dist/board/daily-voice/index.js +722 -101
  4. package/dist/board/itc/index.js +13 -0
  5. package/dist/index.js +1796 -102
  6. package/package.json +1 -1
  7. package/plugins/generations/content-gen/dist/index.js +4 -0
  8. package/plugins/generations/core-facts-gen/dist/index.js +4 -0
  9. package/plugins/generations/daily-article-gen/dist/index.js +4 -0
  10. package/plugins/generations/dailynews-gen/dist/index.js +4 -0
  11. package/plugins/generations/episode-plan-gen/dist/index.js +102 -1
  12. package/plugins/generations/episode-plan-gen/src/index.ts +2 -4
  13. package/plugins/generations/gemini-tts/dist/index.js +4 -0
  14. package/plugins/generations/image-gen/dist/index.js +4 -0
  15. package/plugins/generations/news-report-gen/dist/index.js +4 -0
  16. package/plugins/generations/voice-gen/dist/index.js +4 -0
  17. package/plugins/generations/voxcpm-tts/dist/index.js +4 -0
  18. package/plugins/ingestions/aihot-ingest/dist/index.js +4 -0
  19. package/plugins/ingestions/horizon-ingest/dist/index.js +28 -1
  20. package/plugins/ingestions/horizon-ingest/src/index.ts +34 -0
  21. package/plugins/ingestions/last30days-ingest/dist/index.js +11 -3
  22. package/plugins/ingestions/last30days-ingest/src/index.ts +14 -3
  23. package/plugins/ingestions/web-search/dist/index.js +4 -0
  24. package/plugins/kk/config.example.yaml +8 -0
  25. package/plugins/kk/plugin.json +1 -1
  26. package/plugins/kk/publish-presets/daily-ai-voice.json +123 -0
  27. package/plugins/kk/schedules/kk-daily-publish3-cn-peak.cron.tmpl +20 -0
  28. package/plugins/kk/schedules/kk-daily-publish3-overseas.cron.tmpl +17 -0
  29. package/plugins/kk/scripts/daily-stages.ts +665 -78
  30. package/plugins/kk/scripts/kk-workflow-stages.ts +11 -1
  31. package/plugins/kk/scripts/sentences.ts +134 -0
  32. package/plugins/kk/scripts/series-names.ts +124 -0
  33. package/plugins/kk/scripts/topic-boundary.json +18 -83
  34. package/plugins/kk/workflows/kk-collect.yaml +2 -0
  35. package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +112 -9
  36. package/plugins/kk/workflows/kk-daily-ai-voice3.yaml +842 -0
  37. package/plugins/kk/workflows/kk-daily-publish3.yaml +282 -0
  38. package/plugins/kk/workflows/kk-daily-safety.yaml +50 -5
  39. package/plugins/publishings/bilibili-pub/dist/index.js +4 -0
  40. package/plugins/publishings/cnblogs-pub/dist/index.js +4 -0
  41. package/plugins/publishings/csdn-pub/dist/index.js +4 -0
  42. package/plugins/publishings/csdn-pub/dist/profile-check.js +4 -0
  43. package/plugins/publishings/emdash-pub/dist/index.js +4 -0
  44. package/plugins/publishings/infoq-pub/dist/index.js +4 -0
  45. package/plugins/publishings/infoq-pub/dist/profile-check.js +4 -0
  46. package/plugins/publishings/jike-pub/dist/index.js +4 -0
  47. package/plugins/publishings/jike-pub/dist/profile-check.js +4 -0
  48. package/plugins/publishings/juejin-pub/dist/index.js +4 -0
  49. package/plugins/publishings/juejin-pub/dist/profile-check.js +4 -0
  50. package/plugins/publishings/medium-pub/dist/index.js +4 -0
  51. package/plugins/publishings/podcast-pub/README.md +65 -0
  52. package/plugins/publishings/podcast-pub/dist/index.js +1906 -374
  53. package/plugins/publishings/podcast-pub/plugin.json +1 -1
  54. package/plugins/publishings/podcast-pub/src/backfill.ts +465 -0
  55. package/plugins/publishings/podcast-pub/src/index.ts +211 -34
  56. package/plugins/publishings/podcast-pub/src/microfeed-client.ts +32 -3
  57. package/plugins/publishings/podcast-pub/src/seo.ts +1079 -0
  58. package/plugins/publishings/podcast-pub/src/show-notes.ts +74 -14
  59. package/plugins/publishings/qiita-pub/dist/index.js +4 -0
  60. package/plugins/publishings/shipinhao-pub/dist/index.js +4 -0
  61. package/plugins/publishings/sspai-pub/dist/index.js +4 -0
  62. package/plugins/publishings/substack-pub/dist/index.js +4 -0
  63. package/plugins/publishings/substack-pub/dist/profile-check.js +4 -0
  64. package/plugins/publishings/surfdash-pub/dist/index.js +313 -32
  65. package/plugins/publishings/surfdash-pub/src/digest-check.ts +218 -0
  66. package/plugins/publishings/surfdash-pub/src/index.ts +170 -39
  67. package/plugins/publishings/surfdash-pub/src/patch.ts +44 -7
  68. package/plugins/publishings/threads-pub/dist/index.js +4 -0
  69. package/plugins/publishings/wechatmp-pub/dist/index.js +4 -0
  70. package/plugins/publishings/wechatmp-pub/dist/profile-check.js +4 -0
  71. package/plugins/publishings/weibo-pub/dist/index.js +4 -0
  72. package/plugins/publishings/weibo-pub/dist/profile-check.js +4 -0
  73. package/plugins/publishings/x-pub/dist/index.js +4 -0
  74. package/plugins/publishings/x-pub/dist/profile-check.js +4 -0
  75. package/plugins/publishings/xhs-pub/dist/index.js +4 -0
  76. package/plugins/publishings/xhs-pub/dist/profile-check.js +4 -0
  77. package/plugins/publishings/xiaoyuzhou-pub/dist/index.js +4 -0
  78. package/plugins/publishings/zenn-pub/dist/index.js +4 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobing-ai/knowledge-kit",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "An ingest → create → publish content pipeline CLI (Bun).",
5
5
  "repository": {
6
6
  "type": "git",
@@ -12253,6 +12253,10 @@ init_config();
12253
12253
 
12254
12254
  // ../../packages/kk-core/src/render-md.ts
12255
12255
  if (false) {}
12256
+ // ../../packages/kk-core/src/sentences.ts
12257
+ var CJK_TERMINATORS = new Set(["\u3002", "\uFF01", "\uFF1F"]);
12258
+ var LATIN_TERMINATORS = new Set([".", "!", "?"]);
12259
+ var ATTACHED_CLOSERS = new Set(["\u201D", "\u2019", "\u300D", "\u300F", "\uFF09", ")", '"', "'"]);
12256
12260
  // ../../plugins/generations/content-gen/src/index.ts
12257
12261
  init_dist();
12258
12262
 
@@ -12319,6 +12319,10 @@ function uniqueToken() {
12319
12319
  }
12320
12320
  // ../../packages/kk-core/src/render-md.ts
12321
12321
  if (false) {}
12322
+ // ../../packages/kk-core/src/sentences.ts
12323
+ var CJK_TERMINATORS = new Set(["\u3002", "\uFF01", "\uFF1F"]);
12324
+ var LATIN_TERMINATORS = new Set([".", "!", "?"]);
12325
+ var ATTACHED_CLOSERS = new Set(["\u201D", "\u2019", "\u300D", "\u300F", "\uFF09", ")", '"', "'"]);
12322
12326
  // ../../plugins/generations/core-facts-gen/src/index.ts
12323
12327
  init_dist();
12324
12328
  function sourceFileOf(entry) {
@@ -12319,6 +12319,10 @@ function uniqueToken() {
12319
12319
  }
12320
12320
  // ../../packages/kk-core/src/render-md.ts
12321
12321
  if (false) {}
12322
+ // ../../packages/kk-core/src/sentences.ts
12323
+ var CJK_TERMINATORS = new Set(["\u3002", "\uFF01", "\uFF1F"]);
12324
+ var LATIN_TERMINATORS = new Set([".", "!", "?"]);
12325
+ var ATTACHED_CLOSERS = new Set(["\u201D", "\u2019", "\u300D", "\u300F", "\uFF09", ")", '"', "'"]);
12322
12326
  // ../../plugins/generations/daily-article-gen/src/index.ts
12323
12327
  init_dist();
12324
12328
  function sourceLabelOf(doc) {
@@ -12254,6 +12254,10 @@ init_config();
12254
12254
 
12255
12255
  // ../../packages/kk-core/src/render-md.ts
12256
12256
  if (false) {}
12257
+ // ../../packages/kk-core/src/sentences.ts
12258
+ var CJK_TERMINATORS = new Set(["\u3002", "\uFF01", "\uFF1F"]);
12259
+ var LATIN_TERMINATORS = new Set([".", "!", "?"]);
12260
+ var ATTACHED_CLOSERS = new Set(["\u201D", "\u2019", "\u300D", "\u300F", "\uFF09", ")", '"', "'"]);
12257
12261
  // ../../plugins/generations/dailynews-gen/src/index.ts
12258
12262
  init_dist();
12259
12263
 
@@ -12319,6 +12319,107 @@ function uniqueToken() {
12319
12319
  }
12320
12320
  // ../../packages/kk-core/src/render-md.ts
12321
12321
  if (false) {}
12322
+ // ../../packages/kk-core/src/sentences.ts
12323
+ var CJK_TERMINATORS = new Set(["\u3002", "\uFF01", "\uFF1F"]);
12324
+ var LATIN_TERMINATORS = new Set([".", "!", "?"]);
12325
+ var ATTACHED_CLOSERS = new Set(["\u201D", "\u2019", "\u300D", "\u300F", "\uFF09", ")", '"', "'"]);
12326
+ var OPEN_PAIRS = {
12327
+ "\u300C": "\u300D",
12328
+ "\u300E": "\u300F",
12329
+ "\u201C": "\u201D",
12330
+ "\uFF08": "\uFF09",
12331
+ "(": ")"
12332
+ };
12333
+ var CLOSE_PAIRS = {
12334
+ "\u300D": "\u300C",
12335
+ "\u300F": "\u300E",
12336
+ "\u201D": "\u201C",
12337
+ "\uFF09": "\uFF08",
12338
+ ")": "("
12339
+ };
12340
+ function splitSentences(text, options = {}) {
12341
+ if (!text || typeof text !== "string")
12342
+ return [];
12343
+ const results = [];
12344
+ let sentenceStart = 0;
12345
+ let stack = [];
12346
+ let i = 0;
12347
+ const isTerminator = (c) => CJK_TERMINATORS.has(c) || options.cjkOnly !== true && LATIN_TERMINATORS.has(c);
12348
+ while (i < text.length) {
12349
+ const ch = text[i];
12350
+ if (isTerminator(ch)) {
12351
+ let termEnd = i;
12352
+ let hasCjkTerm = false;
12353
+ let hasLatinTerm = false;
12354
+ while (termEnd < text.length) {
12355
+ const termChar = text[termEnd];
12356
+ if (!termChar || !isTerminator(termChar)) {
12357
+ break;
12358
+ }
12359
+ if (CJK_TERMINATORS.has(termChar))
12360
+ hasCjkTerm = true;
12361
+ if (LATIN_TERMINATORS.has(termChar))
12362
+ hasLatinTerm = true;
12363
+ termEnd++;
12364
+ }
12365
+ let closerEnd = termEnd;
12366
+ const tempStack = [...stack];
12367
+ while (closerEnd < text.length) {
12368
+ const closer = text[closerEnd];
12369
+ if (!closer || !ATTACHED_CLOSERS.has(closer)) {
12370
+ break;
12371
+ }
12372
+ const top = tempStack[tempStack.length - 1];
12373
+ if (top !== undefined && top === closer) {
12374
+ tempStack.pop();
12375
+ } else if (CLOSE_PAIRS[closer]) {
12376
+ const idx = tempStack.lastIndexOf(closer);
12377
+ if (idx >= 0)
12378
+ tempStack.splice(idx, 1);
12379
+ }
12380
+ closerEnd++;
12381
+ }
12382
+ let isBoundary = false;
12383
+ if (tempStack.length === 0) {
12384
+ if (hasCjkTerm) {
12385
+ isBoundary = true;
12386
+ } else if (hasLatinTerm) {
12387
+ const nextChar = text[closerEnd];
12388
+ if (closerEnd === text.length || nextChar !== undefined && /\s/.test(nextChar)) {
12389
+ isBoundary = true;
12390
+ }
12391
+ }
12392
+ }
12393
+ if (isBoundary) {
12394
+ const sentence = text.slice(sentenceStart, closerEnd).trim();
12395
+ if (sentence.length > 0) {
12396
+ results.push(sentence);
12397
+ }
12398
+ sentenceStart = closerEnd;
12399
+ stack = tempStack;
12400
+ i = closerEnd;
12401
+ continue;
12402
+ }
12403
+ }
12404
+ const openMatch = OPEN_PAIRS[ch];
12405
+ const closeMatch = CLOSE_PAIRS[ch];
12406
+ if (openMatch) {
12407
+ stack.push(openMatch);
12408
+ } else if (closeMatch) {
12409
+ const idx = stack.lastIndexOf(ch);
12410
+ if (idx >= 0)
12411
+ stack.splice(idx, 1);
12412
+ }
12413
+ i++;
12414
+ }
12415
+ if (sentenceStart < text.length) {
12416
+ const tail = text.slice(sentenceStart).trim();
12417
+ if (tail.length > 0) {
12418
+ results.push(tail);
12419
+ }
12420
+ }
12421
+ return results;
12422
+ }
12322
12423
  // ../../plugins/generations/episode-plan-gen/src/index.ts
12323
12424
  init_dist();
12324
12425
  var SOURCE_BASE = {
@@ -12731,7 +12832,7 @@ function planEpisode(docs, options) {
12731
12832
  return selected.map((r) => {
12732
12833
  const meta = asRecord(r.doc.metadata);
12733
12834
  const ascii = asciiRatio(r.doc.title ?? "");
12734
- const brief = (r.doc.body ?? "").trim().split(/(?<=[\u3002\uFF01\uFF1F])\s*/).slice(0, 2).join("");
12835
+ const brief = splitSentences((r.doc.body ?? "").trim(), { cjkOnly: true }).slice(0, 2).join("");
12735
12836
  const published = docTimestamp(r.doc);
12736
12837
  return {
12737
12838
  ...r.doc,
@@ -1,6 +1,6 @@
1
1
  import { dirname } from 'node:path';
2
2
  import { parseArgs } from 'node:util';
3
- import { type Content, ContentSchema, type Doc, DocListSchema } from '@gobing-ai/kk-core';
3
+ import { type Content, ContentSchema, type Doc, DocListSchema, splitSentences } from '@gobing-ai/kk-core';
4
4
  import { atomicWriteJson, createNodeFileSystem, readJsonFile } from '@gobing-ai/ts-runtime';
5
5
  import { echoError } from '@gobing-ai/ts-utils';
6
6
 
@@ -597,9 +597,7 @@ export function planEpisode(docs: Doc[], options: PlanOptions): Doc[] {
597
597
  // Task 0118: 1–2-sentence briefing (first two 。!?-terminated sentences of the
598
598
  // cleaned body) + best-effort publish timestamp ride the plan metadata so the
599
599
  // podcast show-notes brief/timeline render stays deterministic, no LLM.
600
- const brief = (r.doc.body ?? '')
601
- .trim()
602
- .split(/(?<=[。!?])\s*/)
600
+ const brief = splitSentences((r.doc.body ?? '').trim(), { cjkOnly: true })
603
601
  .slice(0, 2)
604
602
  .join('');
605
603
  const published = docTimestamp(r.doc);
@@ -12257,6 +12257,10 @@ init_config();
12257
12257
 
12258
12258
  // ../../packages/kk-core/src/render-md.ts
12259
12259
  if (false) {}
12260
+ // ../../packages/kk-core/src/sentences.ts
12261
+ var CJK_TERMINATORS = new Set(["\u3002", "\uFF01", "\uFF1F"]);
12262
+ var LATIN_TERMINATORS = new Set([".", "!", "?"]);
12263
+ var ATTACHED_CLOSERS = new Set(["\u201D", "\u2019", "\u300D", "\u300F", "\uFF09", ")", '"', "'"]);
12260
12264
  // ../../plugins/generations/voice-gen/src/index.ts
12261
12265
  init_dist();
12262
12266
 
@@ -12595,6 +12595,10 @@ init_config();
12595
12595
 
12596
12596
  // ../../packages/kk-core/src/render-md.ts
12597
12597
  if (false) {}
12598
+ // ../../packages/kk-core/src/sentences.ts
12599
+ var CJK_TERMINATORS = new Set(["\u3002", "\uFF01", "\uFF1F"]);
12600
+ var LATIN_TERMINATORS = new Set([".", "!", "?"]);
12601
+ var ATTACHED_CLOSERS = new Set(["\u201D", "\u2019", "\u300D", "\u300F", "\uFF09", ")", '"', "'"]);
12598
12602
  // ../../plugins/generations/image-gen/src/index.ts
12599
12603
  init_dist();
12600
12604
 
@@ -12319,6 +12319,10 @@ function uniqueToken() {
12319
12319
  }
12320
12320
  // ../../packages/kk-core/src/render-md.ts
12321
12321
  if (false) {}
12322
+ // ../../packages/kk-core/src/sentences.ts
12323
+ var CJK_TERMINATORS = new Set(["\u3002", "\uFF01", "\uFF1F"]);
12324
+ var LATIN_TERMINATORS = new Set([".", "!", "?"]);
12325
+ var ATTACHED_CLOSERS = new Set(["\u201D", "\u2019", "\u300D", "\u300F", "\uFF09", ")", '"', "'"]);
12322
12326
  // ../../plugins/generations/news-report-gen/src/index.ts
12323
12327
  init_dist();
12324
12328
  var QC_DIMENSIONS = ["quality", "importance", "urgency", "impact"];
@@ -12254,6 +12254,10 @@ init_config();
12254
12254
 
12255
12255
  // ../../packages/kk-core/src/render-md.ts
12256
12256
  if (false) {}
12257
+ // ../../packages/kk-core/src/sentences.ts
12258
+ var CJK_TERMINATORS = new Set(["\u3002", "\uFF01", "\uFF1F"]);
12259
+ var LATIN_TERMINATORS = new Set([".", "!", "?"]);
12260
+ var ATTACHED_CLOSERS = new Set(["\u201D", "\u2019", "\u300D", "\u300F", "\uFF09", ")", '"', "'"]);
12257
12261
  // ../../plugins/generations/voice-gen/src/index.ts
12258
12262
  init_dist();
12259
12263
 
@@ -12257,6 +12257,10 @@ init_config();
12257
12257
 
12258
12258
  // ../../packages/kk-core/src/render-md.ts
12259
12259
  if (false) {}
12260
+ // ../../packages/kk-core/src/sentences.ts
12261
+ var CJK_TERMINATORS = new Set(["\u3002", "\uFF01", "\uFF1F"]);
12262
+ var LATIN_TERMINATORS = new Set([".", "!", "?"]);
12263
+ var ATTACHED_CLOSERS = new Set(["\u201D", "\u2019", "\u300D", "\u300F", "\uFF09", ")", '"', "'"]);
12260
12264
  // ../../plugins/generations/voice-gen/src/index.ts
12261
12265
  init_dist();
12262
12266
 
@@ -12571,6 +12571,10 @@ function uniqueToken() {
12571
12571
  }
12572
12572
  // ../../packages/kk-core/src/render-md.ts
12573
12573
  if (false) {}
12574
+ // ../../packages/kk-core/src/sentences.ts
12575
+ var CJK_TERMINATORS = new Set(["\u3002", "\uFF01", "\uFF1F"]);
12576
+ var LATIN_TERMINATORS = new Set([".", "!", "?"]);
12577
+ var ATTACHED_CLOSERS = new Set(["\u201D", "\u2019", "\u300D", "\u300F", "\uFF09", ")", '"', "'"]);
12574
12578
  // ../../plugins/ingestions/aihot-ingest/src/index.ts
12575
12579
  init_dist();
12576
12580
  init_zod();
@@ -12320,6 +12320,10 @@ function uniqueToken() {
12320
12320
  }
12321
12321
  // ../../packages/kk-core/src/render-md.ts
12322
12322
  if (false) {}
12323
+ // ../../packages/kk-core/src/sentences.ts
12324
+ var CJK_TERMINATORS = new Set(["\u3002", "\uFF01", "\uFF1F"]);
12325
+ var LATIN_TERMINATORS = new Set([".", "!", "?"]);
12326
+ var ATTACHED_CLOSERS = new Set(["\u201D", "\u2019", "\u300D", "\u300F", "\uFF09", ")", '"', "'"]);
12323
12327
  // ../../plugins/ingestions/horizon-ingest/src/index.ts
12324
12328
  init_dist();
12325
12329
  init_zod();
@@ -12328,8 +12332,10 @@ var InputConfigSchema = object({
12328
12332
  config: string2().trim().min(1).optional(),
12329
12333
  hours: number2().int().min(1).optional(),
12330
12334
  languages: array(string2().trim().min(1)).optional(),
12331
- horizon_dir: string2().trim().min(1).optional()
12335
+ horizon_dir: string2().trim().min(1).optional(),
12336
+ stale_fallback_ms: number2().int().min(0).optional()
12332
12337
  });
12338
+ var DEFAULT_STALE_FALLBACK_MS = 26 * 3600000;
12333
12339
  var defaultRunner = async (config, horizonDir) => {
12334
12340
  const argv = [process.env.HORIZON_UV ?? "uv", "run", "horizon"];
12335
12341
  if (config.hours !== undefined)
@@ -12447,6 +12453,27 @@ async function processIngestionIO(options, config, runner = defaultRunner, now =
12447
12453
  continue;
12448
12454
  docs.push(...digestToDocs(await fs.readFile(file), match[1], match[2]));
12449
12455
  }
12456
+ if (docs.length === 0) {
12457
+ const fallbackMs = config.stale_fallback_ms ?? DEFAULT_STALE_FALLBACK_MS;
12458
+ let newest = null;
12459
+ for (const entry of entries) {
12460
+ const match = entry.match(digestRe);
12461
+ if (!match?.[1] || !match?.[2] || !langs.has(match[2]))
12462
+ continue;
12463
+ const stat = await fs.stat(join(summariesDir, entry));
12464
+ if (!stat)
12465
+ continue;
12466
+ if (!newest || stat.mtimeMs > newest.mtimeMs)
12467
+ newest = { entry, mtimeMs: stat.mtimeMs };
12468
+ }
12469
+ if (newest && runStartedAt - newest.mtimeMs <= fallbackMs) {
12470
+ const match = newest.entry.match(digestRe);
12471
+ if (match?.[1] && match?.[2]) {
12472
+ echoError(`horizon-ingest: no fresh digest this run \u2014 falling back to ${newest.entry} (${Math.round((runStartedAt - newest.mtimeMs) / 3600000)}h old)`);
12473
+ docs.push(...digestToDocs(await fs.readFile(join(summariesDir, newest.entry)), match[1], match[2]));
12474
+ }
12475
+ }
12476
+ }
12450
12477
  if (docs.length === 0) {
12451
12478
  echoError(`horizon-ingest: no fresh digest in ${summariesDir} (languages: ${[...langs].join(", ")}) \u2014 emitting []`);
12452
12479
  }
@@ -19,8 +19,18 @@ const InputConfigSchema = z.object({
19
19
  hours: z.number().int().min(1).optional(),
20
20
  languages: z.array(z.string().trim().min(1)).optional(),
21
21
  horizon_dir: z.string().trim().min(1).optional(),
22
+ /**
23
+ * When this tick's run writes no digest (Horizon exits 0 on an empty 6h
24
+ * window), fall back to the newest digest at most this many ms old instead
25
+ * of emitting []. Doc ids are date-keyed and stable, so repeats dedupe
26
+ * downstream (2026-10-23: Oct 21 contributed zero docs — every tick hit an
27
+ * empty window).
28
+ */
29
+ stale_fallback_ms: z.number().int().min(0).optional(),
22
30
  });
23
31
 
32
+ const DEFAULT_STALE_FALLBACK_MS = 26 * 3_600_000;
33
+
24
34
  export interface HorizonRunResult {
25
35
  exitCode: number;
26
36
  stderr: string;
@@ -166,6 +176,30 @@ export async function processIngestionIO(
166
176
  if (!stat || stat.mtimeMs < runStartedAt - 1000) continue; // stale file from an earlier run
167
177
  docs.push(...digestToDocs(await fs.readFile(file), match[1], match[2]));
168
178
  }
179
+ if (docs.length === 0) {
180
+ // This tick wrote no digest (empty fetch window exits 0). Reuse the newest
181
+ // digest while it is young enough to still matter for a daily podcast.
182
+ const fallbackMs = config.stale_fallback_ms ?? DEFAULT_STALE_FALLBACK_MS;
183
+ let newest: { entry: string; mtimeMs: number } | null = null;
184
+ for (const entry of entries) {
185
+ const match = entry.match(digestRe);
186
+ if (!match?.[1] || !match?.[2] || !langs.has(match[2])) continue;
187
+ const stat = await fs.stat(join(summariesDir, entry));
188
+ if (!stat) continue;
189
+ if (!newest || stat.mtimeMs > newest.mtimeMs) newest = { entry, mtimeMs: stat.mtimeMs };
190
+ }
191
+ if (newest && runStartedAt - newest.mtimeMs <= fallbackMs) {
192
+ const match = newest.entry.match(digestRe);
193
+ if (match?.[1] && match?.[2]) {
194
+ echoError(
195
+ `horizon-ingest: no fresh digest this run — falling back to ${newest.entry} (${Math.round(
196
+ (runStartedAt - newest.mtimeMs) / 3_600_000,
197
+ )}h old)`,
198
+ );
199
+ docs.push(...digestToDocs(await fs.readFile(join(summariesDir, newest.entry)), match[1], match[2]));
200
+ }
201
+ }
202
+ }
169
203
  if (docs.length === 0) {
170
204
  echoError(
171
205
  `horizon-ingest: no fresh digest in ${summariesDir} (languages: ${[...langs].join(', ')}) — emitting []`,
@@ -11983,7 +11983,7 @@ var init_file_system_node = () => {};
11983
11983
  // ../../plugins/ingestions/last30days-ingest/src/index.ts
11984
11984
  import { createHash } from "crypto";
11985
11985
  import { homedir } from "os";
11986
- import { dirname as dirname2, join } from "path";
11986
+ import { dirname as dirname2, join, resolve } from "path";
11987
11987
  import { parseArgs } from "util";
11988
11988
 
11989
11989
  // ../../packages/kk-core/src/content-limits.ts
@@ -12321,6 +12321,10 @@ function uniqueToken() {
12321
12321
  }
12322
12322
  // ../../packages/kk-core/src/render-md.ts
12323
12323
  if (false) {}
12324
+ // ../../packages/kk-core/src/sentences.ts
12325
+ var CJK_TERMINATORS = new Set(["\u3002", "\uFF01", "\uFF1F"]);
12326
+ var LATIN_TERMINATORS = new Set([".", "!", "?"]);
12327
+ var ATTACHED_CLOSERS = new Set(["\u201D", "\u2019", "\u300D", "\u300F", "\uFF09", ")", '"', "'"]);
12324
12328
  // ../../plugins/ingestions/last30days-ingest/src/index.ts
12325
12329
  init_dist();
12326
12330
  init_zod();
@@ -12332,6 +12336,9 @@ var InputConfigSchema = object({
12332
12336
  });
12333
12337
  var defaultRunner = async (config, outputPath) => {
12334
12338
  const skillDir = process.env.LAST30DAYS_SKILL_DIR ?? join(homedir(), ".agents", "skills", "last30days");
12339
+ if (!createNodeFileSystem().exists(skillDir)) {
12340
+ throw new Error(`skill dir missing at ${skillDir} \u2014 restore the last30days skill checkout/symlink or set LAST30DAYS_SKILL_DIR`);
12341
+ }
12335
12342
  const argv = [
12336
12343
  process.env.LAST30DAYS_PYTHON ?? "python3",
12337
12344
  join(skillDir, "scripts", "last30days.py"),
@@ -12391,7 +12398,8 @@ function mapResultsToDocs(agentJson, topic) {
12391
12398
  }
12392
12399
  async function processIngestionIO(options, config, runner = defaultRunner) {
12393
12400
  const fs = createNodeFileSystem();
12394
- const outDir = dirname2(options.out);
12401
+ const out = resolve(options.out);
12402
+ const outDir = dirname2(out);
12395
12403
  if (outDir && outDir !== ".")
12396
12404
  await fs.ensureDir(outDir);
12397
12405
  const capturePath = join(outDir || ".", ".last30days-agent.json");
@@ -12400,7 +12408,7 @@ async function processIngestionIO(options, config, runner = defaultRunner) {
12400
12408
  throw new Error(`last30days exited ${run.exitCode}: ${run.stderr.trim() || "no stderr"}`);
12401
12409
  }
12402
12410
  const docs = mapResultsToDocs(await readJsonFile(capturePath), config.topic);
12403
- await atomicWriteJson(options.out, docs, fs);
12411
+ await atomicWriteJson(out, docs, fs);
12404
12412
  return { docs };
12405
12413
  }
12406
12414
  async function main2(runner) {
@@ -1,6 +1,6 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { homedir } from 'node:os';
3
- import { dirname, join } from 'node:path';
3
+ import { dirname, join, resolve } from 'node:path';
4
4
  import { parseArgs } from 'node:util';
5
5
  import { type Doc, DocListSchema } from '@gobing-ai/kk-core';
6
6
  import { atomicWriteJson, createNodeFileSystem, readJsonFile } from '@gobing-ai/ts-runtime';
@@ -36,6 +36,14 @@ export type RunLast30Days = (
36
36
 
37
37
  export const defaultRunner: RunLast30Days = async (config, outputPath) => {
38
38
  const skillDir = process.env.LAST30DAYS_SKILL_DIR ?? join(homedir(), '.agents', 'skills', 'last30days');
39
+ // The skill dir is the spawn cwd: when the ~/.agents/skills/last30days symlink dangles,
40
+ // Bun reports it as `ENOENT ... posix_spawn 'python3'` — pointing at the interpreter, not
41
+ // the dir (2026-10-20 incident lost 9h of diagnosis to that). Fail with the real problem.
42
+ if (!createNodeFileSystem().exists(skillDir)) {
43
+ throw new Error(
44
+ `skill dir missing at ${skillDir} — restore the last30days skill checkout/symlink or set LAST30DAYS_SKILL_DIR`,
45
+ );
46
+ }
39
47
  const argv = [
40
48
  process.env.LAST30DAYS_PYTHON ?? 'python3',
41
49
  join(skillDir, 'scripts', 'last30days.py'),
@@ -109,7 +117,10 @@ export async function processIngestionIO(
109
117
  runner: RunLast30Days = defaultRunner,
110
118
  ): Promise<{ docs: Doc[] }> {
111
119
  const fs = createNodeFileSystem();
112
- const outDir = dirname(options.out);
120
+ // Relative --out would split cwd semantics: the engine writes its capture relative to the
121
+ // skill dir (its cwd) while we read it relative to ours. Pin everything to an absolute path.
122
+ const out = resolve(options.out);
123
+ const outDir = dirname(out);
113
124
  if (outDir && outDir !== '.') await fs.ensureDir(outDir);
114
125
  const capturePath = join(outDir || '.', '.last30days-agent.json');
115
126
  const run = await runner(config, capturePath);
@@ -117,7 +128,7 @@ export async function processIngestionIO(
117
128
  throw new Error(`last30days exited ${run.exitCode}: ${run.stderr.trim() || 'no stderr'}`);
118
129
  }
119
130
  const docs = mapResultsToDocs(await readJsonFile(capturePath), config.topic);
120
- await atomicWriteJson(options.out, docs, fs);
131
+ await atomicWriteJson(out, docs, fs);
121
132
  return { docs };
122
133
  }
123
134
 
@@ -12275,6 +12275,10 @@ init_config();
12275
12275
 
12276
12276
  // ../../packages/kk-core/src/render-md.ts
12277
12277
  if (false) {}
12278
+ // ../../packages/kk-core/src/sentences.ts
12279
+ var CJK_TERMINATORS = new Set(["\u3002", "\uFF01", "\uFF1F"]);
12280
+ var LATIN_TERMINATORS = new Set([".", "!", "?"]);
12281
+ var ATTACHED_CLOSERS = new Set(["\u201D", "\u2019", "\u300D", "\u300F", "\uFF09", ")", '"', "'"]);
12278
12282
  // ../../node_modules/.bun/@logtape+logtape@2.3.1/node_modules/@logtape/logtape/dist/context.js
12279
12283
  var categoryPrefixSymbol = Symbol.for("logtape.categoryPrefix");
12280
12284
  function getCategoryPrefix() {
@@ -9,6 +9,14 @@ defaults:
9
9
  maxResults: 8
10
10
  fixture: false
11
11
 
12
+ # Per-workflow settings. `publish_preset` names the publish preset that owns the daily channel
13
+ # matrix (files live in `plugins/kk/publish-presets/`, overridable per project under
14
+ # `.kk/publish-presets/`). The preset decides WHICH channels publish; `live` is never in a preset
15
+ # file — it is a launch var (`live_channels`) or an installed schedule entry (ADR-022).
16
+ workflows:
17
+ kk-daily-ai-voice3:
18
+ publish_preset: "daily-ai-voice"
19
+
12
20
  # Plugin credentials — paste values between the quotes.
13
21
  # Path: plugins/<collection>/<name>/env — each entry is injected into that plugin's
14
22
  # process environment at exec time. A real environment variable always wins
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "kk",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "knowledge-kit Claude Code plugin: skills, commands, subagents, hooks, and rules"
5
5
  }
@@ -0,0 +1,123 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "name": "daily-ai-voice",
4
+ "targets": [
5
+ {
6
+ "plugin": "surfdash-pub",
7
+ "operation": "post",
8
+ "variant": "article",
9
+ "tier": 0,
10
+ "window": "after-generate",
11
+ "mode": "draft",
12
+ "required": true
13
+ },
14
+ {
15
+ "plugin": "podcast-pub",
16
+ "operation": "episode",
17
+ "variant": "episode",
18
+ "tier": 0,
19
+ "window": "after-generate",
20
+ "mode": "draft",
21
+ "required": true
22
+ },
23
+ {
24
+ "plugin": "wechatmp-pub",
25
+ "operation": "播客",
26
+ "variant": "episode",
27
+ "tier": 1,
28
+ "window": "after-generate",
29
+ "mode": "draft"
30
+ },
31
+ {
32
+ "plugin": "xhs-pub",
33
+ "operation": "发播客",
34
+ "variant": "episode",
35
+ "tier": 1,
36
+ "window": "after-generate",
37
+ "mode": "draft"
38
+ },
39
+ {
40
+ "plugin": "wechatmp-pub",
41
+ "operation": "文章",
42
+ "variant": "article",
43
+ "tier": 2,
44
+ "window": "after-generate",
45
+ "mode": "draft",
46
+ "options": {
47
+ "collection": "每日AI资讯"
48
+ }
49
+ },
50
+ {
51
+ "plugin": "xhs-pub",
52
+ "operation": "写长文",
53
+ "variant": "article",
54
+ "tier": 2,
55
+ "window": "after-generate",
56
+ "mode": "dummy"
57
+ },
58
+ {
59
+ "plugin": "xhs-pub",
60
+ "operation": "上传图文",
61
+ "variant": "cards",
62
+ "tier": 3,
63
+ "window": "after-generate",
64
+ "mode": "off"
65
+ },
66
+ {
67
+ "plugin": "weibo-pub",
68
+ "operation": "article",
69
+ "variant": "article",
70
+ "tier": 2,
71
+ "window": "cn-peak",
72
+ "mode": "draft"
73
+ },
74
+ {
75
+ "plugin": "x-pub",
76
+ "operation": "article",
77
+ "variant": "article",
78
+ "tier": 2,
79
+ "window": "cn-peak",
80
+ "mode": "draft"
81
+ },
82
+ {
83
+ "plugin": "x-pub",
84
+ "operation": "post",
85
+ "variant": "social:x",
86
+ "tier": 3,
87
+ "window": "overseas-rss",
88
+ "mode": "draft"
89
+ },
90
+ {
91
+ "plugin": "weibo-pub",
92
+ "operation": "post",
93
+ "variant": "social:weibo",
94
+ "tier": 3,
95
+ "window": "overseas-rss",
96
+ "mode": "draft"
97
+ },
98
+ {
99
+ "plugin": "threads-pub",
100
+ "operation": "post",
101
+ "variant": "social:threads",
102
+ "tier": 3,
103
+ "window": "overseas-rss",
104
+ "mode": "draft"
105
+ },
106
+ {
107
+ "plugin": "shipinhao-pub",
108
+ "operation": "图文",
109
+ "variant": "cards",
110
+ "tier": 3,
111
+ "window": "overseas-rss",
112
+ "mode": "off"
113
+ },
114
+ {
115
+ "plugin": "threads-pub",
116
+ "operation": "post-images",
117
+ "variant": "cards",
118
+ "tier": 3,
119
+ "window": "overseas-rss",
120
+ "mode": "off"
121
+ }
122
+ ]
123
+ }
@@ -0,0 +1,20 @@
1
+ # kk-daily-publish3-cn-peak — domestic live window (task 0323 R5).
2
+ #
3
+ # Fires at 17:00 local because the audience window is 09:00 Asia/Shanghai and launchd/cron
4
+ # intervals follow the MACHINE's zone; the first node (`publish-matrix-wait-until 09:00
5
+ # Asia/Shanghai`) sleeps the remainder, which is 1 h from 17:00 PDT and 0 h after the DST flip —
6
+ # so the entry never needs editing twice a year.
7
+ #
8
+ # `live_channels` is baked in: this installed line IS the operator's ADR-022 consent for a live
9
+ # send. Nothing in the workflow or the preset can produce `live` on its own.
10
+ #
11
+ # Rendered by `kk schedule render kk-daily-publish3-cn-peak --platform cron --repo <checkout>`;
12
+ # kk never edits the operator's crontab. MERGE, never replace:
13
+ #
14
+ # crontab -l > /tmp/kk-cn-peak.cron 2>/dev/null || :
15
+ # kk schedule render kk-daily-publish3-cn-peak --platform cron --repo "$PWD" >> /tmp/kk-cn-peak.cron
16
+ # crontab /tmp/kk-cn-peak.cron
17
+ #
18
+ # `run_date` is computed at tick time in America/Los_Angeles: a run that starts at 17:00 PDT is
19
+ # still "today" for the day dir the 04:30 generation wrote.
20
+ 0 17 * * * cd {{repo}} && mkdir -p .works/kk-daily-publish3 && PATH="{{home}}/.bun/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" TZ=America/Los_Angeles spur workflow run plugins/kk/workflows/kk-daily-publish3.yaml --vars "{\"work_dir\":\"{{repo}}/.works/kk-daily-publish3/$(TZ=America/Los_Angeles date +%Y%m%d)\",\"run_date\":\"$(TZ=America/Los_Angeles date +%F)\",\"window\":\"cn-peak\",\"live_channels\":\"wechatmp-pub,xhs-pub,weibo-pub,x-pub\"}" >> {{repo}}/.works/kk-daily-publish3/cn-peak.log 2>&1