@gobing-ai/knowledge-kit 0.1.9 → 0.1.11

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 (72) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/dist/index.js +132 -25
  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-scheduled-run.ts +298 -0
  22. package/plugins/kk/scripts/daily-stages.ts +1083 -49
  23. package/plugins/kk/scripts/kk-workflow-stages.ts +76 -10
  24. package/plugins/kk/scripts/safety-artifact-check.ts +235 -0
  25. package/plugins/kk/scripts/sentences.ts +134 -0
  26. package/plugins/kk/scripts/series-names.ts +124 -0
  27. package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +194 -113
  28. package/plugins/kk/workflows/kk-daily-safety.yaml +160 -36
  29. package/plugins/publishings/bilibili-pub/dist/index.js +6 -1
  30. package/plugins/publishings/bilibili-pub/dist/profile-check.js +2 -1
  31. package/plugins/publishings/cnblogs-pub/dist/index.js +6 -1
  32. package/plugins/publishings/cnblogs-pub/dist/profile-check.js +2 -1
  33. package/plugins/publishings/csdn-pub/dist/index.js +6 -1
  34. package/plugins/publishings/csdn-pub/dist/profile-check.js +6 -1
  35. package/plugins/publishings/emdash-pub/dist/index.js +4 -0
  36. package/plugins/publishings/infoq-pub/dist/index.js +6 -1
  37. package/plugins/publishings/infoq-pub/dist/profile-check.js +6 -1
  38. package/plugins/publishings/jike-pub/dist/index.js +6 -1
  39. package/plugins/publishings/jike-pub/dist/profile-check.js +6 -1
  40. package/plugins/publishings/juejin-pub/dist/index.js +6 -1
  41. package/plugins/publishings/juejin-pub/dist/profile-check.js +6 -1
  42. package/plugins/publishings/medium-pub/dist/index.js +4 -0
  43. package/plugins/publishings/podcast-pub/dist/index.js +151 -16
  44. package/plugins/publishings/podcast-pub/src/index.ts +62 -5
  45. package/plugins/publishings/podcast-pub/src/seo.ts +2 -4
  46. package/plugins/publishings/podcast-pub/src/show-notes.ts +3 -8
  47. package/plugins/publishings/qiita-pub/dist/index.js +4 -0
  48. package/plugins/publishings/shipinhao-pub/dist/index.js +6 -1
  49. package/plugins/publishings/shipinhao-pub/dist/profile-check.js +2 -1
  50. package/plugins/publishings/sspai-pub/dist/index.js +6 -1
  51. package/plugins/publishings/sspai-pub/dist/profile-check.js +2 -1
  52. package/plugins/publishings/substack-pub/dist/index.js +6 -1
  53. package/plugins/publishings/substack-pub/dist/profile-check.js +6 -1
  54. package/plugins/publishings/surfdash-pub/dist/index.js +691 -63
  55. package/plugins/publishings/surfdash-pub/src/digest-check.ts +284 -0
  56. package/plugins/publishings/surfdash-pub/src/index.ts +656 -84
  57. package/plugins/publishings/surfdash-pub/src/patch.ts +44 -7
  58. package/plugins/publishings/threads-pub/dist/index.js +4 -0
  59. package/plugins/publishings/wechatmp-pub/dist/index.js +937 -98
  60. package/plugins/publishings/wechatmp-pub/dist/profile-check.js +64 -3
  61. package/plugins/publishings/wechatmp-pub/src/flow.ts +1125 -85
  62. package/plugins/publishings/wechatmp-pub/src/index.ts +112 -30
  63. package/plugins/publishings/wechatmp-pub/src/page.ts +105 -0
  64. package/plugins/publishings/weibo-pub/dist/index.js +6 -1
  65. package/plugins/publishings/weibo-pub/dist/profile-check.js +6 -1
  66. package/plugins/publishings/x-pub/dist/index.js +6 -1
  67. package/plugins/publishings/x-pub/dist/profile-check.js +6 -1
  68. package/plugins/publishings/xhs-pub/dist/index.js +6 -1
  69. package/plugins/publishings/xhs-pub/dist/profile-check.js +6 -1
  70. package/plugins/publishings/xiaoyuzhou-pub/dist/index.js +6 -1
  71. package/plugins/publishings/xiaoyuzhou-pub/dist/profile-check.js +2 -1
  72. package/plugins/publishings/zenn-pub/dist/index.js +4 -0
@@ -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.11",
4
4
  "description": "knowledge-kit Claude Code plugin: skills, commands, subagents, hooks, and rules"
5
5
  }
@@ -0,0 +1,298 @@
1
+ #!/usr/bin/env bun
2
+ /** Supervise one scheduled daily run and persist its terminal outcome even after a deadline. */
3
+ import { spawn, spawnSync } from 'node:child_process';
4
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
5
+ import { dirname, join, resolve } from 'node:path';
6
+ import { parseArgs } from 'node:util';
7
+
8
+ const { values } = parseArgs({
9
+ options: {
10
+ repo: { type: 'string', default: process.cwd() },
11
+ 'works-dir': { type: 'string' },
12
+ 'run-id': { type: 'string' },
13
+ 'timeout-ms': { type: 'string', default: '13500000' },
14
+ receipt: { type: 'string' },
15
+ agent: { type: 'string', default: 'inline' },
16
+ 'retry-agent': { type: 'string', default: 'claude' },
17
+ 'required-channels': { type: 'string', default: 'wechat,locales,cms-resync' },
18
+ 'wechat-live': { type: 'boolean', default: false },
19
+ },
20
+ });
21
+ const timeoutMs = Number(values['timeout-ms']);
22
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) {
23
+ console.error('daily-scheduled-run: --timeout-ms must be a positive integer');
24
+ process.exit(2);
25
+ }
26
+ const repo = resolve(values.repo);
27
+ const now = new Date();
28
+ const stamp = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}`;
29
+ const runId = values['run-id'] ?? `daily-ai-voice-${stamp}-${now.getTime()}`;
30
+ const workDir = join(resolve(values['works-dir'] ?? join(repo, '.works')), 'kk-daily-ai-voice2', stamp);
31
+ const receiptPath = resolve(values.receipt ?? join(workDir, 'schedule-receipt.json'));
32
+ const startedAt = now.toISOString();
33
+ mkdirSync(dirname(receiptPath), { recursive: true });
34
+ const vars = {
35
+ publish_enabled: 'true',
36
+ skip_review: 'true',
37
+ agent: values.agent,
38
+ agent_retry: values['retry-agent'],
39
+ wechat_live: values['wechat-live'] === true ? 'true' : 'false',
40
+ work_dir: workDir,
41
+ run_date: stamp,
42
+ };
43
+ writeFileSync(receiptPath, JSON.stringify({ runId, startedAt, status: 'running', workDir }, null, 2));
44
+ let interrupted = false;
45
+ let timedOut = false;
46
+ let spawnError = '';
47
+ let forceKill: ReturnType<typeof setTimeout> | undefined;
48
+ const child = spawn(
49
+ 'spur',
50
+ [
51
+ '--no-logo',
52
+ 'workflow',
53
+ 'run',
54
+ 'plugins/kk/workflows/kk-daily-ai-voice2.yaml',
55
+ '--run-id',
56
+ runId,
57
+ '--vars',
58
+ JSON.stringify(vars),
59
+ '--quiet',
60
+ ],
61
+ { cwd: repo, detached: true, stdio: 'inherit' },
62
+ );
63
+
64
+ const ownedPids = new Set<number>();
65
+ /** Snapshot descendants before the root exits, including workers that created their own groups. */
66
+ function rememberDescendants(): void {
67
+ if (child.pid === undefined) return;
68
+ ownedPids.add(child.pid);
69
+ const snapshot = spawnSync('ps', ['-axo', 'pid=,ppid='], { encoding: 'utf-8', timeout: 5000 });
70
+ const rows = snapshot.stdout
71
+ .trim()
72
+ .split('\n')
73
+ .map((line) => line.trim().split(/\s+/).map(Number));
74
+ const descendants = new Set([child.pid]);
75
+ let added = true;
76
+ while (added) {
77
+ added = false;
78
+ for (const [pid, parent] of rows) {
79
+ if (pid !== undefined && parent !== undefined && descendants.has(parent) && !descendants.has(pid)) {
80
+ descendants.add(pid);
81
+ added = true;
82
+ }
83
+ }
84
+ }
85
+ for (const pid of descendants) ownedPids.add(pid);
86
+ }
87
+
88
+ /** Stop only the recorded owned tree; process-group signals also catch late same-group forks. */
89
+ function signalOwned(signal: NodeJS.Signals): void {
90
+ if (child.pid === undefined) return;
91
+ rememberDescendants();
92
+ for (const pid of [...ownedPids].reverse()) {
93
+ try {
94
+ process.kill(-pid, signal);
95
+ } catch {}
96
+ try {
97
+ process.kill(pid, signal);
98
+ } catch {}
99
+ }
100
+ }
101
+
102
+ function terminate(): void {
103
+ signalOwned('SIGTERM');
104
+ if (forceKill === undefined) {
105
+ forceKill = setTimeout(() => {
106
+ signalOwned('SIGKILL');
107
+ }, 300);
108
+ }
109
+ }
110
+ const onSignal = (): void => {
111
+ interrupted = true;
112
+ terminate();
113
+ };
114
+ process.once('SIGTERM', onSignal);
115
+ process.once('SIGINT', onSignal);
116
+ const deadline = setTimeout(() => {
117
+ timedOut = true;
118
+ terminate();
119
+ }, timeoutMs);
120
+ const exitCode = await new Promise<number>((done) => {
121
+ child.once('error', (error) => {
122
+ spawnError = error.message;
123
+ done(1);
124
+ });
125
+ child.once('close', (code) => done(code ?? 1));
126
+ });
127
+ clearTimeout(deadline);
128
+ if (forceKill !== undefined) clearTimeout(forceKill);
129
+ // A killed shell can leave owned descendants that ignore TERM. Always finish the group before
130
+ // recording a terminal outcome; no detached grandchild may continue to publish after a timeout.
131
+ if (child.pid !== undefined && (timedOut || interrupted || exitCode !== 0)) {
132
+ terminate();
133
+ await Bun.sleep(300);
134
+ signalOwned('SIGKILL');
135
+ await Bun.sleep(100);
136
+ }
137
+ const workflowStatus = timedOut ? 'timed-out' : interrupted ? 'interrupted' : exitCode === 0 ? 'done' : 'failed';
138
+ const primaryPublishStatus = readOptional(join(workDir, '31-publish', `${stamp}_17_publish_publish-status.txt`));
139
+ const runSummary = readOptional(join(workDir, 'run-report.summary.txt'));
140
+ const cleanup: { command: string; exitCode: number | null }[] = [];
141
+ let reservationRecovery: { status: string; reason: string; released?: number } = {
142
+ status: 'retained',
143
+ reason: 'workflow completed; publication settlement owns its reservations',
144
+ };
145
+ if (workflowStatus !== 'done' || runSummary?.startsWith('no-content:')) {
146
+ // The caller gives the nested safety run this exact id. Unknown/already-terminal ids are
147
+ // harmless cleanup failures; they never override the original workflow failure.
148
+ if (workflowStatus !== 'done') {
149
+ for (const id of [`${runId}-safety`, runId]) {
150
+ const result = spawnSync('spur', ['workflow', 'cancel', id, '--json'], {
151
+ cwd: repo,
152
+ encoding: 'utf-8',
153
+ timeout: 20000,
154
+ });
155
+ cleanup.push({ command: `spur workflow cancel ${id}`, exitCode: result.status });
156
+ }
157
+ }
158
+ // Only a complete trace proving no channel send began permits recycling reservations. A
159
+ // missing receipt alone is insufficient: a remote send may have succeeded before it crashed.
160
+ const trace = spawnSync('spur', ['workflow', 'trace', runId, '--json'], {
161
+ cwd: repo,
162
+ encoding: 'utf-8',
163
+ timeout: 20000,
164
+ });
165
+ reservationRecovery = { status: 'retained', reason: 'publication state could not be established' };
166
+ try {
167
+ const parsed = JSON.parse(trace.stdout) as {
168
+ run?: { runId?: string; workflowName?: string; status?: string; startedAt?: string };
169
+ events?: { kind?: string; node?: string; status?: string }[];
170
+ };
171
+ const sendNodes = new Set([
172
+ 'publish-surfdash',
173
+ 'publish-podcast',
174
+ 'xhs-publish',
175
+ 'wechat-publish',
176
+ 'translate-localize',
177
+ 'translate',
178
+ 'translate-retry',
179
+ 'translate-sync',
180
+ 'publish-classify',
181
+ ]);
182
+ const validTrace =
183
+ trace.status === 0 &&
184
+ parsed.run?.runId === runId &&
185
+ parsed.run?.workflowName === 'kk-daily-ai-voice2' &&
186
+ parsed.run?.status === (workflowStatus === 'done' ? 'done' : 'failed') &&
187
+ Date.parse(parsed.run.startedAt ?? '') >= Date.parse(startedAt) - 1000 &&
188
+ Date.parse(parsed.run.startedAt ?? '') <= Date.now() &&
189
+ Array.isArray(parsed.events) &&
190
+ parsed.events.length > 0;
191
+ const sendStarted = parsed.events?.some(
192
+ (event) => event.kind === 'action' && sendNodes.has(event.node ?? '') && event.status !== 'skipped',
193
+ );
194
+ if (validTrace && !sendStarted) {
195
+ // Cancellation is durable before reuse. Require a second stable snapshot after the
196
+ // owned processes are gone, so an in-flight or partially finalized row cannot pass.
197
+ await Bun.sleep(200);
198
+ const secondTrace = spawnSync('spur', ['workflow', 'trace', runId, '--json'], {
199
+ cwd: repo,
200
+ encoding: 'utf-8',
201
+ timeout: 20000,
202
+ });
203
+ const second = JSON.parse(secondTrace.stdout) as typeof parsed;
204
+ if (
205
+ secondTrace.status !== 0 ||
206
+ JSON.stringify(second.run) !== JSON.stringify(parsed.run) ||
207
+ JSON.stringify(second.events) !== JSON.stringify(parsed.events)
208
+ ) {
209
+ throw new Error('failed-run trace is not stable after cancellation');
210
+ }
211
+ const release = spawnSync('kk', ['store', 'release', '--topic', 'ai-news', '--run', runId], {
212
+ cwd: repo,
213
+ encoding: 'utf-8',
214
+ timeout: 20000,
215
+ });
216
+ cleanup.push({ command: 'kk store release (verified pre-publication failure)', exitCode: release.status });
217
+ const result = release.status === 0 ? (JSON.parse(release.stdout) as { released?: unknown }) : null;
218
+ reservationRecovery =
219
+ result !== null && typeof result.released === 'number'
220
+ ? { status: 'released', reason: 'trace proves no channel send began', released: result.released }
221
+ : { status: 'retained', reason: 'reservation release did not produce a valid receipt' };
222
+ } else if (sendStarted) {
223
+ reservationRecovery = { status: 'retained', reason: 'a channel send began; preserve uncertain delivery' };
224
+ }
225
+ } catch {
226
+ // Unreadable/truncated evidence or an unexpected CLI shape must never cause reuse of a
227
+ // potentially aired story. Keep the reservation and the original failure visible.
228
+ }
229
+ }
230
+ const ledger = spawnSync('kk', ['ledger', 'sync', '--workflow', 'kk-daily-ai-voice2', '--work-dir', workDir], {
231
+ cwd: repo,
232
+ encoding: 'utf-8',
233
+ timeout: 20000,
234
+ });
235
+ cleanup.push({ command: 'kk ledger sync', exitCode: ledger.status });
236
+ function readOptional(path: string): string | null {
237
+ try {
238
+ return readFileSync(path, 'utf-8').trim();
239
+ } catch {
240
+ return null;
241
+ }
242
+ }
243
+ const requiredChannels = values['required-channels']
244
+ .split(',')
245
+ .map((channel) => channel.trim())
246
+ .filter(Boolean);
247
+ let channelCompleteness: { channel: string; state: string; detail?: string }[] = [];
248
+ try {
249
+ const parsed = JSON.parse(
250
+ readOptional(join(workDir, '31-publish', `${stamp}_17_publish_completeness.json`)) ?? 'null',
251
+ ) as { rows?: unknown } | null;
252
+ if (Array.isArray(parsed?.rows)) {
253
+ channelCompleteness = parsed.rows.filter(
254
+ (row): row is { channel: string; state: string; detail?: string } =>
255
+ row !== null &&
256
+ typeof row === 'object' &&
257
+ typeof row.channel === 'string' &&
258
+ typeof row.state === 'string',
259
+ );
260
+ }
261
+ } catch {}
262
+ const missingRequiredChannels = requiredChannels.filter((channel) => {
263
+ const rows = channelCompleteness.filter((row) => row.channel === channel);
264
+ return rows.length !== 1 || rows[0]?.state !== 'done';
265
+ });
266
+ // A terminal green workflow can mean "no content" or failed delivery. Make that observable to
267
+ // the scheduler instead of equating the workflow's process exit with a published edition.
268
+ const status =
269
+ workflowStatus !== 'done'
270
+ ? workflowStatus
271
+ : runSummary?.startsWith('no-content:')
272
+ ? 'no-content'
273
+ : primaryPublishStatus === 'full' && missingRequiredChannels.length === 0
274
+ ? 'done'
275
+ : 'incomplete';
276
+ const receipt = {
277
+ runId,
278
+ startedAt,
279
+ completedAt: new Date().toISOString(),
280
+ status,
281
+ exitCode: timedOut ? 124 : interrupted ? 143 : exitCode !== 0 ? exitCode : status === 'done' ? 0 : 4,
282
+ workDir,
283
+ timeoutMs,
284
+ agent: values.agent,
285
+ retryAgent: values['retry-agent'],
286
+ error: spawnError || null,
287
+ primaryPublishStatus,
288
+ channelCompleteness,
289
+ missingRequiredChannels,
290
+ runSummary,
291
+ reservationRecovery,
292
+ cleanup,
293
+ };
294
+ writeFileSync(receiptPath, `${JSON.stringify(receipt, null, 2)}\n`);
295
+ console.log(JSON.stringify(receipt));
296
+ process.removeListener('SIGTERM', onSignal);
297
+ process.removeListener('SIGINT', onSignal);
298
+ process.exit(receipt.exitCode);