@gobing-ai/knowledge-kit 0.0.13 → 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 +20 -0
- package/package.json +1 -1
- package/plugins/generations/content-gen/dist/index.js +20 -0
- package/plugins/generations/core-facts-gen/dist/index.js +20 -0
- package/plugins/generations/daily-article-gen/dist/index.js +29 -1
- package/plugins/generations/daily-article-gen/src/index.ts +13 -1
- package/plugins/generations/dailynews-gen/dist/index.js +20 -0
- package/plugins/generations/episode-plan-gen/dist/index.js +24 -0
- package/plugins/generations/episode-plan-gen/src/index.ts +11 -0
- package/plugins/generations/image-gen/dist/index.js +2296 -53
- 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 +1 -1
- package/plugins/generations/image-gen/src/providers/types.ts +52 -51
- package/plugins/generations/image-gen/src/providers/zai.ts +237 -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 +30 -2
- 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 +20 -0
- package/plugins/ingestions/horizon-ingest/dist/index.js +20 -0
- package/plugins/ingestions/last30days-ingest/dist/index.js +20 -0
- package/plugins/ingestions/web-search/dist/index.js +20 -0
- package/plugins/kk/commands/image-extract.md +40 -0
- package/plugins/kk/commands/image-generate.md +18 -1
- package/plugins/kk/plugin.json +1 -1
- package/plugins/kk/skills/image-authoring/SKILL.md +10 -3
- package/plugins/kk/skills/image-authoring/references/format-drafting.md +57 -0
- package/plugins/kk/skills/image-authoring/references/style-extraction.md +14 -9
- package/plugins/kk/workflows/kk-daily-ai-voice.yaml +130 -30
- package/plugins/publishings/emdash-pub/dist/index.js +20 -0
- package/plugins/publishings/podcast-pub/dist/index.js +57 -3
- 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 +20 -0
- package/plugins/publishings/surfdash-pub/dist/index.js +96 -6
- package/plugins/publishings/surfdash-pub/src/index.ts +109 -9
- package/plugins/publishings/zenn-pub/dist/index.js +20 -0
- 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,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
|
+
};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { extname } from 'node:path';
|
|
2
|
+
import { readBytes } from '../bytes';
|
|
3
|
+
import { extractImageFromResponse, getOpenAISize, type OpenAIImageResponse } from './openai';
|
|
4
|
+
import type { ImageProvider, ImageRequest, ImageResult, ProviderFactory, ProviderOptions } from './types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Azure OpenAI adapter (0115 R1) — port of
|
|
8
|
+
* vendors/baoyu-skills/skills/baoyu-image-gen/scripts/providers/azure.ts
|
|
9
|
+
* (MIT © 2026 Jim Liu). `CliArgs.size` becomes `ImageRequest.width/height`.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const DEFAULT_AZURE_API_VERSION = '2025-04-01-preview';
|
|
13
|
+
const AZURE_EDIT_IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg']);
|
|
14
|
+
|
|
15
|
+
interface AzureEndpoint {
|
|
16
|
+
resourceBaseURL: string;
|
|
17
|
+
deployment: string | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function parseAzureBaseURL(url: string): AzureEndpoint {
|
|
21
|
+
const parsed = new URL(url);
|
|
22
|
+
const trimmedPath = parsed.pathname.replace(/\/+$/, '');
|
|
23
|
+
const deploymentMatch = trimmedPath.match(/^(.*?)(?:\/openai)?\/deployments\/([^/]+)$/);
|
|
24
|
+
if (deploymentMatch !== null) {
|
|
25
|
+
parsed.pathname = `${deploymentMatch[1] ?? ''}/openai`;
|
|
26
|
+
return {
|
|
27
|
+
resourceBaseURL: parsed.toString().replace(/\/+$/, ''),
|
|
28
|
+
deployment: decodeURIComponent(deploymentMatch[2] as string),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
parsed.pathname = trimmedPath.endsWith('/openai') ? trimmedPath : `${trimmedPath}/openai`;
|
|
32
|
+
return { resourceBaseURL: parsed.toString().replace(/\/+$/, ''), deployment: null };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function azureEndpoint(env: Record<string, string | undefined>): AzureEndpoint {
|
|
36
|
+
const url = env.AZURE_OPENAI_BASE_URL ?? '';
|
|
37
|
+
if (url === '') {
|
|
38
|
+
throw new Error(
|
|
39
|
+
'AZURE_OPENAI_BASE_URL is required. Set it to your Azure resource or deployment endpoint, e.g.: https://your-resource.openai.azure.com or https://your-resource.openai.azure.com/openai/deployments/your-deployment',
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
return parseAzureBaseURL(url);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getApiKey(env: Record<string, string | undefined>): string {
|
|
46
|
+
const key = env.AZURE_OPENAI_API_KEY ?? '';
|
|
47
|
+
if (key === '') {
|
|
48
|
+
throw new Error(
|
|
49
|
+
'AZURE_OPENAI_API_KEY is required. Get it from Azure Portal → your OpenAI resource → Keys and Endpoint.',
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return key;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function buildURL(
|
|
56
|
+
endpoint: AzureEndpoint,
|
|
57
|
+
deployment: string,
|
|
58
|
+
pathSuffix: string,
|
|
59
|
+
env: Record<string, string | undefined>,
|
|
60
|
+
): string {
|
|
61
|
+
const apiVersion = env.AZURE_API_VERSION ?? DEFAULT_AZURE_API_VERSION;
|
|
62
|
+
return `${endpoint.resourceBaseURL}/deployments/${encodeURIComponent(deployment)}${pathSuffix}?api-version=${apiVersion}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getAzureQuality(quality: 'normal' | '2k' | undefined): 'medium' | 'high' {
|
|
66
|
+
return quality === '2k' ? 'high' : 'medium';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Azure reference images must be PNG or JPG/JPEG before the API call is spent. */
|
|
70
|
+
export function validateReferenceImages(referenceImages: readonly string[]): void {
|
|
71
|
+
for (const refPath of referenceImages) {
|
|
72
|
+
if (!AZURE_EDIT_IMAGE_EXTENSIONS.has(extname(refPath).toLowerCase())) {
|
|
73
|
+
throw new Error(`Azure OpenAI reference images must be PNG or JPG/JPEG. Unsupported file: ${refPath}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const createAzure: ProviderFactory = (options?: ProviderOptions): ImageProvider => {
|
|
79
|
+
const fetchFn = options?.fetch ?? fetch;
|
|
80
|
+
const env = options?.env ?? process.env;
|
|
81
|
+
// Azure's own env-precedence for a default model: explicit deployment, then the base URL,
|
|
82
|
+
// then the frozen 'gpt-image-2' (matches the baoyu getDefaultModel chain; spec facts unchanged).
|
|
83
|
+
function defaultModel(): string {
|
|
84
|
+
const explicit = env.AZURE_OPENAI_DEPLOYMENT?.trim();
|
|
85
|
+
if (explicit !== undefined && explicit !== '') return explicit;
|
|
86
|
+
const baseURL = env.AZURE_OPENAI_BASE_URL;
|
|
87
|
+
if (baseURL !== undefined) {
|
|
88
|
+
try {
|
|
89
|
+
const { deployment } = parseAzureBaseURL(baseURL);
|
|
90
|
+
if (deployment !== null) return deployment;
|
|
91
|
+
} catch {
|
|
92
|
+
// A malformed base URL surfaces later through the required-env check.
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return 'gpt-image-2';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const supportsRef = (): boolean => true;
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
name: 'azure',
|
|
102
|
+
defaultModel,
|
|
103
|
+
isAvailable: () => (env.AZURE_OPENAI_API_KEY ?? '') !== '',
|
|
104
|
+
supportsReferenceImages: supportsRef,
|
|
105
|
+
validate(req: ImageRequest): void {
|
|
106
|
+
validateReferenceImages(req.referenceImages ?? []);
|
|
107
|
+
},
|
|
108
|
+
async generate(req: ImageRequest): Promise<ImageResult> {
|
|
109
|
+
const apiKey = getApiKey(env);
|
|
110
|
+
const endpoint = azureEndpoint(env);
|
|
111
|
+
const deployment = req.model.trim();
|
|
112
|
+
if (deployment === '') {
|
|
113
|
+
throw new Error(
|
|
114
|
+
'Azure deployment name is required. Use --model <deployment>, AZURE_OPENAI_DEPLOYMENT, AZURE_OPENAI_IMAGE_MODEL, or embed the deployment in AZURE_OPENAI_BASE_URL.',
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
const size =
|
|
118
|
+
(req.width !== undefined && req.height !== undefined ? `${req.width}x${req.height}` : undefined) ??
|
|
119
|
+
getOpenAISize(req.model, req.aspectRatio ?? null, req.quality);
|
|
120
|
+
const refs = req.referenceImages ?? [];
|
|
121
|
+
const bytes = await (async (): Promise<Uint8Array> => {
|
|
122
|
+
if (refs.length > 0) {
|
|
123
|
+
const form = new FormData();
|
|
124
|
+
form.append('prompt', req.prompt);
|
|
125
|
+
form.append('size', size);
|
|
126
|
+
form.append('n', '1');
|
|
127
|
+
form.append('quality', getAzureQuality(req.quality));
|
|
128
|
+
for (const refPath of refs) {
|
|
129
|
+
const fileBytes = await readBytes(refPath);
|
|
130
|
+
const filename = refPath.split('/').pop() ?? refPath;
|
|
131
|
+
const mimeType = extname(filename).toLowerCase() === '.png' ? 'image/png' : 'image/jpeg';
|
|
132
|
+
form.append('image[]', new Blob([fileBytes], { type: mimeType }), filename);
|
|
133
|
+
}
|
|
134
|
+
const res = await fetchFn(buildURL(endpoint, deployment, '/images/edits', env), {
|
|
135
|
+
method: 'POST',
|
|
136
|
+
headers: { 'api-key': apiKey },
|
|
137
|
+
body: form,
|
|
138
|
+
});
|
|
139
|
+
if (!res.ok) throw new Error(`Azure OpenAI edits API error: ${await res.text()}`);
|
|
140
|
+
return extractImageFromResponse((await res.json()) as OpenAIImageResponse, fetchFn);
|
|
141
|
+
}
|
|
142
|
+
const res = await fetchFn(buildURL(endpoint, deployment, '/images/generations', env), {
|
|
143
|
+
method: 'POST',
|
|
144
|
+
headers: { 'content-type': 'application/json', 'api-key': apiKey },
|
|
145
|
+
body: JSON.stringify({ prompt: req.prompt, size, n: 1, quality: getAzureQuality(req.quality) }),
|
|
146
|
+
});
|
|
147
|
+
if (!res.ok) throw new Error(`Azure OpenAI API error: ${await res.text()}`);
|
|
148
|
+
return extractImageFromResponse((await res.json()) as OpenAIImageResponse, fetchFn);
|
|
149
|
+
})();
|
|
150
|
+
return { bytes, extension: 'png', provider: 'azure', model: req.model };
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
};
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { randomBytes } from 'node:crypto';
|
|
3
|
+
import { access, mkdir, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { resolve } from 'node:path';
|
|
6
|
+
import { readBytes } from '../bytes';
|
|
7
|
+
import type { ImageProvider, ImageRequest, ImageResult, ProviderFactory, ProviderOptions } from './types';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* no-direct-fs-io / no-direct-process-spawn exemption is recorded as per-file
|
|
11
|
+
* exclusions in .spur/rules/strict/runtime-boundaries.yaml: this provider's
|
|
12
|
+
* transport IS spawning the local `codex` CLI and staging temp files for it
|
|
13
|
+
* (same vendor-CLI-boundary class as the file-cli publish transports).
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* codex-cli adapter (0115 R1) — port of
|
|
17
|
+
* vendors/baoyu-skills/skills/baoyu-image-gen/scripts/providers/codex-cli.ts
|
|
18
|
+
* (MIT © 2026 Jim Liu). Never auto-selected (frozen NEVER_AUTO rule): readiness is
|
|
19
|
+
* the 'codex' CLI login, not an env fact, so isAvailable() stays false.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const DEFAULT_MODEL = 'codex-image-gen';
|
|
23
|
+
|
|
24
|
+
type WrapperOkResult = {
|
|
25
|
+
status: 'ok';
|
|
26
|
+
path?: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type WrapperErrorResult = {
|
|
30
|
+
status: 'error';
|
|
31
|
+
error: string;
|
|
32
|
+
error_kind: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type WrapperResult = WrapperOkResult | WrapperErrorResult;
|
|
36
|
+
|
|
37
|
+
async function exists(filePath: string): Promise<boolean> {
|
|
38
|
+
try {
|
|
39
|
+
await access(filePath);
|
|
40
|
+
return true;
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Wrapper resolution keeps the baoyu env-override channel verbatim, including the
|
|
48
|
+
* inherited variable names (0115 straight port; kk has no bundled codex wrapper,
|
|
49
|
+
* so the override is the only supported location).
|
|
50
|
+
*/
|
|
51
|
+
export async function resolveWrapperPath(readEnv: (key: string) => string): Promise<string> {
|
|
52
|
+
const override = readEnv('BAOYU_CODEX_IMAGEGEN_BIN');
|
|
53
|
+
if (override.length > 0) {
|
|
54
|
+
if (!(await exists(override))) {
|
|
55
|
+
throw new Error(`Invalid BAOYU_CODEX_IMAGEGEN_BIN: ${override} does not exist.`);
|
|
56
|
+
}
|
|
57
|
+
return override;
|
|
58
|
+
}
|
|
59
|
+
throw new Error(
|
|
60
|
+
'codex-cli wrapper not found. Set BAOYU_CODEX_IMAGEGEN_BIN to a codex-imagegen main.ts (or .sh) path.',
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
type SpawnResult = { stdout: string; stderr: string; code: number };
|
|
65
|
+
|
|
66
|
+
function spawnWrapper(wrapperPath: string, cliArgs: string[]): Promise<SpawnResult> {
|
|
67
|
+
return new Promise((resolveSpawn, rejectSpawn) => {
|
|
68
|
+
const isTs = wrapperPath.endsWith('.ts');
|
|
69
|
+
const command = isTs ? 'bun' : wrapperPath;
|
|
70
|
+
const args = isTs ? [wrapperPath, ...cliArgs] : cliArgs;
|
|
71
|
+
const child = spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
72
|
+
let stdout = '';
|
|
73
|
+
let stderr = '';
|
|
74
|
+
child.stdout.on('data', (chunk: Buffer) => {
|
|
75
|
+
stdout += chunk.toString('utf8');
|
|
76
|
+
});
|
|
77
|
+
child.stderr.on('data', (chunk: Buffer) => {
|
|
78
|
+
const text = chunk.toString('utf8');
|
|
79
|
+
stderr += text;
|
|
80
|
+
});
|
|
81
|
+
child.on('error', (err) => rejectSpawn(err));
|
|
82
|
+
child.on('close', (code) => resolveSpawn({ stdout, stderr, code: code ?? 1 }));
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function parseWrapperJson(stdout: string): WrapperResult {
|
|
87
|
+
const trimmed = stdout.trim();
|
|
88
|
+
if (trimmed === '') throw new Error('Invalid codex-cli response: empty stdout from wrapper.');
|
|
89
|
+
const lastLine = trimmed.split(/\r?\n/).pop() ?? trimmed;
|
|
90
|
+
try {
|
|
91
|
+
return JSON.parse(lastLine) as WrapperResult;
|
|
92
|
+
} catch (parseErr: unknown) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`Invalid codex-cli response: could not parse JSON from wrapper stdout (${parseErr instanceof Error ? parseErr.message : String(parseErr)}).`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function parsePositiveInt(value: string): number | null {
|
|
100
|
+
if (value === '') return null;
|
|
101
|
+
const parsed = Number.parseInt(value, 10);
|
|
102
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export const createCodexCli: ProviderFactory = (options?: ProviderOptions): ImageProvider => {
|
|
106
|
+
const env = options?.env ?? process.env;
|
|
107
|
+
const readEnv = (key: string): string => env[key] ?? '';
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
name: 'codex-cli',
|
|
111
|
+
defaultModel: () => DEFAULT_MODEL,
|
|
112
|
+
isAvailable: () => false,
|
|
113
|
+
supportsReferenceImages: () => true,
|
|
114
|
+
validate(req: ImageRequest): void {
|
|
115
|
+
if ((req.n ?? 1) > 1) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
'codex-cli provider supports only n=1 (Codex image_gen returns a single image per call).',
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
async generate(req: ImageRequest): Promise<ImageResult> {
|
|
122
|
+
const wrapperPath = await resolveWrapperPath(readEnv);
|
|
123
|
+
|
|
124
|
+
const sessionDir = resolve(tmpdir(), 'kk-image-gen-codex-cli');
|
|
125
|
+
await mkdir(sessionDir, { recursive: true });
|
|
126
|
+
const token = randomBytes(8).toString('hex');
|
|
127
|
+
const tmpOutput = resolve(sessionDir, `out-${token}.png`);
|
|
128
|
+
const tmpPrompt = resolve(sessionDir, `prompt-${token}.md`);
|
|
129
|
+
await writeFile(tmpPrompt, req.prompt, 'utf8');
|
|
130
|
+
|
|
131
|
+
const cliArgs: string[] = [
|
|
132
|
+
'--image',
|
|
133
|
+
tmpOutput,
|
|
134
|
+
'--prompt-file',
|
|
135
|
+
tmpPrompt,
|
|
136
|
+
'--aspect',
|
|
137
|
+
req.aspectRatio ?? '1:1',
|
|
138
|
+
];
|
|
139
|
+
for (const ref of req.referenceImages ?? []) {
|
|
140
|
+
cliArgs.push('--ref', resolve(ref));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const cacheDir = readEnv('BAOYU_CODEX_IMAGEGEN_CACHE_DIR');
|
|
144
|
+
if (cacheDir !== '') cliArgs.push('--cache-dir', cacheDir);
|
|
145
|
+
const timeoutMs = parsePositiveInt(readEnv('BAOYU_CODEX_IMAGEGEN_TIMEOUT_MS'));
|
|
146
|
+
if (timeoutMs !== null) cliArgs.push('--timeout', String(timeoutMs));
|
|
147
|
+
const retries = parsePositiveInt(readEnv('BAOYU_CODEX_IMAGEGEN_RETRIES'));
|
|
148
|
+
if (retries !== null) cliArgs.push('--retries', String(retries));
|
|
149
|
+
const logFile = readEnv('BAOYU_CODEX_IMAGEGEN_LOG_FILE');
|
|
150
|
+
if (logFile !== '') cliArgs.push('--log-file', logFile);
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
const spawnResult = await spawnWrapper(wrapperPath, cliArgs);
|
|
154
|
+
const parsed = parseWrapperJson(spawnResult.stdout);
|
|
155
|
+
if (parsed.status === 'error') {
|
|
156
|
+
throw new Error(`Invalid codex-cli result (${parsed.error_kind}): ${parsed.error}`);
|
|
157
|
+
}
|
|
158
|
+
if (spawnResult.code !== 0) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
`Invalid codex-cli result: wrapper exited with code ${spawnResult.code} despite reporting status=ok.`,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
const bytes = await readBytes(parsed.path ?? tmpOutput);
|
|
164
|
+
return { bytes, extension: 'png', provider: 'codex-cli', model: req.model };
|
|
165
|
+
} finally {
|
|
166
|
+
await Promise.allSettled([rm(tmpOutput, { force: true }), rm(tmpPrompt, { force: true })]);
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
};
|