@gobing-ai/knowledge-kit 0.0.12 → 0.0.14
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.
- package/.claude-plugin/marketplace.json +1 -1
- package/dist/index.js +120 -26
- package/package.json +1 -1
- package/plugins/generations/content-gen/dist/index.js +22187 -0
- package/plugins/generations/content-gen/plugin.json +1 -1
- package/plugins/generations/core-facts-gen/dist/index.js +22068 -0
- package/plugins/generations/core-facts-gen/plugin.json +1 -1
- package/plugins/generations/daily-article-gen/dist/index.js +22050 -0
- package/plugins/generations/daily-article-gen/plugin.json +1 -1
- package/plugins/generations/daily-article-gen/src/index.ts +13 -1
- package/plugins/generations/dailynews-gen/dist/index.js +22344 -0
- package/plugins/generations/dailynews-gen/plugin.json +1 -1
- package/plugins/generations/episode-plan-gen/dist/index.js +22503 -0
- package/plugins/generations/episode-plan-gen/plugin.json +1 -1
- package/plugins/generations/episode-plan-gen/src/index.ts +11 -0
- package/plugins/generations/image-gen/config.example.yaml +75 -0
- package/plugins/generations/image-gen/dist/index.js +24862 -0
- package/plugins/generations/image-gen/package.json +17 -0
- package/plugins/generations/image-gen/plugin.json +7 -0
- package/plugins/generations/image-gen/presets/formats/cover.yaml +57 -0
- package/plugins/generations/image-gen/presets/formats/free.yaml +46 -0
- package/plugins/generations/image-gen/presets/formats/illustration.yaml +48 -0
- package/plugins/generations/image-gen/presets/styles/clean-webapp-ui.yaml +28 -0
- package/plugins/generations/image-gen/presets/styles/cute.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/editorial.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/fresh.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/minimalist.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/photorealistic.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/sketch.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/technical-diagram.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/vibrant.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/warm.yaml +3 -0
- package/plugins/generations/image-gen/src/bytes.ts +19 -0
- package/plugins/generations/image-gen/src/index.ts +319 -0
- package/plugins/generations/image-gen/src/job.ts +143 -0
- package/plugins/generations/image-gen/src/paths.ts +31 -0
- package/plugins/generations/image-gen/src/presets.ts +344 -0
- package/plugins/generations/image-gen/src/providers/agnes.ts +110 -0
- package/plugins/generations/image-gen/src/providers/azure.ts +153 -0
- package/plugins/generations/image-gen/src/providers/codex-cli.ts +170 -0
- package/plugins/generations/image-gen/src/providers/dashscope.ts +485 -0
- package/plugins/generations/image-gen/src/providers/google.ts +268 -0
- package/plugins/generations/image-gen/src/providers/huggingface.ts +59 -0
- package/plugins/generations/image-gen/src/providers/jimeng.ts +259 -0
- package/plugins/generations/image-gen/src/providers/minimax.ts +171 -0
- package/plugins/generations/image-gen/src/providers/openai.ts +319 -0
- package/plugins/generations/image-gen/src/providers/openrouter.ts +257 -0
- package/plugins/generations/image-gen/src/providers/refs.ts +24 -0
- package/plugins/generations/image-gen/src/providers/replicate.ts +279 -0
- package/plugins/generations/image-gen/src/providers/seedream.ts +128 -0
- package/plugins/generations/image-gen/src/providers/types.ts +286 -0
- package/plugins/generations/image-gen/src/providers/zai.ts +237 -0
- package/plugins/generations/image-gen/tsconfig.json +8 -0
- package/plugins/generations/news-report-gen/dist/index.js +22193 -0
- package/plugins/generations/news-report-gen/package.json +17 -0
- package/plugins/generations/news-report-gen/plugin.json +7 -0
- package/plugins/generations/news-report-gen/src/index.ts +308 -0
- package/plugins/generations/news-report-gen/tsconfig.json +4 -0
- package/plugins/generations/omni-voice-gen/Makefile +14 -0
- package/plugins/generations/omni-voice-gen/README.md +112 -0
- package/plugins/generations/omni-voice-gen/bin/omni-voice-gen +2 -0
- package/plugins/generations/omni-voice-gen/dist/omni-voice-gen-prr8skpb. +2 -0
- package/plugins/generations/omni-voice-gen/dist/omni-voice-gen.js +6 -0
- package/plugins/generations/omni-voice-gen/plugin.json +6 -0
- package/plugins/generations/omni-voice-gen/profiles.json +12 -0
- package/plugins/generations/omni-voice-gen/pyproject.toml +25 -0
- package/plugins/generations/omni-voice-gen/scripts/coverage_gate.py +74 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/__init__.py +1 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/__main__.py +39 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/audio.py +190 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/backend.py +150 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/contract.py +76 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/mp3.py +60 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/pipeline.py +289 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/profiles.py +100 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/qc.py +234 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/voicescript.py +352 -0
- package/plugins/generations/omni-voice-gen/uv.lock +3510 -0
- package/plugins/generations/voice-gen/dist/index.js +23055 -0
- package/plugins/generations/voice-gen/plugin.json +1 -1
- package/plugins/generations/voice-gen/src/index.ts +16 -1
- package/plugins/generations/voice-gen/src/voicebox-client.ts +3 -1
- package/plugins/ingestions/aihot-ingest/dist/index.js +22378 -0
- package/plugins/ingestions/aihot-ingest/plugin.json +1 -1
- package/plugins/ingestions/horizon-ingest/dist/index.js +22125 -0
- package/plugins/ingestions/horizon-ingest/plugin.json +1 -1
- package/plugins/ingestions/karakeep-local/dist/index.js +24204 -0
- package/plugins/ingestions/karakeep-local/plugin.json +1 -1
- package/plugins/ingestions/last30days-ingest/dist/index.js +22070 -0
- package/plugins/ingestions/last30days-ingest/plugin.json +1 -1
- package/plugins/ingestions/web-search/dist/index.js +24399 -0
- package/plugins/ingestions/web-search/plugin.json +1 -1
- package/plugins/kk/commands/image-extract.md +40 -0
- package/plugins/kk/commands/image-generate.md +32 -0
- package/plugins/kk/config.example.yaml +80 -0
- package/plugins/kk/plugin.json +1 -1
- package/plugins/kk/skills/image-authoring/SKILL.md +257 -0
- package/plugins/kk/skills/image-authoring/references/format-drafting.md +57 -0
- package/plugins/kk/skills/image-authoring/references/illustration-positions.md +87 -0
- package/plugins/kk/skills/image-authoring/references/migrating-from-wt.md +31 -0
- package/plugins/kk/skills/image-authoring/references/providers.md +52 -0
- package/plugins/kk/skills/image-authoring/references/style-extraction.md +139 -0
- package/plugins/kk/workflows/kk-daily-ai-voice.yaml +130 -30
- package/plugins/publishings/emdash-pub/dist/index.js +22263 -0
- package/plugins/publishings/emdash-pub/plugin.json +1 -1
- package/plugins/publishings/podcast-pub/dist/index.js +22650 -0
- package/plugins/publishings/podcast-pub/plugin.json +8 -2
- package/plugins/publishings/podcast-pub/src/index.ts +18 -2
- package/plugins/publishings/podcast-pub/src/show-notes.ts +56 -9
- package/plugins/publishings/qiita-pub/dist/index.js +22101 -0
- package/plugins/publishings/qiita-pub/plugin.json +1 -1
- package/plugins/publishings/surfdash-pub/dist/index.js +22323 -0
- package/plugins/publishings/surfdash-pub/plugin.json +1 -1
- package/plugins/publishings/surfdash-pub/src/index.ts +109 -9
- package/plugins/publishings/zenn-pub/dist/index.js +22142 -0
- package/plugins/publishings/zenn-pub/plugin.json +1 -1
- package/plugins/sp/scripts/batch-preflight.mjs +346 -0
- package/plugins/sp/scripts/batch-preflight.ts +459 -0
- package/plugins/sp/scripts/daily-summary/daily-summary.mjs +615 -0
- package/plugins/sp/scripts/daily-summary/daily-summary.ts +846 -0
- package/plugins/sp/scripts/daily-summary/logger.ts +28 -0
- package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +223 -0
- package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +367 -0
- package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +132 -0
- package/plugins/sp/scripts/dogfood-testing/validate-report.ts +169 -0
- package/plugins/sp/scripts/feature-dev-precheck.mjs +171 -0
- package/plugins/sp/scripts/feature-dev-precheck.ts +238 -0
- package/plugins/sp/scripts/feature-sync-bounded.mjs +285 -0
- package/plugins/sp/scripts/feature-sync-bounded.ts +478 -0
- package/plugins/sp/scripts/history-anatomy-cache.mjs +902 -0
- package/plugins/sp/scripts/history-anatomy-cache.ts +1028 -0
- package/plugins/sp/scripts/idea-handoff.mjs +22 -0
- package/plugins/sp/scripts/idea-handoff.ts +44 -0
- package/plugins/sp/scripts/inline-pipeline-parity-check.ts +185 -0
- package/plugins/sp/scripts/inline-run-setup.ts +198 -0
- package/plugins/sp/scripts/pr-reviewing.mjs +769 -0
- package/plugins/sp/scripts/pr-reviewing.ts +925 -0
- package/plugins/sp/scripts/quality-gate.mjs +179 -0
- package/plugins/sp/scripts/quality-gate.ts +217 -0
- package/plugins/sp/scripts/script-contract-check.ts +319 -0
- package/plugins/sp/scripts/stage-registry-adapter.ts +1533 -0
- package/plugins/sp/scripts/surface-drift-inventory.ts +929 -0
- package/plugins/sp/scripts/task-evidence-precheck.ts +181 -0
- package/plugins/sp/scripts/task-size-precheck.ts +175 -0
- package/plugins/sp/scripts/transition-shim-check.ts +238 -0
- package/plugins/sp/scripts/validate-commands.ts +689 -0
- package/plugins/sp/scripts/validate-flag-contracts.ts +878 -0
- package/plugins/sp/scripts/verify-answer-lint.ts +530 -0
- package/plugins/sp/scripts/workflow-step-profile.mjs +316 -0
- package/plugins/sp/scripts/workflow-step-profile.ts +456 -0
- package/plugins/sp/scripts/wrapup-steps.mjs +373 -0
- package/plugins/sp/scripts/wrapup-steps.ts +466 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type { Doc } from '@gobing-ai/kk-core';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Task 0104 — the frozen `--in` envelope. A plugin's only input channel is the
|
|
6
|
+
* `--in` JSON (the spawn argv is frozen at `--in`/`--out`,
|
|
7
|
+
* `apps/cli/src/invoke.ts:118-123`), so exactly one job spec rides on the
|
|
8
|
+
* first document's `metadata.imageGen`, and the array itself is the content.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** The three v1 job branches; a resolved format preset's `job` field must match. */
|
|
12
|
+
export const IMAGE_JOBS = ['free', 'cover', 'illustration'] as const;
|
|
13
|
+
export type ImageJob = (typeof IMAGE_JOBS)[number];
|
|
14
|
+
|
|
15
|
+
/** Job spec parsed from `docs[0].metadata.imageGen` (frozen 0104 Design). */
|
|
16
|
+
export interface ImageJobSpec {
|
|
17
|
+
job: ImageJob;
|
|
18
|
+
/** FormatPreset name (0102); default per job is the job's own preset name. */
|
|
19
|
+
format?: string;
|
|
20
|
+
/** StylePreset name; default = format.defaultStyle. */
|
|
21
|
+
style?: string;
|
|
22
|
+
/** Template variables (0102 composePrompt); caller values win over preset defaults. */
|
|
23
|
+
vars?: Record<string, string>;
|
|
24
|
+
/** Free text appended as composePrompt's 4th segment. */
|
|
25
|
+
extra?: string;
|
|
26
|
+
/** 0103 selectProvider's `requested` name. */
|
|
27
|
+
provider?: string;
|
|
28
|
+
model?: string;
|
|
29
|
+
/** Absolute paths (0105 pass-through). */
|
|
30
|
+
referenceImages?: string[];
|
|
31
|
+
/** Illustration only: images to produce; default 1. */
|
|
32
|
+
count?: number;
|
|
33
|
+
/** Budget cap (R6); wins over KK_IMAGE_MAX_IMAGES. */
|
|
34
|
+
maxImages?: number;
|
|
35
|
+
seed?: number;
|
|
36
|
+
/** Overrides the `--out` basename for artefact names. */
|
|
37
|
+
outputStem?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Strict on purpose: the envelope is frozen and 0106 writes it — a typo'd key must fail loud, not vanish. */
|
|
41
|
+
export const ImageJobSpecSchema = z.strictObject({
|
|
42
|
+
job: z.enum(IMAGE_JOBS),
|
|
43
|
+
format: z.string().optional(),
|
|
44
|
+
style: z.string().optional(),
|
|
45
|
+
vars: z.record(z.string(), z.string()).optional(),
|
|
46
|
+
extra: z.string().optional(),
|
|
47
|
+
provider: z.string().optional(),
|
|
48
|
+
model: z.string().optional(),
|
|
49
|
+
referenceImages: z.array(z.string()).optional(),
|
|
50
|
+
count: z.number().int().positive().optional(),
|
|
51
|
+
maxImages: z.number().int().positive().optional(),
|
|
52
|
+
seed: z.number().int().optional(),
|
|
53
|
+
outputStem: z.string().optional(),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
/** One generated image inside `Content.metadata.images` (frozen 0104 Design). */
|
|
57
|
+
export interface ImageArtifact {
|
|
58
|
+
/** Absolute; always a sibling of `--out`. */
|
|
59
|
+
path: string;
|
|
60
|
+
/** FormatPreset name. */
|
|
61
|
+
format: string;
|
|
62
|
+
/** StylePreset name. */
|
|
63
|
+
style: string;
|
|
64
|
+
provider: string;
|
|
65
|
+
model: string;
|
|
66
|
+
/** The exact prompt string sent. */
|
|
67
|
+
prompt: string;
|
|
68
|
+
seed?: number;
|
|
69
|
+
width?: number;
|
|
70
|
+
height?: number;
|
|
71
|
+
bytes: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** One illustration caption, the wt captions.json payload reduced to what consumers read (R4). */
|
|
75
|
+
export interface CaptionEntry {
|
|
76
|
+
index: number;
|
|
77
|
+
path: string;
|
|
78
|
+
caption: string;
|
|
79
|
+
altText: string;
|
|
80
|
+
anchor: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Typed shape of this plugin's `Content.metadata` (frozen keys, 0104 Design). */
|
|
84
|
+
export interface ImageGenMetadata {
|
|
85
|
+
generator: 'kk:image-gen';
|
|
86
|
+
job: ImageJob;
|
|
87
|
+
images: ImageArtifact[];
|
|
88
|
+
/** Illustration/cover: the input doc's id — the original article is never mutated. */
|
|
89
|
+
sourceDocId?: string;
|
|
90
|
+
/** Illustration only: the captions payload. */
|
|
91
|
+
captions?: CaptionEntry[];
|
|
92
|
+
/** Set only when the run spent nothing (R6 dry run). */
|
|
93
|
+
dryRun?: true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function jobList(): string {
|
|
97
|
+
return IMAGE_JOBS.map((job) => `'${job}'`).join(', ');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function zodIssues(error: z.ZodError): string {
|
|
101
|
+
return error.issues.map((issue) => `${issue.path.join('.') || 'spec'}: ${issue.message}`).join('; ');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Parse the job spec from `docs[0].metadata.imageGen`; fail loud, naming the
|
|
106
|
+
* frozen path and the valid `job` values, before any preset or provider work.
|
|
107
|
+
*/
|
|
108
|
+
export function parseJobSpec(docs: Doc[]): ImageJobSpec {
|
|
109
|
+
const raw = docs[0]?.metadata?.imageGen;
|
|
110
|
+
if (raw === undefined || raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
111
|
+
throw new Error(
|
|
112
|
+
`Invalid image job spec at metadata.imageGen: expected an object with a "job" of ${jobList()}, received ${
|
|
113
|
+
raw === undefined ? 'nothing' : JSON.stringify(raw)
|
|
114
|
+
}`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
const result = ImageJobSpecSchema.safeParse(raw);
|
|
118
|
+
if (!result.success) {
|
|
119
|
+
throw new Error(`Invalid image job spec at metadata.imageGen: ${zodIssues(result.error)}`);
|
|
120
|
+
}
|
|
121
|
+
return result.data;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const PositionsSchema = z.array(z.string());
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Read `docs[0].metadata.positions` — the caller's (0106) chosen insertion
|
|
128
|
+
* anchors for the illustration job. Shape-only here; the anchor-occurs-once
|
|
129
|
+
* check against the body belongs to the illustration branch.
|
|
130
|
+
*/
|
|
131
|
+
export function readPositions(docs: Doc[]): string[] | undefined {
|
|
132
|
+
const raw = docs[0]?.metadata?.positions;
|
|
133
|
+
if (raw === undefined) {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
const result = PositionsSchema.safeParse(raw);
|
|
137
|
+
if (!result.success) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
`Invalid docs[0].metadata.positions: expected an array of string anchors: ${zodIssues(result.error)}`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
return result.data;
|
|
143
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { basename, dirname, extname, resolve } from 'node:path';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Task 0104 R3 — artefacts are siblings of `--out` (the voice-gen rule): the
|
|
5
|
+
* invoker expands `works_dir` before building `--out`, and a plugin's cwd is
|
|
6
|
+
* its own package dir, so every path resolves against `--out`'s directory —
|
|
7
|
+
* never `process.cwd()`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** `ImageJobSpec.outputStem` ?? the `--out` basename without extension. */
|
|
11
|
+
export function outStemFor(outPath: string, override?: string): string {
|
|
12
|
+
return override ?? basename(outPath, extname(outPath));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* `<outDir>/<stem>-<format>-<NN>.<ext>` — NN zero-padded from 01, so a
|
|
17
|
+
* deterministic re-run against the same `--out` overwrites instead of appending.
|
|
18
|
+
*
|
|
19
|
+
* 0107 R5: stem and extension guards keep a hostile spec/provider from escaping
|
|
20
|
+
* `--out`'s directory — this is the single choke point for all artefact paths.
|
|
21
|
+
*/
|
|
22
|
+
export function artifactPath(outPath: string, stem: string, format: string, index: number, extension: string): string {
|
|
23
|
+
if (stem.includes('/') || stem.includes('\\')) {
|
|
24
|
+
throw new Error(`Artefact stem must not contain path separators: '${stem}'`);
|
|
25
|
+
}
|
|
26
|
+
if (!/^[a-z0-9]{1,5}$/i.test(extension)) {
|
|
27
|
+
throw new Error(`Artefact extension must be a short alphanumeric segment: '${extension}'`);
|
|
28
|
+
}
|
|
29
|
+
const nn = String(index).padStart(2, '0');
|
|
30
|
+
return resolve(dirname(outPath), `${stem}-${format}-${nn}.${extension}`);
|
|
31
|
+
}
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { createNodeFileSystem } from '@gobing-ai/ts-runtime';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Image preset contract (task 0102): the format × style preset schemas, the
|
|
8
|
+
* frozen prompt-composition rule (R2), and multi-root resolution with
|
|
9
|
+
* shadowing (R4), mirroring ADR-012's plugin discovery
|
|
10
|
+
* (`docs/design/multi-root-plugin-discovery.md`).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** One colour entry in a structured (baoyu-style) palette. */
|
|
14
|
+
export interface PaletteEntry {
|
|
15
|
+
role: string;
|
|
16
|
+
name: string;
|
|
17
|
+
hex: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Provenance of a style preset; 0105 populates it after extraction. */
|
|
21
|
+
export interface PresetSource {
|
|
22
|
+
kind: 'authored' | 'extracted';
|
|
23
|
+
refImages?: string[];
|
|
24
|
+
extractedAt?: string;
|
|
25
|
+
confidence?: 'high' | 'medium' | 'low';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** One declared template variable: caller-facing description plus default. */
|
|
29
|
+
export interface FormatVariable {
|
|
30
|
+
description: string;
|
|
31
|
+
default: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** R1 — `formats/<name>.yaml`: the job skeleton (template, keywords, variables). */
|
|
35
|
+
export interface FormatPreset {
|
|
36
|
+
/** Filename stem must equal this. */
|
|
37
|
+
name: string;
|
|
38
|
+
description: string;
|
|
39
|
+
/** Which plugin job branch this format drives (0104). */
|
|
40
|
+
job: 'free' | 'cover' | 'illustration';
|
|
41
|
+
/** Portable primary dimension; width/height are the optional exact pixels. */
|
|
42
|
+
aspectRatio: string;
|
|
43
|
+
width?: number;
|
|
44
|
+
height?: number;
|
|
45
|
+
/** Style used when the caller names none. */
|
|
46
|
+
defaultStyle: string;
|
|
47
|
+
/** Quality/composition tail, joined with ', '. */
|
|
48
|
+
keywords: string[];
|
|
49
|
+
/** Same placeholder grammar as the template, e.g. '{{title | cover}}.png'. */
|
|
50
|
+
outputFilename: string;
|
|
51
|
+
variables: Record<string, FormatVariable>;
|
|
52
|
+
/** Prompt body; YAML block scalar, embedded newlines preserved. */
|
|
53
|
+
template: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** R1 — `styles/<name>.yaml`; `modifiers` is the only required prompt field. */
|
|
57
|
+
export interface StylePreset {
|
|
58
|
+
name: string;
|
|
59
|
+
description: string;
|
|
60
|
+
modifiers: string;
|
|
61
|
+
palette?: PaletteEntry[];
|
|
62
|
+
typography?: string;
|
|
63
|
+
visualElements?: string[];
|
|
64
|
+
bestFor?: string;
|
|
65
|
+
/** Provider negative prompt when supported, else appended as 'avoid: …'. */
|
|
66
|
+
negative?: string;
|
|
67
|
+
source?: PresetSource;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const PaletteEntrySchema = z.object({ role: z.string(), name: z.string(), hex: z.string() });
|
|
71
|
+
|
|
72
|
+
const PresetSourceSchema = z.object({
|
|
73
|
+
kind: z.enum(['authored', 'extracted']),
|
|
74
|
+
refImages: z.array(z.string()).optional(),
|
|
75
|
+
extractedAt: z.string().optional(),
|
|
76
|
+
confidence: z.enum(['high', 'medium', 'low']).optional(),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
/** Parse gate for `formats/<name>.yaml`. */
|
|
80
|
+
export const FormatPresetSchema = z.object({
|
|
81
|
+
name: z.string(),
|
|
82
|
+
description: z.string(),
|
|
83
|
+
job: z.enum(['free', 'cover', 'illustration']),
|
|
84
|
+
aspectRatio: z.string(),
|
|
85
|
+
width: z.number().optional(),
|
|
86
|
+
height: z.number().optional(),
|
|
87
|
+
defaultStyle: z.string(),
|
|
88
|
+
keywords: z.array(z.string()),
|
|
89
|
+
outputFilename: z.string(),
|
|
90
|
+
variables: z.record(z.string(), z.object({ description: z.string(), default: z.string() })),
|
|
91
|
+
template: z.string(),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
/** Parse gate for `styles/<name>.yaml`. */
|
|
95
|
+
export const StylePresetSchema = z.object({
|
|
96
|
+
name: z.string(),
|
|
97
|
+
description: z.string(),
|
|
98
|
+
modifiers: z.string(),
|
|
99
|
+
palette: z.array(PaletteEntrySchema).optional(),
|
|
100
|
+
typography: z.string().optional(),
|
|
101
|
+
visualElements: z.array(z.string()).optional(),
|
|
102
|
+
bestFor: z.string().optional(),
|
|
103
|
+
negative: z.string().optional(),
|
|
104
|
+
source: PresetSourceSchema.optional(),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Merge caller vars over the format's declared defaults; the caller wins.
|
|
109
|
+
*/
|
|
110
|
+
export function mergeVars(
|
|
111
|
+
declared: Record<string, FormatVariable>,
|
|
112
|
+
vars: Record<string, string>,
|
|
113
|
+
): Record<string, string> {
|
|
114
|
+
const merged: Record<string, string> = {};
|
|
115
|
+
for (const [name, def] of Object.entries(declared)) {
|
|
116
|
+
merged[name] = def.default;
|
|
117
|
+
}
|
|
118
|
+
for (const [name, value] of Object.entries(vars)) {
|
|
119
|
+
merged[name] = value;
|
|
120
|
+
}
|
|
121
|
+
return merged;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Match one `{{ … }}` expression with brace-depth counting, so a fallback
|
|
126
|
+
* that itself contains a placeholder survives whole. wt's `[^}]+` regex
|
|
127
|
+
* truncates at the first inner `}` (`{{subtitle | , subtitled
|
|
128
|
+
* "{{subtitle}}"}}` lost its tail); the R6 golden prompt pins that form as
|
|
129
|
+
* rendering correctly, so the port must balance the braces.
|
|
130
|
+
*/
|
|
131
|
+
function matchPlaceholder(template: string, start: number): { expr: string; end: number } | undefined {
|
|
132
|
+
let depth = 1;
|
|
133
|
+
for (let i = start + 2; i < template.length; i++) {
|
|
134
|
+
if (template.startsWith('{{', i)) {
|
|
135
|
+
depth++;
|
|
136
|
+
i++;
|
|
137
|
+
} else if (template.startsWith('}}', i)) {
|
|
138
|
+
depth--;
|
|
139
|
+
if (depth === 0) {
|
|
140
|
+
return { expr: template.slice(start + 2, i), end: i + 2 };
|
|
141
|
+
}
|
|
142
|
+
i++;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Resolve one expression against the merged vars. An empty value (including
|
|
150
|
+
* a caller-passed '') resolves to the fallback; an unknown placeholder with
|
|
151
|
+
* no fallback throws, naming the placeholder and the format — wt silently
|
|
152
|
+
* left it in place, and a literal `{{title}}` must not reach a paid provider.
|
|
153
|
+
* Leading trim on the fallback skips the `| ` separator; its trailing space
|
|
154
|
+
* is preserved (the R6 golden prompt pins it). Single pass: a fallback is
|
|
155
|
+
* emitted literally and never re-scanned, like wt's `re.sub`.
|
|
156
|
+
*/
|
|
157
|
+
function resolvePlaceholder(expr: string, vars: Record<string, string>, formatName: string): string {
|
|
158
|
+
const pipe = expr.indexOf('|');
|
|
159
|
+
if (pipe !== -1) {
|
|
160
|
+
const name = expr.slice(0, pipe).trim();
|
|
161
|
+
const fallback = expr.slice(pipe + 1).trimStart();
|
|
162
|
+
const value = vars[name];
|
|
163
|
+
if (!value) {
|
|
164
|
+
return fallback;
|
|
165
|
+
}
|
|
166
|
+
// A fallback that embeds its own '{{name}}' slot is a mini-template
|
|
167
|
+
// wrapping the value (the R6 golden's ', subtitled "…"' form); a
|
|
168
|
+
// plain-text fallback is just the default, so the value wins.
|
|
169
|
+
return fallback.includes(`{{${name}}}`) ? fallback.split(`{{${name}}}`).join(value) : value;
|
|
170
|
+
}
|
|
171
|
+
const name = expr.trim();
|
|
172
|
+
const value = vars[name];
|
|
173
|
+
if (value === undefined) {
|
|
174
|
+
throw new Error(`Format "${formatName}": placeholder {{${name}}} has no value and no fallback`);
|
|
175
|
+
}
|
|
176
|
+
return value;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Render `{{name}}` / `{{name | fallback}}` (wt's two-form grammar,
|
|
181
|
+
* `template_engine.py` `_substitute_variables`).
|
|
182
|
+
*/
|
|
183
|
+
export function renderTemplate(template: string, vars: Record<string, string>, formatName: string): string {
|
|
184
|
+
let rendered = '';
|
|
185
|
+
let cursor = 0;
|
|
186
|
+
while (cursor < template.length) {
|
|
187
|
+
const start = template.indexOf('{{', cursor);
|
|
188
|
+
if (start === -1) {
|
|
189
|
+
rendered += template.slice(cursor);
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
const match = matchPlaceholder(template, start);
|
|
193
|
+
if (match === undefined) {
|
|
194
|
+
rendered += template.slice(cursor, start + 2);
|
|
195
|
+
cursor = start + 2;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
rendered += template.slice(cursor, start);
|
|
199
|
+
rendered += resolvePlaceholder(match.expr, vars, formatName);
|
|
200
|
+
cursor = match.end;
|
|
201
|
+
}
|
|
202
|
+
return rendered;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* R2 — the frozen composition rule, wt's order as built
|
|
207
|
+
* (`template_engine.py:174-190`): rendered body → keywords → style modifiers
|
|
208
|
+
* → caller extra, empty segments dropped. Keeping wt's order makes the ported
|
|
209
|
+
* presets render byte-identically to their wt originals; the extra segment is
|
|
210
|
+
* the only addition (0105's ref-derived traits land there).
|
|
211
|
+
*/
|
|
212
|
+
export function composePrompt(
|
|
213
|
+
format: FormatPreset,
|
|
214
|
+
style: StylePreset,
|
|
215
|
+
vars: Record<string, string>,
|
|
216
|
+
extra?: string,
|
|
217
|
+
): string {
|
|
218
|
+
const body = renderTemplate(format.template, mergeVars(format.variables, vars), format.name);
|
|
219
|
+
return [body, format.keywords.join(', '), style.modifiers, extra].filter(Boolean).join(', ');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Discovery diagnostic, mirroring kk-core's `DiscoveryDiagnostic` shape. */
|
|
223
|
+
export interface PresetDiagnostic {
|
|
224
|
+
code: 'shadowed' | 'invalid';
|
|
225
|
+
path: string;
|
|
226
|
+
reason: string;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export interface LoadedPresets {
|
|
230
|
+
formats: Map<string, FormatPreset>;
|
|
231
|
+
styles: Map<string, StylePreset>;
|
|
232
|
+
diagnostics: PresetDiagnostic[];
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// File IO goes through the ts-runtime FileSystem seam (rule no-direct-fs-io). The
|
|
236
|
+
// node backend uses sync fs APIs by default (file-system-node.d.ts); the interface
|
|
237
|
+
// unions Promise for CF stubs, so pin the sync shape once here.
|
|
238
|
+
const fs = createNodeFileSystem() as unknown as {
|
|
239
|
+
exists(path: string): boolean;
|
|
240
|
+
readFile(path: string): string;
|
|
241
|
+
readDir(path: string): string[];
|
|
242
|
+
stat(path: string): { isDirectory(): boolean } | null;
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
function isDirectory(path: string): boolean {
|
|
246
|
+
return fs.stat(path)?.isDirectory() ?? false;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* R4 — preset roots, low → high, mirroring ADR-012's plugin roots
|
|
251
|
+
* (`docs/design/multi-root-plugin-discovery.md`): builtin → user → project →
|
|
252
|
+
* each `KK_IMAGE_PRESET_PATH` entry (colon-separated, left → right). The
|
|
253
|
+
* project root resolves against KNOWLEDGE_KIT_PROJECT_ROOT, not cwd, because
|
|
254
|
+
* plugins are spawned with cwd set to their own package dir
|
|
255
|
+
* (`apps/cli/src/invoke.ts`).
|
|
256
|
+
*/
|
|
257
|
+
export function resolvePresetRoots(): string[] {
|
|
258
|
+
const roots: string[] = [join(import.meta.dir, '..', 'presets')];
|
|
259
|
+
// process.env.HOME directly: Bun snapshots os.homedir() at startup, so a
|
|
260
|
+
// runtime HOME override (tests) would be invisible through homedir().
|
|
261
|
+
const userRoot = join(process.env.HOME ?? homedir(), '.config', 'kk', 'image-presets');
|
|
262
|
+
if (isDirectory(userRoot)) {
|
|
263
|
+
roots.push(userRoot);
|
|
264
|
+
}
|
|
265
|
+
const projectRoot = join(process.env.KNOWLEDGE_KIT_PROJECT_ROOT ?? process.cwd(), '.kk', 'image-presets');
|
|
266
|
+
if (isDirectory(projectRoot)) {
|
|
267
|
+
roots.push(projectRoot);
|
|
268
|
+
}
|
|
269
|
+
for (const entry of process.env.KK_IMAGE_PRESET_PATH?.split(':') ?? []) {
|
|
270
|
+
if (entry !== '' && isDirectory(entry)) {
|
|
271
|
+
roots.push(entry);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return roots;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Minimal structural shape of a Zod schema, so the loader stays generic. */
|
|
278
|
+
interface SchemaLike<T extends { name: string }> {
|
|
279
|
+
safeParse(value: unknown): { success: true; data: T } | { success: false; error: Error };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Load one axis (`formats/` or `styles/`) from one root. A file that fails its
|
|
284
|
+
* schema parse (or YAML parse) is skipped with an `invalid` diagnostic, never
|
|
285
|
+
* fatal; a name already loaded from a lower root is replaced wholesale with a
|
|
286
|
+
* `shadowed` diagnostic naming the losing file — no field merging, because a
|
|
287
|
+
* half-overridden preset cannot be reproduced from its file alone.
|
|
288
|
+
*/
|
|
289
|
+
function loadAxis<T extends { name: string }>(
|
|
290
|
+
dir: string,
|
|
291
|
+
byName: Map<string, T>,
|
|
292
|
+
pathsByName: Map<string, string>,
|
|
293
|
+
schema: SchemaLike<T>,
|
|
294
|
+
diagnostics: PresetDiagnostic[],
|
|
295
|
+
): void {
|
|
296
|
+
if (!isDirectory(dir)) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
for (const entry of fs.readDir(dir).sort()) {
|
|
300
|
+
if (!entry.endsWith('.yaml') && !entry.endsWith('.yml')) {
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
const path = join(dir, entry);
|
|
304
|
+
let parsed: unknown;
|
|
305
|
+
try {
|
|
306
|
+
parsed = Bun.YAML.parse(fs.readFile(path));
|
|
307
|
+
} catch (error) {
|
|
308
|
+
diagnostics.push({ code: 'invalid', path, reason: `unreadable or unparseable: ${error}` });
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
const result = schema.safeParse(parsed);
|
|
312
|
+
if (!result.success) {
|
|
313
|
+
diagnostics.push({ code: 'invalid', path, reason: result.error.message });
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
const previousPath = pathsByName.get(result.data.name);
|
|
317
|
+
if (previousPath !== undefined) {
|
|
318
|
+
diagnostics.push({
|
|
319
|
+
code: 'shadowed',
|
|
320
|
+
path: previousPath,
|
|
321
|
+
reason: `Preset "${result.data.name}" shadowed by ${path}`,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
byName.set(result.data.name, result.data);
|
|
325
|
+
pathsByName.set(result.data.name, path);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* R4 — load formats and styles across all resolved roots; a later root
|
|
331
|
+
* shadows an earlier one by preset name within each axis.
|
|
332
|
+
*/
|
|
333
|
+
export function loadPresets(): LoadedPresets {
|
|
334
|
+
const diagnostics: PresetDiagnostic[] = [];
|
|
335
|
+
const formats = new Map<string, FormatPreset>();
|
|
336
|
+
const styles = new Map<string, StylePreset>();
|
|
337
|
+
const formatPaths = new Map<string, string>();
|
|
338
|
+
const stylePaths = new Map<string, string>();
|
|
339
|
+
for (const root of resolvePresetRoots()) {
|
|
340
|
+
loadAxis(join(root, 'formats'), formats, formatPaths, FormatPresetSchema, diagnostics);
|
|
341
|
+
loadAxis(join(root, 'styles'), styles, stylePaths, StylePresetSchema, diagnostics);
|
|
342
|
+
}
|
|
343
|
+
return { formats, styles, diagnostics };
|
|
344
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
import { refToDataUri } from './refs';
|
|
3
|
+
import { extensionFromUrl } from './seedream';
|
|
4
|
+
import type { ImageProvider, ImageRequest, ImageResult, ProviderFactory, ProviderOptions } from './types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Agnes adapter (0115 R1) — port of
|
|
8
|
+
* vendors/baoyu-skills/skills/baoyu-image-gen/scripts/providers/agnes.ts
|
|
9
|
+
* (MIT © 2026 Jim Liu). `CliArgs.size` becomes `ImageRequest.width/height`.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const DEFAULT_MODEL = 'agnes-image-2.1-flash';
|
|
13
|
+
const DEFAULT_BASE_URL = 'https://apihub.agnes-ai.com/v1';
|
|
14
|
+
const DEFAULT_SIZE = '1024x1024';
|
|
15
|
+
|
|
16
|
+
interface AgnesResponse {
|
|
17
|
+
created?: number;
|
|
18
|
+
data: Array<{ url?: string; b64_json?: string }>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function parseAspectRatio(ar: string): { width: number; height: number } | null {
|
|
22
|
+
const match = ar.match(/^(\d+(?:\.\d+)?):(\d+(?:\.\d+)?)$/);
|
|
23
|
+
if (match === null) return null;
|
|
24
|
+
const w = Number.parseFloat(match[1] as string);
|
|
25
|
+
const h = Number.parseFloat(match[2] as string);
|
|
26
|
+
if (w <= 0 || h <= 0) return null;
|
|
27
|
+
return { width: w, height: h };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function snapDim(n: number): number {
|
|
31
|
+
return Math.max(32, Math.round(n / 32) * 32);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type SizeArgs = Pick<ImageRequest, 'aspectRatio' | 'width' | 'height'>;
|
|
35
|
+
|
|
36
|
+
export function resolveSize(args: SizeArgs): string {
|
|
37
|
+
if (args.width !== undefined && args.height !== undefined) return `${args.width}x${args.height}`;
|
|
38
|
+
if (args.aspectRatio !== undefined) {
|
|
39
|
+
const parsed = parseAspectRatio(args.aspectRatio);
|
|
40
|
+
if (parsed !== null) {
|
|
41
|
+
if (parsed.width === 1 && parsed.height === 1) return '1024x1024';
|
|
42
|
+
const maxEdge = 2048;
|
|
43
|
+
const scale = Math.max(1, Math.floor(maxEdge / Math.max(parsed.width, parsed.height)));
|
|
44
|
+
return `${snapDim(parsed.width * scale)}x${snapDim(parsed.height * scale)}`;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return DEFAULT_SIZE;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function buildRequestBody(
|
|
51
|
+
prompt: string,
|
|
52
|
+
model: string,
|
|
53
|
+
args: SizeArgs & { referenceImages: string[] },
|
|
54
|
+
): Record<string, unknown> {
|
|
55
|
+
const body: Record<string, unknown> = { model, prompt, size: resolveSize(args) };
|
|
56
|
+
if (args.referenceImages.length > 0) body.image = args.referenceImages;
|
|
57
|
+
body.extra_body = { response_format: 'url' };
|
|
58
|
+
return body;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function extractImageFromResponse(
|
|
62
|
+
result: AgnesResponse,
|
|
63
|
+
fetchFn: typeof fetch = fetch,
|
|
64
|
+
): Promise<Uint8Array> {
|
|
65
|
+
const img = result.data[0];
|
|
66
|
+
if (img?.b64_json !== undefined && img.b64_json !== '') {
|
|
67
|
+
return Uint8Array.from(Buffer.from(img.b64_json, 'base64'));
|
|
68
|
+
}
|
|
69
|
+
if (img?.url !== undefined) {
|
|
70
|
+
const imgRes = await fetchFn(img.url);
|
|
71
|
+
if (!imgRes.ok) throw new Error(`Failed to download image from Agnes: ${imgRes.status}`);
|
|
72
|
+
return new Uint8Array(await imgRes.arrayBuffer());
|
|
73
|
+
}
|
|
74
|
+
throw new Error('No image in Agnes response');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const createAgnes: ProviderFactory = (options?: ProviderOptions): ImageProvider => {
|
|
78
|
+
const fetchFn = options?.fetch ?? fetch;
|
|
79
|
+
const env = options?.env ?? process.env;
|
|
80
|
+
const readEnv = (key: string): string => env[key] ?? '';
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
name: 'agnes',
|
|
84
|
+
defaultModel: () => DEFAULT_MODEL,
|
|
85
|
+
isAvailable: () => readEnv('AGNES_API_KEY') !== '',
|
|
86
|
+
supportsReferenceImages: () => true,
|
|
87
|
+
async generate(req: ImageRequest): Promise<ImageResult> {
|
|
88
|
+
const apiKey = readEnv('AGNES_API_KEY');
|
|
89
|
+
if (apiKey === '') throw new Error('AGNES_API_KEY is required. Get one from https://apihub.agnes-ai.com.');
|
|
90
|
+
const referenceImages = await Promise.all((req.referenceImages ?? []).map(refToDataUri));
|
|
91
|
+
const body = buildRequestBody(req.prompt, req.model, { ...req, referenceImages });
|
|
92
|
+
const base = (readEnv('AGNES_BASE_URL') || DEFAULT_BASE_URL).replace(/\/+$/, '');
|
|
93
|
+
const res = await fetchFn(`${base}/images/generations`, {
|
|
94
|
+
method: 'POST',
|
|
95
|
+
headers: { 'content-type': 'application/json', authorization: `Bearer ${apiKey}` },
|
|
96
|
+
body: JSON.stringify(body),
|
|
97
|
+
});
|
|
98
|
+
if (!res.ok) throw new Error(`Agnes API error (${res.status}): ${await res.text()}`);
|
|
99
|
+
const result = (await res.json()) as AgnesResponse;
|
|
100
|
+
const bytes = await extractImageFromResponse(result, fetchFn);
|
|
101
|
+
const url = result.data[0]?.url;
|
|
102
|
+
return {
|
|
103
|
+
bytes,
|
|
104
|
+
extension: url === undefined || url.startsWith('data:') ? 'png' : extensionFromUrl(url),
|
|
105
|
+
provider: 'agnes',
|
|
106
|
+
model: req.model,
|
|
107
|
+
};
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
};
|