@gobing-ai/knowledge-kit 0.1.11 → 0.1.12

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 (126) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/dist/index.js +2359 -1721
  3. package/package.json +1 -1
  4. package/plugins/generations/image-gen/README.md +108 -0
  5. package/plugins/generations/image-gen/assets/sample/cover__clean-webapp-ui.jpg +0 -0
  6. package/plugins/generations/image-gen/assets/sample/cover__cute.jpg +0 -0
  7. package/plugins/generations/image-gen/assets/sample/cover__daily-news.jpg +0 -0
  8. package/plugins/generations/image-gen/assets/sample/cover__editorial.jpg +0 -0
  9. package/plugins/generations/image-gen/assets/sample/cover__fresh.jpg +0 -0
  10. package/plugins/generations/image-gen/assets/sample/cover__minimalist.jpg +0 -0
  11. package/plugins/generations/image-gen/assets/sample/cover__photorealistic.jpg +0 -0
  12. package/plugins/generations/image-gen/assets/sample/cover__sketch.jpg +0 -0
  13. package/plugins/generations/image-gen/assets/sample/cover__technical-diagram.jpg +0 -0
  14. package/plugins/generations/image-gen/assets/sample/cover__vibrant.jpg +0 -0
  15. package/plugins/generations/image-gen/assets/sample/cover__warm.jpg +0 -0
  16. package/plugins/generations/image-gen/assets/sample/cover__xhs-knowledge.jpg +0 -0
  17. package/plugins/generations/image-gen/assets/sample/daily-news-cover__clean-webapp-ui.jpg +0 -0
  18. package/plugins/generations/image-gen/assets/sample/daily-news-cover__cute.jpg +0 -0
  19. package/plugins/generations/image-gen/assets/sample/daily-news-cover__daily-news.jpg +0 -0
  20. package/plugins/generations/image-gen/assets/sample/daily-news-cover__editorial.jpg +0 -0
  21. package/plugins/generations/image-gen/assets/sample/daily-news-cover__fresh.jpg +0 -0
  22. package/plugins/generations/image-gen/assets/sample/daily-news-cover__minimalist.jpg +0 -0
  23. package/plugins/generations/image-gen/assets/sample/daily-news-cover__photorealistic.jpg +0 -0
  24. package/plugins/generations/image-gen/assets/sample/daily-news-cover__sketch.jpg +0 -0
  25. package/plugins/generations/image-gen/assets/sample/daily-news-cover__technical-diagram.jpg +0 -0
  26. package/plugins/generations/image-gen/assets/sample/daily-news-cover__vibrant.jpg +0 -0
  27. package/plugins/generations/image-gen/assets/sample/daily-news-cover__warm.jpg +0 -0
  28. package/plugins/generations/image-gen/assets/sample/daily-news-cover__xhs-knowledge.jpg +0 -0
  29. package/plugins/generations/image-gen/assets/sample/free__clean-webapp-ui.jpg +0 -0
  30. package/plugins/generations/image-gen/assets/sample/free__cute.jpg +0 -0
  31. package/plugins/generations/image-gen/assets/sample/free__daily-news.jpg +0 -0
  32. package/plugins/generations/image-gen/assets/sample/free__editorial.jpg +0 -0
  33. package/plugins/generations/image-gen/assets/sample/free__fresh.jpg +0 -0
  34. package/plugins/generations/image-gen/assets/sample/free__minimalist.jpg +0 -0
  35. package/plugins/generations/image-gen/assets/sample/free__photorealistic.jpg +0 -0
  36. package/plugins/generations/image-gen/assets/sample/free__sketch.jpg +0 -0
  37. package/plugins/generations/image-gen/assets/sample/free__technical-diagram.jpg +0 -0
  38. package/plugins/generations/image-gen/assets/sample/free__vibrant.jpg +0 -0
  39. package/plugins/generations/image-gen/assets/sample/free__warm.jpg +0 -0
  40. package/plugins/generations/image-gen/assets/sample/free__xhs-knowledge.jpg +0 -0
  41. package/plugins/generations/image-gen/assets/sample/illustration__clean-webapp-ui.jpg +0 -0
  42. package/plugins/generations/image-gen/assets/sample/illustration__cute.jpg +0 -0
  43. package/plugins/generations/image-gen/assets/sample/illustration__daily-news.jpg +0 -0
  44. package/plugins/generations/image-gen/assets/sample/illustration__editorial.jpg +0 -0
  45. package/plugins/generations/image-gen/assets/sample/illustration__fresh.jpg +0 -0
  46. package/plugins/generations/image-gen/assets/sample/illustration__minimalist.jpg +0 -0
  47. package/plugins/generations/image-gen/assets/sample/illustration__photorealistic.jpg +0 -0
  48. package/plugins/generations/image-gen/assets/sample/illustration__sketch.jpg +0 -0
  49. package/plugins/generations/image-gen/assets/sample/illustration__technical-diagram.jpg +0 -0
  50. package/plugins/generations/image-gen/assets/sample/illustration__vibrant.jpg +0 -0
  51. package/plugins/generations/image-gen/assets/sample/illustration__warm.jpg +0 -0
  52. package/plugins/generations/image-gen/assets/sample/illustration__xhs-knowledge.jpg +0 -0
  53. package/plugins/generations/image-gen/assets/sample/xhs-card__clean-webapp-ui.jpg +0 -0
  54. package/plugins/generations/image-gen/assets/sample/xhs-card__cute.jpg +0 -0
  55. package/plugins/generations/image-gen/assets/sample/xhs-card__daily-news.jpg +0 -0
  56. package/plugins/generations/image-gen/assets/sample/xhs-card__editorial.jpg +0 -0
  57. package/plugins/generations/image-gen/assets/sample/xhs-card__fresh.jpg +0 -0
  58. package/plugins/generations/image-gen/assets/sample/xhs-card__minimalist.jpg +0 -0
  59. package/plugins/generations/image-gen/assets/sample/xhs-card__photorealistic.jpg +0 -0
  60. package/plugins/generations/image-gen/assets/sample/xhs-card__sketch.jpg +0 -0
  61. package/plugins/generations/image-gen/assets/sample/xhs-card__technical-diagram.jpg +0 -0
  62. package/plugins/generations/image-gen/assets/sample/xhs-card__vibrant.jpg +0 -0
  63. package/plugins/generations/image-gen/assets/sample/xhs-card__warm.jpg +0 -0
  64. package/plugins/generations/image-gen/assets/sample/xhs-card__xhs-knowledge.jpg +0 -0
  65. package/plugins/generations/image-gen/dist/index.js +102 -58
  66. package/plugins/generations/image-gen/scripts/compose-e2e-artifact.ts +5 -21
  67. package/plugins/generations/image-gen/scripts/gen-samples.ts +208 -0
  68. package/plugins/generations/image-gen/src/compose/chromium.ts +2 -3
  69. package/plugins/generations/image-gen/src/presets.ts +6 -3
  70. package/plugins/generations/image-gen/src/providers/errors.ts +16 -1
  71. package/plugins/generations/image-gen/src/providers/minimax.ts +46 -17
  72. package/plugins/ingestions/web-search/src/index.ts +1 -1
  73. package/plugins/kk/README.md +18 -0
  74. package/plugins/kk/commands/image-extract.md +1 -0
  75. package/plugins/kk/commands/image-generate.md +1 -0
  76. package/plugins/kk/commands/tell-me.md +1 -0
  77. package/plugins/kk/commands/workflow-run.md +7 -1
  78. package/plugins/kk/decisions/extract/it-topics.yaml +24 -0
  79. package/plugins/kk/decisions/kk-itc.yaml +6 -4
  80. package/plugins/kk/plugin.json +1 -1
  81. package/plugins/kk/publish-presets/daily-ai-voice-migration.json +146 -0
  82. package/plugins/kk/publish-presets/daily-ai-voice.json +11 -3
  83. package/plugins/kk/schedules/kk-collect.grok-bot.md.tmpl +69 -0
  84. package/plugins/kk/schedules/kk-daily-ai-voice3.cron.tmpl +18 -0
  85. package/plugins/kk/schedules/kk-daily-publish3-cn-peak.cron.tmpl +15 -5
  86. package/plugins/kk/schedules/kk-daily-publish3-overseas.cron.tmpl +9 -4
  87. package/plugins/kk/scripts/daily-stages.ts +346 -48
  88. package/plugins/kk/scripts/kk-workflow-stages.ts +190 -0
  89. package/plugins/kk/scripts/safety-artifact-check.ts +70 -28
  90. package/plugins/kk/scripts/voice3-publish-window.ts +202 -0
  91. package/plugins/kk/skills/article-adapt/SKILL.md +1 -0
  92. package/plugins/kk/skills/audio-authoring/SKILL.md +1 -0
  93. package/plugins/kk/skills/content-judge/SKILL.md +1 -0
  94. package/plugins/kk/skills/content-safety/SKILL.md +1 -0
  95. package/plugins/kk/skills/explain-things/SKILL.md +1 -0
  96. package/plugins/kk/skills/image-authoring/SKILL.md +1 -0
  97. package/plugins/kk/skills/itc-generating/SKILL.md +1 -0
  98. package/plugins/kk/skills/publish/SKILL.md +1 -0
  99. package/plugins/kk/skills/smart-seo/SKILL.md +1 -0
  100. package/plugins/kk/skills/storm-research/SKILL.md +1 -0
  101. package/plugins/kk/skills/taste-unslop/SKILL.md +1 -0
  102. package/plugins/kk/skills/topic/SKILL.md +1 -0
  103. package/plugins/kk/skills/web-automation/SKILL.md +1 -0
  104. package/plugins/kk/skills/xhs-images/SKILL.md +1 -0
  105. package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +9 -7
  106. package/plugins/kk/workflows/kk-daily-ai-voice3.yaml +161 -61
  107. package/plugins/kk/workflows/kk-daily-publish3.yaml +82 -26
  108. package/plugins/kk/workflows/kk-daily-safety.yaml +39 -31
  109. package/plugins/kk/workflows/kk-itc-author.yaml +75 -0
  110. package/plugins/kk/workflows/kk-itc-publish-tail.yaml +25 -3
  111. package/plugins/kk/workflows/kk-itc.yaml +36 -4
  112. package/plugins/publishings/linkedin-pub/dist/index.js +21333 -0
  113. package/plugins/publishings/linkedin-pub/dist/profile-check.js +21005 -0
  114. package/plugins/publishings/linkedin-pub/package.json +19 -0
  115. package/plugins/publishings/linkedin-pub/plugin.json +12 -0
  116. package/plugins/publishings/linkedin-pub/src/index.ts +705 -0
  117. package/plugins/publishings/linkedin-pub/src/profile-check.ts +17 -0
  118. package/plugins/publishings/linkedin-pub/tsconfig.json +4 -0
  119. package/plugins/publishings/surfdash-pub/dist/index.js +1 -1
  120. package/plugins/publishings/surfdash-pub/src/index.ts +3 -1
  121. package/plugins/publishings/threads-pub/dist/index.js +7 -0
  122. package/plugins/publishings/threads-pub/src/index.ts +12 -0
  123. package/plugins/publishings/wechatmp-pub/dist/index.js +5 -2
  124. package/plugins/publishings/wechatmp-pub/src/flow.ts +11 -3
  125. package/plugins/publishings/x-pub/dist/index.js +7 -0
  126. package/plugins/publishings/x-pub/src/index.ts +11 -0
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Preset sample gallery (task 0340): render one sample per shipped format × style
3
+ * pair into assets/sample/ and regenerate README.md as the index over them.
4
+ *
5
+ * Usage (run from the repo root so `kk` resolves this checkout's plugin):
6
+ * bun plugins/generations/image-gen/scripts/gen-samples.ts # render missing pairs + README
7
+ * bun plugins/generations/image-gen/scripts/gen-samples.ts --force # re-render every pair
8
+ * bun plugins/generations/image-gen/scripts/gen-samples.ts --readme-only # README from samples on disk
9
+ * bun plugins/generations/image-gen/scripts/gen-samples.ts --provider gemini --model <m>
10
+ *
11
+ * Only the builtin presets/ root is read — user/project preset roots never leak
12
+ * into the shipped index. A finished sample is never overwritten without --force,
13
+ * so a run that dies on pair 37 resumes from pair 37. Failed pairs are named and
14
+ * the script exits 1.
15
+ */
16
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync } from 'node:fs';
17
+ import { join } from 'node:path';
18
+ import { type FormatPreset, FormatPresetSchema, type StylePreset, StylePresetSchema } from '../src/presets';
19
+
20
+ const PLUGIN_DIR = join(import.meta.dir, '..');
21
+ const SAMPLE_DIR = join(PLUGIN_DIR, 'assets', 'sample');
22
+ const README = join(PLUGIN_DIR, 'README.md');
23
+ const WORK_DIR = join(process.cwd(), '.works', 'tmp', 'test-samples');
24
+ const THUMB_PX = 640;
25
+ const CONCURRENCY = 3;
26
+
27
+ /** Fixed per-job inputs so every style is compared on the same subject. */
28
+ const SAMPLE_SUBJECT = 'a request flowing through an API gateway to three microservices';
29
+ const COVER_VARS = {
30
+ title: 'Building Reliable AI Agents',
31
+ subtitle: 'Patterns for production',
32
+ topics: 'AI agents',
33
+ };
34
+
35
+ const args = process.argv.slice(2);
36
+ const flag = (name: string): boolean => args.includes(name);
37
+ const option = (name: string, fallback: string): string => {
38
+ const index = args.indexOf(name);
39
+ return index >= 0 && args[index + 1] !== undefined ? args[index + 1] : fallback;
40
+ };
41
+ const provider = option('--provider', 'minimax');
42
+ const model = option('--model', 'image-01');
43
+
44
+ function loadBuiltin<T>(axis: string, schema: { parse(value: unknown): T }): T[] {
45
+ const dir = join(PLUGIN_DIR, 'presets', axis);
46
+ return readdirSync(dir)
47
+ .filter((entry) => entry.endsWith('.yaml'))
48
+ .sort()
49
+ .map((entry) => schema.parse(Bun.YAML.parse(readFileSync(join(dir, entry), 'utf8'))));
50
+ }
51
+
52
+ const formats = loadBuiltin<FormatPreset>('formats', FormatPresetSchema);
53
+ const styles = loadBuiltin<StylePreset>('styles', StylePresetSchema);
54
+ const sampleName = (format: string, style: string): string => `${format}__${style}.jpg`;
55
+
56
+ /** The Doc envelope that drives one pair through the real executor. */
57
+ function envelope(format: FormatPreset, style: StylePreset): unknown[] {
58
+ const imageGen: Record<string, unknown> = {
59
+ job: format.job,
60
+ format: format.name,
61
+ style: style.name,
62
+ provider,
63
+ model,
64
+ };
65
+ const doc: Record<string, unknown> = { id: `${format.name}__${style.name}`, title: COVER_VARS.title, body: '' };
66
+ if (format.job === 'cover') {
67
+ imageGen.vars = COVER_VARS;
68
+ } else if (format.job === 'illustration') {
69
+ // The anchor must occur exactly once in the body (index.ts assertAnchorPlan).
70
+ doc.body = `Every call enters at the edge: ${SAMPLE_SUBJECT}.`;
71
+ doc.metadata = { positions: [SAMPLE_SUBJECT] };
72
+ } else if (format.compose !== undefined) {
73
+ imageGen.vars = { subject: SAMPLE_SUBJECT, headline: 'API 网关', points: '统一入口\n鉴权限流\n路由转发' };
74
+ } else {
75
+ imageGen.vars = { subject: SAMPLE_SUBJECT };
76
+ }
77
+ doc.metadata = { ...((doc.metadata as object) ?? {}), imageGen: { ...imageGen, maxImages: 1 } };
78
+ return [doc];
79
+ }
80
+
81
+ async function render(format: FormatPreset, style: StylePreset): Promise<void> {
82
+ const stem = `${format.name}__${style.name}`;
83
+ const dir = join(WORK_DIR, stem);
84
+ rmSync(dir, { recursive: true, force: true });
85
+ mkdirSync(dir, { recursive: true });
86
+ const input = join(dir, 'in.json');
87
+ const output = join(dir, 'out.json');
88
+ await Bun.write(input, JSON.stringify(envelope(format, style)));
89
+ const run = Bun.spawn(['kk', 'executor', 'run', 'image-gen', '--in', input, '--out', output], {
90
+ stdout: 'pipe',
91
+ stderr: 'pipe',
92
+ });
93
+ const [code, stderr] = await Promise.all([run.exited, new Response(run.stderr).text()]);
94
+ if (code !== 0) {
95
+ throw new Error(`kk exited ${code}: ${stderr.trim().split('\n').slice(-3).join(' | ')}`);
96
+ }
97
+ const image = (await Bun.file(output).json())?.metadata?.images?.[0]?.path as string | undefined;
98
+ if (image === undefined) {
99
+ throw new Error('executor output carries no metadata.images[0].path');
100
+ }
101
+ const source = image.startsWith('/') ? image : join(dir, image);
102
+ // ponytail: macOS sips does the downscale (no image dep in the repo); add sharp if this must run on Linux.
103
+ const sips = Bun.spawnSync([
104
+ 'sips',
105
+ '-Z',
106
+ String(THUMB_PX),
107
+ '-s',
108
+ 'format',
109
+ 'jpeg',
110
+ '-s',
111
+ 'formatOptions',
112
+ '80',
113
+ source,
114
+ '--out',
115
+ join(SAMPLE_DIR, sampleName(format.name, style.name)),
116
+ ]);
117
+ if (sips.exitCode !== 0) {
118
+ throw new Error(`sips failed: ${sips.stderr.toString().trim()}`);
119
+ }
120
+ }
121
+
122
+ async function renderAll(): Promise<string[]> {
123
+ mkdirSync(SAMPLE_DIR, { recursive: true });
124
+ const queue = formats.flatMap((format) =>
125
+ styles
126
+ .filter((style) => flag('--force') || !existsSync(join(SAMPLE_DIR, sampleName(format.name, style.name))))
127
+ .map((style) => ({ format, style })),
128
+ );
129
+ console.log(`${queue.length} of ${formats.length * styles.length} pairs to render via ${provider}/${model}`);
130
+ const failed: string[] = [];
131
+ const worker = async (): Promise<void> => {
132
+ for (let pair = queue.shift(); pair !== undefined; pair = queue.shift()) {
133
+ const name = `${pair.format.name} × ${pair.style.name}`;
134
+ try {
135
+ await render(pair.format, pair.style);
136
+ console.log(`ok ${name}`);
137
+ } catch (error) {
138
+ failed.push(name);
139
+ console.error(`FAIL ${name}: ${error instanceof Error ? error.message : error}`);
140
+ }
141
+ }
142
+ };
143
+ await Promise.all(Array.from({ length: CONCURRENCY }, worker));
144
+ return failed;
145
+ }
146
+
147
+ const oneLine = (text: string): string => text.replace(/\s+/g, ' ').trim().replace(/\|/g, '\\|');
148
+
149
+ function readme(): string {
150
+ const lines = [
151
+ '# image-gen',
152
+ '',
153
+ 'Image generation plugin (`kind: generator`): `free`, `cover` and `illustration` jobs over the',
154
+ '`Doc[] → Content` contract. Every image is one **format** (canvas, template, job) composed with one',
155
+ '**style** (visual modifiers); any format pairs with any style.',
156
+ '',
157
+ 'This page is the preset index: one sample per shipped format × style pair, so you can pick a',
158
+ 'combination by eye. Pass the pair as `metadata.imageGen.format` / `metadata.imageGen.style`, or',
159
+ "`--format` / `--style` to the `kk:image-generate` skill. Omitting the style uses the format's",
160
+ 'default (★ below).',
161
+ '',
162
+ `Samples: provider \`${provider}\`, model \`${model}\`, downscaled to ${THUMB_PX}px JPEG. Inputs are fixed per`,
163
+ `job — subject “${SAMPLE_SUBJECT}”; cover title “${COVER_VARS.title}”. Regenerate with`,
164
+ '`bun plugins/generations/image-gen/scripts/gen-samples.ts` (see its header for flags).',
165
+ '',
166
+ '## Formats',
167
+ '',
168
+ '| Format | Job | Aspect | Size | Default style | Description |',
169
+ '| --- | --- | --- | --- | --- | --- |',
170
+ ...formats.map(
171
+ (f) =>
172
+ `| [\`${f.name}\`](#format-${f.name}) | ${f.job} | ${f.aspectRatio} | ${f.width ?? '?'}×${f.height ?? '?'} | ${f.defaultStyle} | ${oneLine(f.description)} |`,
173
+ ),
174
+ '',
175
+ '## Styles',
176
+ '',
177
+ '| Style | Description | Best for |',
178
+ '| --- | --- | --- |',
179
+ ...styles.map((s) => `| \`${s.name}\` | ${oneLine(s.description)} | ${oneLine(s.bestFor ?? '')} |`),
180
+ '',
181
+ '## Gallery',
182
+ ];
183
+ const columns = 3;
184
+ for (const format of formats) {
185
+ lines.push('', `<a id="format-${format.name}"></a>`, '', `### ${format.name}`, '');
186
+ lines.push(`${format.job} job · ${format.aspectRatio} · default style \`${format.defaultStyle}\``, '');
187
+ lines.push(`|${' |'.repeat(columns)}`, `|${' --- |'.repeat(columns)}`);
188
+ for (let i = 0; i < styles.length; i += columns) {
189
+ const cells = styles.slice(i, i + columns).map((style) => {
190
+ const file = sampleName(format.name, style.name);
191
+ const label = `\`${style.name}\`${style.name === format.defaultStyle ? ' ★' : ''}`;
192
+ return existsSync(join(SAMPLE_DIR, file))
193
+ ? `<img src="assets/sample/${file}" alt="${format.name} × ${style.name}" width="260"><br>${label}`
194
+ : `_not rendered_<br>${label}`;
195
+ });
196
+ lines.push(`| ${cells.join(' | ')} |`);
197
+ }
198
+ }
199
+ return `${lines.join('\n')}\n`;
200
+ }
201
+
202
+ const failed = flag('--readme-only') ? [] : await renderAll();
203
+ await Bun.write(README, readme());
204
+ console.log(`wrote ${README}`);
205
+ if (failed.length > 0) {
206
+ console.error(`${failed.length} pair(s) failed — rerun to retry only those:\n ${failed.join('\n ')}`);
207
+ process.exit(1);
208
+ }
@@ -11,9 +11,8 @@
11
11
  * Playwright's pipe under Bun 1.3.14 stalls intermittently (fulfill never
12
12
  * returns, `document.fonts.ready` never settles — 0212 dogfood, ~1 in 3 runs).
13
13
  *
14
- * The E2E test still skips under `bun test` (Playwright's launch blocks that
15
- * runner's event loop); the evidence script runs each variant pair in its own
16
- * subprocess. `compose` carries a deadline as the backstop for any other stall.
14
+ * The E2E test runs inside `bun test` on Bun 1.4.3 (the 1.3.14 launch block is
15
+ * gone). `compose` carries a deadline as the backstop for any other stall.
17
16
  *
18
17
  * Missing Playwright or Chromium surfaces as an actionable error naming the
19
18
  * install command.
@@ -187,7 +187,9 @@ function matchPlaceholder(template: string, start: number): { expr: string; end:
187
187
 
188
188
  /**
189
189
  * Resolve one expression against the merged vars. An empty value (including
190
- * a caller-passed '') resolves to the fallback; an unknown placeholder with
190
+ * a caller-passed '') resolves to the fallback, except a wrapper fallback
191
+ * (one embedding its own `{{name}}`), which drops out entirely so its literal
192
+ * slot never reaches the provider; an unknown placeholder with
191
193
  * no fallback throws, naming the placeholder and the format — wt silently
192
194
  * left it in place, and a literal `{{title}}` must not reach a paid provider.
193
195
  * Leading trim on the fallback skips the `| ` separator; its trailing space
@@ -200,13 +202,14 @@ function resolvePlaceholder(expr: string, vars: Record<string, string>, formatNa
200
202
  const name = expr.slice(0, pipe).trim();
201
203
  const fallback = expr.slice(pipe + 1).trimStart();
202
204
  const value = vars[name];
205
+ const slot = `{{${name}}}`;
203
206
  if (!value) {
204
- return fallback;
207
+ return fallback.includes(slot) ? '' : fallback;
205
208
  }
206
209
  // A fallback that embeds its own '{{name}}' slot is a mini-template
207
210
  // wrapping the value (the R6 golden's ', subtitled "…"' form); a
208
211
  // plain-text fallback is just the default, so the value wins.
209
- return fallback.includes(`{{${name}}}`) ? fallback.split(`{{${name}}}`).join(value) : value;
212
+ return fallback.includes(slot) ? fallback.split(slot).join(value) : value;
210
213
  }
211
214
  const name = expr.trim();
212
215
  const value = vars[name];
@@ -24,6 +24,16 @@
24
24
  /** The retry decision classes the D6 fallback loop (0166) acts on. */
25
25
  export type ProviderErrorClass = 'auth' | 'quota' | 'transient' | 'policy' | 'unknown';
26
26
 
27
+ /** MiniMax business codes are not HTTP statuses; messages may be only "system error". */
28
+ export function minimaxErrorClass(code: number): ProviderErrorClass {
29
+ // https://platform.minimax.io/docs/api-reference/errorcode
30
+ if ([1026, 1027, 1040, 1046].includes(code)) return 'policy';
31
+ if ([1004, 2049].includes(code)) return 'auth';
32
+ if ([1002, 1008, 1039, 1045, 2056].includes(code)) return 'quota';
33
+ if ([1000, 1001, 1024, 1033].includes(code)) return 'transient';
34
+ return 'unknown';
35
+ }
36
+
27
37
  /** Content-policy / safety / moderation rejections — checked first, they often arrive as 400/403. */
28
38
  const POLICY_RE = /content[_ -]?policy|moderation|safety|sensitive|inappropriate|violat|blocked/i;
29
39
  /** Credential wording; 401/403 statuses are checked separately. */
@@ -69,7 +79,12 @@ function partsOf(err: unknown): { readonly name: string; readonly message: strin
69
79
  export function classifyProviderError(err: unknown): ProviderErrorClass {
70
80
  const parts = partsOf(err);
71
81
  const statuses = statusesIn(parts.message);
72
- if (POLICY_RE.test(parts.message)) return 'policy';
82
+ const miniCode = /^MiniMax API error \(status_code=(\d+)\):/.exec(parts.message)?.[1];
83
+ const miniClass = miniCode === undefined ? 'unknown' : minimaxErrorClass(Number(miniCode));
84
+ if (miniClass === 'policy' || POLICY_RE.test(parts.message)) return 'policy';
85
+ // An explicit business code owns retry semantics; e.g. invalid params (2013) must not
86
+ // become transient merely because its description contains the word "timeout".
87
+ if (miniCode !== undefined) return miniClass;
73
88
  if (statuses.some((status) => status === 401 || status === 403) || AUTH_RE.test(parts.message)) return 'auth';
74
89
  if (statuses.some((status) => status === 402 || status === 429) || QUOTA_RE.test(parts.message)) return 'quota';
75
90
  if (
@@ -1,5 +1,8 @@
1
1
  import { Buffer } from 'node:buffer';
2
+ import { setTimeout as delay } from 'node:timers/promises';
3
+ import { echoError } from '@gobing-ai/ts-utils';
2
4
  import { readBytes } from '../bytes';
5
+ import { classifyProviderError } from './errors';
3
6
  import { mimeForPath } from './refs';
4
7
  import { PROVIDER_REQUEST_TIMEOUT_MS } from './timeouts';
5
8
  import type { ImageProvider, ImageRequest, ImageResult, ProviderFactory, ProviderOptions } from './types';
@@ -41,6 +44,13 @@ interface MinimaxResponse {
41
44
  };
42
45
  }
43
46
 
47
+ function responseError(result: MinimaxResponse): Error | null {
48
+ const code = result?.base_resp?.status_code;
49
+ return code !== undefined && code !== 0
50
+ ? new Error(`MiniMax API error (status_code=${code}): ${result.base_resp?.status_msg ?? 'unknown error'}`)
51
+ : null;
52
+ }
53
+
44
54
  export function parsePixelSize(size: string): { width: number; height: number } | null {
45
55
  const match = size.trim().match(/^(\d+)\s*[xX*]\s*(\d+)$/);
46
56
  if (match === null) return null;
@@ -126,10 +136,8 @@ export async function extractImageFromResponse(
126
136
  fetchFn: typeof fetch = fetch,
127
137
  timeoutMs: number = PROVIDER_REQUEST_TIMEOUT_MS,
128
138
  ): Promise<Uint8Array> {
129
- const baseResp = result.base_resp;
130
- if (baseResp?.status_code !== undefined && baseResp.status_code !== 0) {
131
- throw new Error(baseResp.status_msg ?? `MiniMax API returned status_code=${baseResp.status_code}`);
132
- }
139
+ const error = responseError(result);
140
+ if (error !== null) throw error;
133
141
  const base64Image = result.data?.image_base64?.[0];
134
142
  if (base64Image !== undefined) return Uint8Array.from(Buffer.from(base64Image, 'base64'));
135
143
  const url = result.data?.image_urls?.[0];
@@ -167,20 +175,41 @@ export const createMinimax: ProviderFactory = (options?: ProviderOptions): Image
167
175
 
168
176
  const base = (readEnv('MINIMAX_BASE_URL') || 'https://api.minimaxi.com').replace(/\/+$/g, '');
169
177
  const url = base.endsWith('/v1') ? `${base}/image_generation` : `${base}/v1/image_generation`;
170
- const response = await fetchFn(url, {
171
- method: 'POST',
172
- headers: { 'content-type': 'application/json', authorization: `Bearer ${apiKey}` },
173
- body: JSON.stringify(body),
174
- signal: AbortSignal.timeout(timeoutMs),
175
- });
176
- if (!response.ok) {
177
- throw new Error(`MiniMax API error (${response.status}): ${await response.text()}`);
178
+ // One deadline covers all attempts and backoff. Retry only explicit failed responses:
179
+ // transport errors leave generation outcome ambiguous and must not silently rebill it.
180
+ const signal = AbortSignal.timeout(timeoutMs);
181
+ let result: MinimaxResponse;
182
+ for (let attempt = 1; ; attempt += 1) {
183
+ signal.throwIfAborted();
184
+ const response = await fetchFn(url, {
185
+ method: 'POST',
186
+ headers: { 'content-type': 'application/json', authorization: `Bearer ${apiKey}` },
187
+ body: JSON.stringify(body),
188
+ signal,
189
+ });
190
+ let error: Error | null;
191
+ if (response.ok) {
192
+ result = (await response.json()) as MinimaxResponse;
193
+ error = responseError(result);
194
+ if (error === null) break;
195
+ } else {
196
+ const detail = await response.text();
197
+ let businessError: Error | null = null;
198
+ try {
199
+ businessError = responseError(JSON.parse(detail) as MinimaxResponse);
200
+ } catch {
201
+ // A non-JSON gateway response still carries its HTTP failure class.
202
+ }
203
+ // Business codes own policy/auth semantics even on a generic HTTP 5xx.
204
+ error = businessError ?? new Error(`MiniMax API error (${response.status}): ${detail}`);
205
+ }
206
+ if (attempt >= 3 || classifyProviderError(error) !== 'transient') throw error;
207
+ const delayMs = attempt * 1000;
208
+ echoError(`WARN image-gen: ${error.message}; retrying MiniMax (${attempt + 1}/3) after ${delayMs}ms`);
209
+ await delay(delayMs, undefined, { signal });
178
210
  }
179
- const bytes = await extractImageFromResponse(
180
- (await response.json()) as MinimaxResponse,
181
- fetchFn,
182
- timeoutMs,
183
- );
211
+ // Download errors are outside the POST retry loop: the image already exists.
212
+ const bytes = await extractImageFromResponse(result, fetchFn, timeoutMs);
184
213
  return { bytes, extension: 'jpg', provider: 'minimax', model: req.model };
185
214
  },
186
215
  };
@@ -392,7 +392,7 @@ export async function main(depsOverride?: Partial<SearchDeps>): Promise<number>
392
392
  ...depsOverride,
393
393
  };
394
394
  // Bun.which with an explicit PATH — without the option it can ignore runtime
395
- // process.env.PATH changes (observed on Bun 1.3.14).
395
+ // process.env.PATH changes (observed on Bun 1.3.14, still true on 1.4.3).
396
396
  if (!deps.markitdown && !Bun.which('markitdown', { PATH: deps.env.PATH })) {
397
397
  // R6 — live mode requires the normalizer; fail before any network call.
398
398
  throw new Error('markitdown is not on PATH');
@@ -28,6 +28,17 @@ Capability files **omit a leading `kk-`**. `superskill install` prefixes the plu
28
28
  `storm-research`, `itc-generating`, `explain-things` (via `/tell-me`), and `/workflow-run`. Product workflow YAML (`kk-storm-research.yaml`)
29
29
  keeps its existing name — that is a workflow stem, not an installable agent capability.
30
30
 
31
+ ### Grok Bot
32
+
33
+ Install: `superskill install kk --targets grok-bot --bot-provides cli:kk --bot-provides local-fs`.
34
+ Add `--bot-provides cli:spur` when the Bot ran the spur bootstrap.
35
+
36
+ Each skill and command declares a `requires:` list (`hooks`, `subagents`, `local-fs`, `cli:<name>`)
37
+ so the install ships only entries the Bot can run; the guard test is
38
+ `plugins/kk/tests/runtime-requires.test.ts`. Which kk work may actually run in the cloud is decided
39
+ by ADR-044 (`docs/00_ADR.md`): cloud Grok Bot routines are store-free and browser-free —
40
+ anything touching `kk.db` or a browser profile stays Mac-side.
41
+
31
42
  ## Schedule
32
43
 
33
44
  `spur serve` **hosts the scheduler**: the `.spur/config.yaml` `bootstrap.scheduler` jobs (the daily
@@ -59,6 +70,13 @@ launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/ai.gobing.kk-collect.pli
59
70
  crontab -l > /tmp/kk-collect.cron 2>/dev/null || :
60
71
  kk schedule render kk-collect --platform cron --repo "$PWD" >> /tmp/kk-collect.cron
61
72
  crontab /tmp/kk-collect.cron
73
+
74
+ # Grok Bot cloud routine (ADR-044): prints a routine definition for the store-free kk-collect
75
+ # prefix — the Bot owns preparation, the Mac owns the kk store. Paste the printed text into the
76
+ # Bot's routine editor (creating the routine there is the operator action; kk never calls an API).
77
+ # kk-serve and the publish3 windows are refused on grok-bot: they are store- or browser-bound.
78
+ kk schedule render kk-collect --platform grok-bot --bot kk-cloud --tz "$TZ" \
79
+ | pbcopy # then paste into the Bot's routine editor
62
80
  ```
63
81
 
64
82
  Both rendered artifacts set `PATH` themselves (`{{home}}/.bun/bin`, `/opt/homebrew/bin`,
@@ -3,6 +3,7 @@ name: image-extract
3
3
  description: Extract a reusable style preset (and optionally draft a format preset) from 1-5 reference images, saved to ~/.config/kk/image-presets/ after confirmation.
4
4
  argument-hint: "--name <style-name> --ref <img> [--ref <img>…] [--emit-format <name> --job free|cover|illustration] (max 5 refs)"
5
5
  allowed-tools: ["Skill"]
6
+ requires: [cli:kk, cli:spur, local-fs]
6
7
  ---
7
8
 
8
9
  # image-extract
@@ -3,6 +3,7 @@ name: image-generate
3
3
  description: Generate images from a prompt or an article — covers, illustrations, or free-form — using named format and style presets.
4
4
  argument-hint: "[prompt] [--format free|cover|illustration] [--style vibrant|minimalist|photorealistic|technical-diagram|editorial|fresh|warm|cute|sketch|clean-webapp-ui|<custom>] [--content <file>] [--ref <img>…] [--var K=V] [--dry-run] [--list-presets]"
5
5
  allowed-tools: ["Skill"]
6
+ requires: [cli:kk, cli:spur, local-fs]
6
7
  ---
7
8
 
8
9
  # image-generate
@@ -3,6 +3,7 @@ name: tell-me
3
3
  description: Explain the supplied or current topic with the smallest useful view; use ELI5 only when requested.
4
4
  argument-hint: "[topic] [--eli5]"
5
5
  allowed-tools: ["Skill"]
6
+ requires: [local-fs]
6
7
  ---
7
8
 
8
9
  # tell-me
@@ -7,6 +7,7 @@ description: >-
7
7
  creates the config with defaults if missing; computes the workspace; shells
8
8
  spur workflow run against the resolved YAML.
9
9
  argument-hint: "[name] <topic|--in file> [--dir <path>] [--playbook generic|english|wechat] [--research] [--judge] [--outline <a|b|c>] [--writer itc-generating|topic] [--duration <min>] [--language <code>] [--script-approved] [--fixture] [--force]"
10
+ requires: [cli:kk, cli:spur, local-fs]
10
11
  ---
11
12
 
12
13
  Thin runner around the **storm-research**, **itc-generating** / **topic**, and **kk-solo-podcast**
@@ -223,12 +224,17 @@ stays `false` unless `--live` is passed explicitly (ADR-022). `--playbook wechat
223
224
  `cover_enabled` / `inline_images` (`false` unless passed); `--max-revisions` binds
224
225
  `max_revisions` (default `2`). Each lands in `itc-options.json` under its camelCase key.
225
226
 
227
+ Two var-only child options skip the options snapshot: `outline_bullets_only=true` keeps the
228
+ outline stage to heading-plus-bullets options (no prose paragraphs), and `plan_only=true`
229
+ stops the parent DAG after the confirmed publish plan (no `send-confirm` pause, no fanout-send,
230
+ no recovery, no settle) — a plan preview, not a send.
231
+
226
232
  **Collected topic (`--from-collect <profile>`, 0299).** `itc-prepare` without `--chosen` returns
227
233
  an *intake* snapshot (`status: intake`, `workDir: null`). Choose the topic in a run-scoped intake
228
234
  dir, then finalize:
229
235
 
230
236
  1. `kk stage run itc-topic-extract --request` `{parentRunId, intakeDir, topicProfile}` → the pool.
231
- 2. The agent writes `$intakeDir/topic-candidates.json` (≤3 candidates `a|b|c` with
237
+ 2. The agent (or `kk itc candidates`) writes `$intakeDir/topic-candidates.json` (≤5 candidates `a|b|c` up to `d|e` with
232
238
  `title`/`reason`/`itemIds` from the pool); `kk stage run itc-topic-validate --request`
233
239
  `{parentRunId, intakeDir}` must pass.
234
240
  3. `kk stage run itc-decision --topic "$intakeDir" "$RUN_ID"` prints the chosen id or `defer`.
@@ -0,0 +1,24 @@
1
+ # it-topics topic profile — general IT article pool for kk-itc collected topics.
2
+ #
3
+ # Modeled on ai-news.yaml (task 0210 R1): the pool for `kk itc candidates --profile it-topics`
4
+ # is every labelled item with a substantive body, most time-sensitive first — not AI-scoped,
5
+ # so category is NOT gated and the pool admits the whole `kk-item-category` taxonomy
6
+ # (ai-* and non-ai alike: general IT/tech articles draw from both).
7
+ #
8
+ # Keys are decision ids from the sibling catalog (plugins/kk/decisions/kk-items.yaml);
9
+ # an unknown key or decision id fails loud with `kk.store.profile.invalid`.
10
+ # `limit` is omitted: the compiler's episode-cap default (20) applies unless
11
+ # `kk itc candidates --limit` overrides it.
12
+ topic: it-topics
13
+ where:
14
+ # quality/importance 2, not 3: real ingest bodies (last30days/aihot summaries) are stub-length,
15
+ # so the heuristic labeler tops out at 1-2 and a gte-3 gate empties real pools (task 0212
16
+ # dogfood finding). Pool selection stays lenient here; any stricter cutoff belongs downstream.
17
+ kk-item-quality: { gte: 2 }
18
+ # importance stays in as its own gate with the same 2 floor: an unjudged item stays excluded
19
+ # (fail-closed INNER join on `labels_current`).
20
+ kk-item-importance: { gte: 2 }
21
+ # Same conservative moderation gate as ai-news (task 0229 R5): political-system items never
22
+ # become article topics without explicit operator review.
23
+ kk-item-china-politics: { lte: 1 }
24
+ order: [kk-item-urgency desc, kk-item-impact desc, first_seen_at desc]
@@ -13,10 +13,12 @@ decisions:
13
13
  type: string
14
14
  description: Formatted candidate options with title, rationale, item IDs and source URLs
15
15
  criteria:
16
- a: Candidate slot a is the strongest and most cohesive technical article topic.
17
- b: Candidate slot b is the strongest and most cohesive technical article topic.
18
- c: Candidate slot c is the strongest and most cohesive technical article topic.
19
- defer: Candidates are insufficient, contradictory, or need explicit operator review.
16
+ a: "Slot a is the strongest and most cohesive general IT topic for a technical article — infrastructure, languages, tooling, systems, developer practice or industry signal."
17
+ b: "Slot b is the strongest and most cohesive general IT topic for a technical article — infrastructure, languages, tooling, systems, developer practice or industry signal."
18
+ c: "Slot c is the strongest and most cohesive general IT topic for a technical article — infrastructure, languages, tooling, systems, developer practice or industry signal."
19
+ d: "Slot d is the strongest and most cohesive general IT topic for a technical article — infrastructure, languages, tooling, systems, developer practice or industry signal."
20
+ e: "Slot e is the strongest and most cohesive general IT topic for a technical article — infrastructure, languages, tooling, systems, developer practice or industry signal."
21
+ defer: Candidates are insufficient, contradictory, overlap, or need explicit operator review.
20
22
  fallback: defer
21
23
 
22
24
  kk-itc-outline-choice:
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "kk",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "knowledge-kit Claude Code plugin: skills, commands, subagents, hooks, and rules"
5
5
  }