@gobing-ai/knowledge-kit 0.1.9 → 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 (60) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/dist/index.js +4 -0
  3. package/package.json +1 -1
  4. package/plugins/generations/content-gen/dist/index.js +4 -0
  5. package/plugins/generations/core-facts-gen/dist/index.js +4 -0
  6. package/plugins/generations/daily-article-gen/dist/index.js +4 -0
  7. package/plugins/generations/dailynews-gen/dist/index.js +4 -0
  8. package/plugins/generations/episode-plan-gen/dist/index.js +102 -1
  9. package/plugins/generations/episode-plan-gen/src/index.ts +2 -4
  10. package/plugins/generations/gemini-tts/dist/index.js +4 -0
  11. package/plugins/generations/image-gen/dist/index.js +4 -0
  12. package/plugins/generations/news-report-gen/dist/index.js +4 -0
  13. package/plugins/generations/voice-gen/dist/index.js +4 -0
  14. package/plugins/generations/voxcpm-tts/dist/index.js +4 -0
  15. package/plugins/ingestions/aihot-ingest/dist/index.js +4 -0
  16. package/plugins/ingestions/horizon-ingest/dist/index.js +4 -0
  17. package/plugins/ingestions/last30days-ingest/dist/index.js +11 -3
  18. package/plugins/ingestions/last30days-ingest/src/index.ts +14 -3
  19. package/plugins/ingestions/web-search/dist/index.js +4 -0
  20. package/plugins/kk/plugin.json +1 -1
  21. package/plugins/kk/scripts/daily-stages.ts +86 -1
  22. package/plugins/kk/scripts/sentences.ts +134 -0
  23. package/plugins/kk/scripts/series-names.ts +124 -0
  24. package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +37 -3
  25. package/plugins/kk/workflows/kk-daily-safety.yaml +50 -5
  26. package/plugins/publishings/bilibili-pub/dist/index.js +4 -0
  27. package/plugins/publishings/cnblogs-pub/dist/index.js +4 -0
  28. package/plugins/publishings/csdn-pub/dist/index.js +4 -0
  29. package/plugins/publishings/csdn-pub/dist/profile-check.js +4 -0
  30. package/plugins/publishings/emdash-pub/dist/index.js +4 -0
  31. package/plugins/publishings/infoq-pub/dist/index.js +4 -0
  32. package/plugins/publishings/infoq-pub/dist/profile-check.js +4 -0
  33. package/plugins/publishings/jike-pub/dist/index.js +4 -0
  34. package/plugins/publishings/jike-pub/dist/profile-check.js +4 -0
  35. package/plugins/publishings/juejin-pub/dist/index.js +4 -0
  36. package/plugins/publishings/juejin-pub/dist/profile-check.js +4 -0
  37. package/plugins/publishings/medium-pub/dist/index.js +4 -0
  38. package/plugins/publishings/podcast-pub/dist/index.js +105 -8
  39. package/plugins/publishings/podcast-pub/src/seo.ts +2 -4
  40. package/plugins/publishings/podcast-pub/src/show-notes.ts +3 -8
  41. package/plugins/publishings/qiita-pub/dist/index.js +4 -0
  42. package/plugins/publishings/shipinhao-pub/dist/index.js +4 -0
  43. package/plugins/publishings/sspai-pub/dist/index.js +4 -0
  44. package/plugins/publishings/substack-pub/dist/index.js +4 -0
  45. package/plugins/publishings/substack-pub/dist/profile-check.js +4 -0
  46. package/plugins/publishings/surfdash-pub/dist/index.js +247 -27
  47. package/plugins/publishings/surfdash-pub/src/digest-check.ts +218 -0
  48. package/plugins/publishings/surfdash-pub/src/index.ts +76 -29
  49. package/plugins/publishings/surfdash-pub/src/patch.ts +44 -7
  50. package/plugins/publishings/threads-pub/dist/index.js +4 -0
  51. package/plugins/publishings/wechatmp-pub/dist/index.js +4 -0
  52. package/plugins/publishings/wechatmp-pub/dist/profile-check.js +4 -0
  53. package/plugins/publishings/weibo-pub/dist/index.js +4 -0
  54. package/plugins/publishings/weibo-pub/dist/profile-check.js +4 -0
  55. package/plugins/publishings/x-pub/dist/index.js +4 -0
  56. package/plugins/publishings/x-pub/dist/profile-check.js +4 -0
  57. package/plugins/publishings/xhs-pub/dist/index.js +4 -0
  58. package/plugins/publishings/xhs-pub/dist/profile-check.js +4 -0
  59. package/plugins/publishings/xiaoyuzhou-pub/dist/index.js +4 -0
  60. package/plugins/publishings/zenn-pub/dist/index.js +4 -0
@@ -6,7 +6,7 @@
6
6
  },
7
7
  "plugins": [
8
8
  {
9
- "version": "0.1.9",
9
+ "version": "0.1.10",
10
10
  "name": "kk",
11
11
  "source": "./plugins/kk",
12
12
  "description": "knowledge-kit — a set of toolkit for knowledge management"
package/dist/index.js CHANGED
@@ -42385,6 +42385,10 @@ function getByName(result, name) {
42385
42385
  }
42386
42386
  // ../../packages/kk-core/src/render-md.ts
42387
42387
  if (false) {}
42388
+ // ../../packages/kk-core/src/sentences.ts
42389
+ var CJK_TERMINATORS = new Set(["\u3002", "\uFF01", "\uFF1F"]);
42390
+ var LATIN_TERMINATORS = new Set([".", "!", "?"]);
42391
+ var ATTACHED_CLOSERS = new Set(["\u201D", "\u2019", "\u300D", "\u300F", "\uFF09", ")", '"', "'"]);
42388
42392
  // src/invoke.ts
42389
42393
  init_dist();
42390
42394
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobing-ai/knowledge-kit",
3
- "version": "0.1.9",
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();
@@ -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() {
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "kk",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "description": "knowledge-kit Claude Code plugin: skills, commands, subagents, hooks, and rules"
5
5
  }
@@ -114,6 +114,8 @@ import {
114
114
  type XhsCard,
115
115
  xhsPlan,
116
116
  } from './kk-workflow-stages.ts';
117
+ import { splitSentences } from './sentences.ts';
118
+ import { normalizeSeriesNames } from './series-names.ts';
117
119
 
118
120
  export function dailyQcContent(args: string[]): void {
119
121
  const [
@@ -1902,6 +1904,37 @@ function readFrontmatterScalar(filePath: string, key: string): string | undefine
1902
1904
  return value.length > 0 ? value : undefined;
1903
1905
  }
1904
1906
 
1907
+ /** The `key_points` item texts of a Surfing post's frontmatter; undefined when absent/unreadable. */
1908
+ export function readKeyPointItems(filePath: string): string[] | undefined {
1909
+ if (!isFile(filePath)) return undefined;
1910
+ let raw: string;
1911
+ try {
1912
+ raw = readFileSync(filePath, 'utf-8');
1913
+ } catch {
1914
+ return undefined;
1915
+ }
1916
+ const block = /^---\r?\n([\s\S]*?)\r?\n---/.exec(raw)?.[1];
1917
+ if (!block) return undefined;
1918
+ // Slice from `key_points` to the first top-level key (or block end), then take the item lines
1919
+ // (`- `), so folded (`>-`) and quoted single-line items both read once.
1920
+ const head = /^key_points:\n/m.exec(block);
1921
+ if (!head) return undefined;
1922
+ const body = block.slice(head.index + head[0].length);
1923
+ const endMatch = /^\S/m.exec(body);
1924
+ const list = endMatch ? body.slice(0, endMatch.index) : body;
1925
+ // A quoted scalar's quotes are YAML syntax, not text — strip them so label lengths are real.
1926
+ return list
1927
+ .split('\n')
1928
+ .filter((line) => /^[ \t]+- /.test(line))
1929
+ .map((line) => line.replace(/^[ \t]+- /, '').trim())
1930
+ .map((item) => item.replace(/^(["'])(.*)\1$/, '$2'));
1931
+ }
1932
+
1933
+ /** Count `key_points` list items in a Surfing post's frontmatter; undefined when absent/unreadable. */
1934
+ export function countKeyPoints(filePath: string): number | undefined {
1935
+ return readKeyPointItems(filePath)?.length;
1936
+ }
1937
+
1905
1938
  function firstRefinedTitle(parsed: Record<string, unknown> | null): string {
1906
1939
  const targets = parsed?.targets;
1907
1940
  if (!Array.isArray(targets)) return '';
@@ -1940,7 +1973,7 @@ function planItemsForPodcast(workDir: string, runDate: string): Array<{ title: s
1940
1973
  ? String((meta as Record<string, unknown>).brief).trim()
1941
1974
  : '';
1942
1975
  const body = typeof record.body === 'string' ? record.body.trim() : '';
1943
- const brief = metaBrief || (body.split(/(?<=[。!?])\s*/)[0] ?? '').trim();
1976
+ const brief = metaBrief || (splitSentences(body, { cjkOnly: true })[0] ?? '').trim();
1944
1977
  items.push(brief.length > 0 ? { title, brief } : { title });
1945
1978
  }
1946
1979
  return items;
@@ -3113,6 +3146,58 @@ export function dailyTranslateSync(args: string[]): void {
3113
3146
  `translate-sync: 4th argument must be the zh marker file (${runDate}_20_translate-localize_zh-post-path.txt), not the post itself — '${zhPathFile}' holds ${zhPostPath.includes('\n') ? 'a multi-line document' : `'${zhPostPath}'`}`,
3114
3147
  );
3115
3148
  }
3149
+ // The 2026-10-23 translate run published spliced en/ja description/key_points; the locale
3150
+ // channel has no judge, so gate the structural minimum here before the CMS sync:
3151
+ // a summary must exist and the key_points count must match the zh post (splices merge items).
3152
+ // The same run also invented retired series names (毎日AIブレンド, "Daily AI Medley",
3153
+ // zh-name listen links inside en/ja): normalizeSeriesNames rewrites the known bad set per
3154
+ // locale deterministically (URLs untouched) and any leftover it cannot make safe fails the
3155
+ // sync. The zh post rides the same sync, so it gets the same series-name pass.
3156
+ const zhPoints = countKeyPoints(zhPostPath);
3157
+ for (const locale of ['zh', 'en', 'ja'] as const) {
3158
+ const sibling = locale === 'zh' ? zhPostPath : zhPostPath.replace('/zh/', `/${locale}/`);
3159
+ if (!isFile(sibling)) continue;
3160
+ const problems: string[] = [];
3161
+ const raw = readFileSync(sibling, 'utf-8');
3162
+ const { markdown, remaining } = normalizeSeriesNames(raw, locale);
3163
+ if (markdown !== raw) sted('write', sibling, () => writeRaw(sibling, markdown));
3164
+ if (remaining.length > 0) {
3165
+ problems.push(`retired series names left after rewrite: ${remaining.join(' / ')}`);
3166
+ }
3167
+ if (locale !== 'zh') {
3168
+ if (readFrontmatterScalar(sibling, 'description') === undefined) {
3169
+ problems.push('description missing/empty');
3170
+ }
3171
+ const points = countKeyPoints(sibling);
3172
+ if (zhPoints !== undefined && points !== undefined && points !== zhPoints) {
3173
+ problems.push(`key_points count ${points} != zh ${zhPoints} (spliced or merged items)`);
3174
+ }
3175
+ // Label discipline (same 2026-10-23 audit): a key_points item is `Label — sentence`
3176
+ // and the label is a short phrase — ≤14 chars for ja, ≤28 for en — never a
3177
+ // sentence and never an ellipsis-truncated fragment.
3178
+ const items = readKeyPointItems(sibling);
3179
+ if (items !== undefined) {
3180
+ const cap = locale === 'en' ? 28 : 14;
3181
+ const overLimit = items.filter((item) => {
3182
+ const sep = item.indexOf(' — ');
3183
+ if (sep < 0) return false;
3184
+ const label = item.slice(0, sep);
3185
+ return label.includes('…') || Array.from(label).length > cap;
3186
+ });
3187
+ if (overLimit.length > 0) {
3188
+ problems.push(
3189
+ `key_points label over ${cap} chars or …-truncated: ${overLimit.length} of ${items.length} items`,
3190
+ );
3191
+ }
3192
+ }
3193
+ }
3194
+ if (problems.length > 0) {
3195
+ failWith(
3196
+ 1,
3197
+ `translate-sync: ${locale} draft failed the pre-sync structural check (${problems.join('; ')}) — repair ${sibling} per the translate stage contract before syncing`,
3198
+ );
3199
+ }
3200
+ }
3116
3201
  sted('write', sync, () =>
3117
3202
  writeJqText(sync, { body: '', options: { operation: 'sync', postPath: zhPostPath } }),
3118
3203
  );