@oh-my-pi/pi-coding-agent 13.18.0 → 14.0.2
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/CHANGELOG.md +316 -1
- package/package.json +86 -24
- package/scripts/format-prompts.ts +2 -2
- package/src/autoresearch/apply-contract-to-state.ts +24 -0
- package/src/autoresearch/contract.ts +0 -44
- package/src/autoresearch/dashboard.ts +1 -2
- package/src/autoresearch/git.ts +116 -30
- package/src/autoresearch/helpers.ts +49 -0
- package/src/autoresearch/index.ts +28 -187
- package/src/autoresearch/prompt.md +26 -9
- package/src/autoresearch/state.ts +0 -6
- package/src/autoresearch/tools/init-experiment.ts +202 -117
- package/src/autoresearch/tools/log-experiment.ts +123 -178
- package/src/autoresearch/tools/run-experiment.ts +48 -10
- package/src/autoresearch/types.ts +2 -2
- package/src/capability/index.ts +4 -2
- package/src/cli/file-processor.ts +3 -3
- package/src/cli/grep-cli.ts +8 -8
- package/src/cli/grievances-cli.ts +78 -0
- package/src/cli/read-cli.ts +67 -0
- package/src/cli/setup-cli.ts +4 -4
- package/src/cli/update-cli.ts +3 -3
- package/src/cli.ts +2 -0
- package/src/commands/grep.ts +6 -1
- package/src/commands/grievances.ts +20 -0
- package/src/commands/read.ts +33 -0
- package/src/commit/agentic/agent.ts +5 -8
- package/src/commit/agentic/index.ts +22 -26
- package/src/commit/agentic/tools/analyze-file.ts +3 -3
- package/src/commit/agentic/tools/git-file-diff.ts +3 -6
- package/src/commit/agentic/tools/git-hunk.ts +3 -3
- package/src/commit/agentic/tools/git-overview.ts +6 -9
- package/src/commit/agentic/tools/index.ts +6 -8
- package/src/commit/agentic/tools/propose-commit.ts +4 -7
- package/src/commit/agentic/tools/recent-commits.ts +3 -3
- package/src/commit/agentic/tools/split-commit.ts +4 -4
- package/src/commit/agentic/validation.ts +1 -1
- package/src/commit/analysis/conventional.ts +4 -4
- package/src/commit/analysis/summary.ts +3 -3
- package/src/commit/changelog/generate.ts +4 -4
- package/src/commit/changelog/index.ts +5 -9
- package/src/commit/map-reduce/map-phase.ts +4 -4
- package/src/commit/map-reduce/reduce-phase.ts +4 -4
- package/src/commit/pipeline.ts +13 -16
- package/src/config/keybindings.ts +7 -6
- package/src/config/prompt-templates.ts +44 -226
- package/src/config/resolve-config-value.ts +4 -2
- package/src/config/settings-schema.ts +98 -2
- package/src/config/settings.ts +25 -26
- package/src/dap/client.ts +674 -0
- package/src/dap/config.ts +150 -0
- package/src/dap/defaults.json +211 -0
- package/src/dap/index.ts +4 -0
- package/src/dap/session.ts +1255 -0
- package/src/dap/types.ts +600 -0
- package/src/debug/log-viewer.ts +3 -2
- package/src/discovery/builtin.ts +1 -2
- package/src/discovery/codex.ts +2 -2
- package/src/discovery/github.ts +2 -1
- package/src/discovery/helpers.ts +2 -2
- package/src/discovery/opencode.ts +2 -2
- package/src/edit/diff.ts +818 -0
- package/src/edit/index.ts +309 -0
- package/src/edit/line-hash.ts +67 -0
- package/src/edit/modes/chunk.ts +454 -0
- package/src/{patch → edit/modes}/hashline.ts +741 -361
- package/src/{patch/applicator.ts → edit/modes/patch.ts} +420 -117
- package/src/{patch/fuzzy.ts → edit/modes/replace.ts} +519 -197
- package/src/{patch → edit}/normalize.ts +97 -76
- package/src/{patch/shared.ts → edit/renderer.ts} +181 -108
- package/src/exec/bash-executor.ts +4 -2
- package/src/exec/idle-timeout-watchdog.ts +126 -0
- package/src/exec/non-interactive-env.ts +5 -0
- package/src/extensibility/custom-commands/bundled/ci-green/index.ts +6 -18
- package/src/extensibility/custom-commands/bundled/review/index.ts +45 -43
- package/src/extensibility/custom-commands/loader.ts +1 -2
- package/src/extensibility/custom-tools/loader.ts +34 -11
- package/src/extensibility/custom-tools/types.ts +1 -1
- package/src/extensibility/extensions/loader.ts +9 -4
- package/src/extensibility/extensions/runner.ts +24 -1
- package/src/extensibility/extensions/types.ts +4 -2
- package/src/extensibility/hooks/loader.ts +5 -6
- package/src/extensibility/hooks/types.ts +2 -2
- package/src/extensibility/plugins/doctor.ts +2 -1
- package/src/extensibility/plugins/marketplace/fetcher.ts +2 -57
- package/src/extensibility/plugins/marketplace/source-resolver.ts +4 -4
- package/src/extensibility/slash-commands.ts +3 -7
- package/src/index.ts +3 -1
- package/src/internal-urls/docs-index.generated.ts +11 -11
- package/src/ipy/executor.ts +58 -17
- package/src/ipy/gateway-coordinator.ts +6 -4
- package/src/ipy/kernel.ts +45 -22
- package/src/ipy/runtime.ts +2 -2
- package/src/lsp/client.ts +7 -4
- package/src/lsp/clients/lsp-linter-client.ts +4 -4
- package/src/lsp/config.ts +2 -2
- package/src/lsp/defaults.json +688 -154
- package/src/lsp/index.ts +234 -45
- package/src/lsp/lspmux.ts +2 -2
- package/src/lsp/startup-events.ts +13 -0
- package/src/lsp/types.ts +12 -1
- package/src/lsp/utils.ts +8 -1
- package/src/main.ts +125 -47
- package/src/memories/index.ts +4 -5
- package/src/modes/acp/acp-agent.ts +563 -163
- package/src/modes/acp/acp-event-mapper.ts +9 -1
- package/src/modes/acp/acp-mode.ts +4 -2
- package/src/modes/components/agent-dashboard.ts +3 -4
- package/src/modes/components/diff.ts +6 -7
- package/src/modes/components/footer.ts +9 -29
- package/src/modes/components/hook-editor.ts +3 -3
- package/src/modes/components/hook-selector.ts +6 -1
- package/src/modes/components/read-tool-group.ts +6 -12
- package/src/modes/components/session-observer-overlay.ts +472 -0
- package/src/modes/components/settings-defs.ts +24 -0
- package/src/modes/components/status-line.ts +15 -61
- package/src/modes/components/tool-execution.ts +1 -1
- package/src/modes/components/welcome.ts +1 -1
- package/src/modes/controllers/btw-controller.ts +2 -2
- package/src/modes/controllers/command-controller.ts +4 -2
- package/src/modes/controllers/event-controller.ts +59 -2
- package/src/modes/controllers/extension-ui-controller.ts +1 -0
- package/src/modes/controllers/input-controller.ts +15 -8
- package/src/modes/controllers/selector-controller.ts +26 -0
- package/src/modes/index.ts +20 -2
- package/src/modes/interactive-mode.ts +278 -69
- package/src/modes/rpc/host-tools.ts +186 -0
- package/src/modes/rpc/rpc-client.ts +178 -13
- package/src/modes/rpc/rpc-mode.ts +73 -3
- package/src/modes/rpc/rpc-types.ts +53 -1
- package/src/modes/session-observer-registry.ts +146 -0
- package/src/modes/shared.ts +0 -42
- package/src/modes/theme/theme.ts +80 -8
- package/src/modes/types.ts +4 -2
- package/src/modes/utils/keybinding-matchers.ts +9 -0
- package/src/prompts/system/custom-system-prompt.md +5 -0
- package/src/prompts/system/system-prompt.md +8 -1
- package/src/prompts/tools/chunk-edit.md +219 -0
- package/src/prompts/tools/debug.md +43 -0
- package/src/prompts/tools/grep.md +3 -0
- package/src/prompts/tools/lsp.md +5 -5
- package/src/prompts/tools/read-chunk.md +17 -0
- package/src/prompts/tools/read.md +19 -5
- package/src/sdk.ts +216 -165
- package/src/secrets/index.ts +1 -1
- package/src/secrets/obfuscator.ts +25 -17
- package/src/session/agent-session.ts +381 -286
- package/src/session/agent-storage.ts +12 -12
- package/src/session/compaction/branch-summarization.ts +3 -3
- package/src/session/compaction/compaction.ts +5 -6
- package/src/session/compaction/utils.ts +3 -3
- package/src/session/history-storage.ts +62 -19
- package/src/session/messages.ts +3 -3
- package/src/session/session-dump-format.ts +203 -0
- package/src/session/session-manager.ts +15 -5
- package/src/session/session-storage.ts +4 -2
- package/src/session/streaming-output.ts +1 -1
- package/src/session/tool-choice-queue.ts +213 -0
- package/src/slash-commands/builtin-registry.ts +56 -8
- package/src/ssh/connection-manager.ts +2 -2
- package/src/ssh/sshfs-mount.ts +5 -5
- package/src/stt/downloader.ts +4 -4
- package/src/stt/recorder.ts +4 -4
- package/src/stt/transcriber.ts +2 -2
- package/src/system-prompt.ts +25 -13
- package/src/task/agents.ts +5 -6
- package/src/task/commands.ts +2 -5
- package/src/task/executor.ts +32 -4
- package/src/task/index.ts +91 -82
- package/src/task/template.ts +2 -2
- package/src/task/types.ts +25 -0
- package/src/task/worktree.ts +131 -149
- package/src/tools/ask.ts +2 -3
- package/src/tools/ast-edit.ts +7 -7
- package/src/tools/ast-grep.ts +7 -7
- package/src/tools/auto-generated-guard.ts +36 -41
- package/src/tools/await-tool.ts +2 -2
- package/src/tools/bash.ts +5 -23
- package/src/tools/browser.ts +4 -5
- package/src/tools/calculator.ts +2 -3
- package/src/tools/cancel-job.ts +2 -2
- package/src/tools/checkpoint.ts +3 -3
- package/src/tools/debug.ts +1007 -0
- package/src/tools/exit-plan-mode.ts +3 -3
- package/src/tools/fetch.ts +67 -3
- package/src/tools/find.ts +4 -5
- package/src/tools/fs-cache-invalidation.ts +5 -0
- package/src/tools/gemini-image.ts +13 -5
- package/src/tools/gh.ts +130 -308
- package/src/tools/grep.ts +57 -9
- package/src/tools/index.ts +44 -22
- package/src/tools/inspect-image.ts +4 -4
- package/src/tools/output-meta.ts +1 -1
- package/src/tools/python.ts +19 -6
- package/src/tools/read.ts +211 -146
- package/src/tools/render-mermaid.ts +2 -3
- package/src/tools/render-utils.ts +20 -6
- package/src/tools/renderers.ts +3 -1
- package/src/tools/report-tool-issue.ts +80 -0
- package/src/tools/resolve.ts +70 -39
- package/src/tools/search-tool-bm25.ts +2 -2
- package/src/tools/ssh.ts +2 -2
- package/src/tools/todo-write.ts +2 -2
- package/src/tools/tool-timeouts.ts +1 -0
- package/src/tools/write.ts +5 -6
- package/src/tui/tree-list.ts +3 -1
- package/src/utils/clipboard.ts +80 -0
- package/src/utils/commit-message-generator.ts +2 -3
- package/src/utils/edit-mode.ts +49 -0
- package/src/utils/external-editor.ts +11 -5
- package/src/utils/file-display-mode.ts +6 -5
- package/src/utils/file-mentions.ts +8 -7
- package/src/utils/git.ts +1400 -0
- package/src/utils/image-loading.ts +98 -0
- package/src/utils/title-generator.ts +2 -3
- package/src/utils/tools-manager.ts +6 -6
- package/src/web/scrapers/choosealicense.ts +1 -1
- package/src/web/search/index.ts +3 -3
- package/src/web/search/render.ts +6 -4
- package/src/autoresearch/command-initialize.md +0 -34
- package/src/commit/git/errors.ts +0 -9
- package/src/commit/git/index.ts +0 -210
- package/src/commit/git/operations.ts +0 -54
- package/src/patch/diff.ts +0 -433
- package/src/patch/index.ts +0 -888
- package/src/patch/parser.ts +0 -532
- package/src/patch/types.ts +0 -292
- package/src/prompts/agents/oracle.md +0 -77
- package/src/tools/gh-cli.ts +0 -125
- package/src/tools/pending-action.ts +0 -49
- package/src/utils/child-process.ts +0 -88
- package/src/utils/frontmatter.ts +0 -117
- package/src/utils/image-input.ts +0 -274
- package/src/utils/mime.ts +0 -53
- package/src/utils/prompt-format.ts +0 -170
package/src/utils/frontmatter.ts
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import { logger, truncate } from "@oh-my-pi/pi-utils";
|
|
2
|
-
import { YAML } from "bun";
|
|
3
|
-
|
|
4
|
-
function stripHtmlComments(content: string): string {
|
|
5
|
-
return content.replace(/<!--[\s\S]*?-->/g, "");
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
/** Convert kebab-case to camelCase (e.g. "thinking-level" -> "thinkingLevel") */
|
|
9
|
-
function kebabToCamel(key: string): string {
|
|
10
|
-
return key.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/** Recursively normalize object keys from kebab-case to camelCase */
|
|
14
|
-
function normalizeKeys<T>(obj: T): T {
|
|
15
|
-
if (obj === null || typeof obj !== "object") {
|
|
16
|
-
return obj;
|
|
17
|
-
}
|
|
18
|
-
if (Array.isArray(obj)) {
|
|
19
|
-
return obj.map(normalizeKeys) as T;
|
|
20
|
-
}
|
|
21
|
-
const result: Record<string, unknown> = {};
|
|
22
|
-
for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
|
|
23
|
-
const normalizedKey = kebabToCamel(key);
|
|
24
|
-
result[normalizedKey] = normalizeKeys(value);
|
|
25
|
-
}
|
|
26
|
-
return result as T;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export class FrontmatterError extends Error {
|
|
30
|
-
constructor(
|
|
31
|
-
error: Error,
|
|
32
|
-
readonly source?: unknown,
|
|
33
|
-
) {
|
|
34
|
-
super(`Failed to parse YAML frontmatter (${source}): ${error.message}`, { cause: error });
|
|
35
|
-
this.name = "FrontmatterError";
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
toString(): string {
|
|
39
|
-
// Format the error with stack and detail, including the error message, stack, and source if present
|
|
40
|
-
const details: string[] = [this.message];
|
|
41
|
-
if (this.source !== undefined) {
|
|
42
|
-
details.push(`Source: ${JSON.stringify(this.source)}`);
|
|
43
|
-
}
|
|
44
|
-
if (this.cause && typeof this.cause === "object" && "stack" in this.cause && this.cause.stack) {
|
|
45
|
-
details.push(`Stack:\n${this.cause.stack}`);
|
|
46
|
-
} else if (this.stack) {
|
|
47
|
-
details.push(`Stack:\n${this.stack}`);
|
|
48
|
-
}
|
|
49
|
-
return details.join("\n\n");
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export interface FrontmatterOptions {
|
|
54
|
-
/** Source of the content (alias: source) */
|
|
55
|
-
location?: unknown;
|
|
56
|
-
/** Source of the content (alias for location) */
|
|
57
|
-
source?: unknown;
|
|
58
|
-
/** Fallback frontmatter values */
|
|
59
|
-
fallback?: Record<string, unknown>;
|
|
60
|
-
/** Normalize the content */
|
|
61
|
-
normalize?: boolean;
|
|
62
|
-
/** Level of error handling */
|
|
63
|
-
level?: "off" | "warn" | "fatal";
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Parse YAML frontmatter from markdown content
|
|
68
|
-
* Returns { frontmatter, body } where body has frontmatter stripped
|
|
69
|
-
*/
|
|
70
|
-
export function parseFrontmatter(
|
|
71
|
-
content: string,
|
|
72
|
-
options?: FrontmatterOptions,
|
|
73
|
-
): { frontmatter: Record<string, unknown>; body: string } {
|
|
74
|
-
const { location, source, fallback, normalize = true, level = "warn" } = options ?? {};
|
|
75
|
-
const loc = location ?? source;
|
|
76
|
-
const frontmatter: Record<string, unknown> = { ...fallback };
|
|
77
|
-
|
|
78
|
-
const normalized = normalize ? stripHtmlComments(content.replace(/\r\n/g, "\n").replace(/\r/g, "\n")) : content;
|
|
79
|
-
if (!normalized.startsWith("---")) {
|
|
80
|
-
return { frontmatter, body: normalized };
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const endIndex = normalized.indexOf("\n---", 3);
|
|
84
|
-
if (endIndex === -1) {
|
|
85
|
-
return { frontmatter, body: normalized };
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const metadata = normalized.slice(4, endIndex);
|
|
89
|
-
const body = normalized.slice(endIndex + 4).trim();
|
|
90
|
-
|
|
91
|
-
try {
|
|
92
|
-
// Replace tabs with spaces for YAML compatibility, use failsafe mode for robustness
|
|
93
|
-
const loaded = YAML.parse(metadata.replaceAll("\t", " ")) as Record<string, unknown> | null;
|
|
94
|
-
return { frontmatter: normalizeKeys({ ...frontmatter, ...loaded }), body };
|
|
95
|
-
} catch (error) {
|
|
96
|
-
const err = new FrontmatterError(
|
|
97
|
-
error instanceof Error ? error : new Error(`YAML: ${error}`),
|
|
98
|
-
loc ?? `Inline '${truncate(content, 64)}'`,
|
|
99
|
-
);
|
|
100
|
-
if (level === "warn" || level === "fatal") {
|
|
101
|
-
logger.warn("Failed to parse YAML frontmatter", { err: err.toString() });
|
|
102
|
-
}
|
|
103
|
-
if (level === "fatal") {
|
|
104
|
-
throw err;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Simple YAML parsing - just key: value pairs
|
|
108
|
-
for (const line of metadata.split("\n")) {
|
|
109
|
-
const match = line.match(/^([\w-]+):\s*(.*)$/);
|
|
110
|
-
if (match) {
|
|
111
|
-
frontmatter[match[1]] = match[2].trim();
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return { frontmatter: normalizeKeys(frontmatter) as Record<string, unknown>, body };
|
|
116
|
-
}
|
|
117
|
-
}
|
package/src/utils/image-input.ts
DELETED
|
@@ -1,274 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs/promises";
|
|
2
|
-
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
3
|
-
import { formatBytes } from "@oh-my-pi/pi-utils";
|
|
4
|
-
import { resolveReadPath } from "../tools/path-utils";
|
|
5
|
-
import { convertToPng } from "./image-convert";
|
|
6
|
-
import { formatDimensionNote, resizeImage } from "./image-resize";
|
|
7
|
-
import { detectSupportedImageMimeTypeFromFile } from "./mime";
|
|
8
|
-
|
|
9
|
-
export const MAX_IMAGE_INPUT_BYTES = 20 * 1024 * 1024;
|
|
10
|
-
const MAX_IMAGE_METADATA_HEADER_BYTES = 256 * 1024;
|
|
11
|
-
export const SUPPORTED_INPUT_IMAGE_MIME_TYPES = new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
|
|
12
|
-
export interface ImageMetadata {
|
|
13
|
-
mimeType: string;
|
|
14
|
-
bytes: number;
|
|
15
|
-
width?: number;
|
|
16
|
-
height?: number;
|
|
17
|
-
channels?: number;
|
|
18
|
-
hasAlpha?: boolean;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface LoadedImageInput {
|
|
22
|
-
resolvedPath: string;
|
|
23
|
-
mimeType: string;
|
|
24
|
-
data: string;
|
|
25
|
-
textNote: string;
|
|
26
|
-
dimensionNote?: string;
|
|
27
|
-
bytes: number;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export async function ensureSupportedImageInput(image: ImageContent): Promise<ImageContent | null> {
|
|
31
|
-
if (SUPPORTED_INPUT_IMAGE_MIME_TYPES.has(image.mimeType)) {
|
|
32
|
-
return image;
|
|
33
|
-
}
|
|
34
|
-
const converted = await convertToPng(image.data, image.mimeType);
|
|
35
|
-
return converted ? { type: "image", data: converted.data, mimeType: converted.mimeType } : null;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface ReadImageMetadataOptions {
|
|
39
|
-
path: string;
|
|
40
|
-
cwd: string;
|
|
41
|
-
resolvedPath?: string;
|
|
42
|
-
detectedMimeType?: string;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export interface LoadImageInputOptions extends ReadImageMetadataOptions {
|
|
46
|
-
autoResize: boolean;
|
|
47
|
-
maxBytes?: number;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export class ImageInputTooLargeError extends Error {
|
|
51
|
-
readonly bytes: number;
|
|
52
|
-
readonly maxBytes: number;
|
|
53
|
-
|
|
54
|
-
constructor(bytes: number, maxBytes: number) {
|
|
55
|
-
super(`Image file too large: ${formatBytes(bytes)} exceeds ${formatBytes(maxBytes)} limit.`);
|
|
56
|
-
this.name = "ImageInputTooLargeError";
|
|
57
|
-
this.bytes = bytes;
|
|
58
|
-
this.maxBytes = maxBytes;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
interface ParsedImageHeaderMetadata {
|
|
63
|
-
width?: number;
|
|
64
|
-
height?: number;
|
|
65
|
-
channels?: number;
|
|
66
|
-
hasAlpha?: boolean;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function parsePngMetadata(header: Buffer): ParsedImageHeaderMetadata {
|
|
70
|
-
if (header.length < 26) return {};
|
|
71
|
-
if (
|
|
72
|
-
header[0] !== 0x89 ||
|
|
73
|
-
header[1] !== 0x50 ||
|
|
74
|
-
header[2] !== 0x4e ||
|
|
75
|
-
header[3] !== 0x47 ||
|
|
76
|
-
header[4] !== 0x0d ||
|
|
77
|
-
header[5] !== 0x0a ||
|
|
78
|
-
header[6] !== 0x1a ||
|
|
79
|
-
header[7] !== 0x0a
|
|
80
|
-
) {
|
|
81
|
-
return {};
|
|
82
|
-
}
|
|
83
|
-
if (header.slice(12, 16).toString("ascii") !== "IHDR") return {};
|
|
84
|
-
const width = header.readUInt32BE(16);
|
|
85
|
-
const height = header.readUInt32BE(20);
|
|
86
|
-
const colorType = header[25];
|
|
87
|
-
if (colorType === 0) return { width, height, channels: 1, hasAlpha: false };
|
|
88
|
-
if (colorType === 2) return { width, height, channels: 3, hasAlpha: false };
|
|
89
|
-
if (colorType === 3) return { width, height, channels: 3 };
|
|
90
|
-
if (colorType === 4) return { width, height, channels: 2, hasAlpha: true };
|
|
91
|
-
if (colorType === 6) return { width, height, channels: 4, hasAlpha: true };
|
|
92
|
-
return { width, height };
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function parseJpegMetadata(header: Buffer): ParsedImageHeaderMetadata {
|
|
96
|
-
if (header.length < 4) return {};
|
|
97
|
-
if (header[0] !== 0xff || header[1] !== 0xd8) return {};
|
|
98
|
-
|
|
99
|
-
let offset = 2;
|
|
100
|
-
while (offset + 9 < header.length) {
|
|
101
|
-
if (header[offset] !== 0xff) {
|
|
102
|
-
offset += 1;
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
let markerOffset = offset + 1;
|
|
107
|
-
while (markerOffset < header.length && header[markerOffset] === 0xff) {
|
|
108
|
-
markerOffset += 1;
|
|
109
|
-
}
|
|
110
|
-
if (markerOffset >= header.length) break;
|
|
111
|
-
|
|
112
|
-
const marker = header[markerOffset];
|
|
113
|
-
const segmentOffset = markerOffset + 1;
|
|
114
|
-
|
|
115
|
-
if (marker === 0xd8 || marker === 0xd9 || marker === 0x01) {
|
|
116
|
-
offset = segmentOffset;
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
if (marker >= 0xd0 && marker <= 0xd7) {
|
|
120
|
-
offset = segmentOffset;
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
if (segmentOffset + 1 >= header.length) break;
|
|
124
|
-
|
|
125
|
-
const segmentLength = header.readUInt16BE(segmentOffset);
|
|
126
|
-
if (segmentLength < 2) break;
|
|
127
|
-
|
|
128
|
-
const isStartOfFrame = marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc;
|
|
129
|
-
if (isStartOfFrame) {
|
|
130
|
-
if (segmentOffset + 7 >= header.length) break;
|
|
131
|
-
const height = header.readUInt16BE(segmentOffset + 3);
|
|
132
|
-
const width = header.readUInt16BE(segmentOffset + 5);
|
|
133
|
-
const channels = header[segmentOffset + 7];
|
|
134
|
-
return {
|
|
135
|
-
width,
|
|
136
|
-
height,
|
|
137
|
-
channels: Number.isFinite(channels) ? channels : undefined,
|
|
138
|
-
hasAlpha: false,
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
offset = segmentOffset + segmentLength;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return {};
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function parseGifMetadata(header: Buffer): ParsedImageHeaderMetadata {
|
|
149
|
-
if (header.length < 10) return {};
|
|
150
|
-
const signature = header.slice(0, 6).toString("ascii");
|
|
151
|
-
if (signature !== "GIF87a" && signature !== "GIF89a") return {};
|
|
152
|
-
return {
|
|
153
|
-
width: header.readUInt16LE(6),
|
|
154
|
-
height: header.readUInt16LE(8),
|
|
155
|
-
channels: 3,
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function parseWebpMetadata(header: Buffer): ParsedImageHeaderMetadata {
|
|
160
|
-
if (header.length < 30) return {};
|
|
161
|
-
if (header.slice(0, 4).toString("ascii") !== "RIFF") return {};
|
|
162
|
-
if (header.slice(8, 12).toString("ascii") !== "WEBP") return {};
|
|
163
|
-
|
|
164
|
-
const chunkType = header.slice(12, 16).toString("ascii");
|
|
165
|
-
if (chunkType === "VP8X") {
|
|
166
|
-
const hasAlpha = (header[20] & 0x10) !== 0;
|
|
167
|
-
const width = (header[24] | (header[25] << 8) | (header[26] << 16)) + 1;
|
|
168
|
-
const height = (header[27] | (header[28] << 8) | (header[29] << 16)) + 1;
|
|
169
|
-
return { width, height, channels: hasAlpha ? 4 : 3, hasAlpha };
|
|
170
|
-
}
|
|
171
|
-
if (chunkType === "VP8L") {
|
|
172
|
-
if (header.length < 25) return {};
|
|
173
|
-
const bits = header.readUInt32LE(21);
|
|
174
|
-
const width = (bits & 0x3fff) + 1;
|
|
175
|
-
const height = ((bits >> 14) & 0x3fff) + 1;
|
|
176
|
-
const hasAlpha = ((bits >> 28) & 0x1) === 1;
|
|
177
|
-
return { width, height, channels: hasAlpha ? 4 : 3, hasAlpha };
|
|
178
|
-
}
|
|
179
|
-
if (chunkType === "VP8 ") {
|
|
180
|
-
const width = header.readUInt16LE(26) & 0x3fff;
|
|
181
|
-
const height = header.readUInt16LE(28) & 0x3fff;
|
|
182
|
-
return { width, height, channels: 3, hasAlpha: false };
|
|
183
|
-
}
|
|
184
|
-
return {};
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function parseImageHeaderMetadata(header: Buffer, mimeType: string): ParsedImageHeaderMetadata {
|
|
188
|
-
if (mimeType === "image/png") return parsePngMetadata(header);
|
|
189
|
-
if (mimeType === "image/jpeg") return parseJpegMetadata(header);
|
|
190
|
-
if (mimeType === "image/gif") return parseGifMetadata(header);
|
|
191
|
-
if (mimeType === "image/webp") return parseWebpMetadata(header);
|
|
192
|
-
return {};
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
async function readHeader(filePath: string, maxBytes: number): Promise<Buffer> {
|
|
196
|
-
if (maxBytes <= 0) return Buffer.alloc(0);
|
|
197
|
-
const fileHandle = await fs.open(filePath, "r");
|
|
198
|
-
try {
|
|
199
|
-
const buffer = Buffer.allocUnsafe(maxBytes);
|
|
200
|
-
const { bytesRead } = await fileHandle.read(buffer, 0, maxBytes, 0);
|
|
201
|
-
return buffer.subarray(0, bytesRead);
|
|
202
|
-
} finally {
|
|
203
|
-
await fileHandle.close();
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
export async function readImageMetadata(options: ReadImageMetadataOptions): Promise<ImageMetadata | null> {
|
|
208
|
-
const resolvedPath = options.resolvedPath ?? resolveReadPath(options.path, options.cwd);
|
|
209
|
-
const mimeType = options.detectedMimeType ?? (await detectSupportedImageMimeTypeFromFile(resolvedPath));
|
|
210
|
-
if (!mimeType) return null;
|
|
211
|
-
|
|
212
|
-
const stats = await Bun.file(resolvedPath).stat();
|
|
213
|
-
const bytes = stats.size;
|
|
214
|
-
const headerBytes = Math.max(0, Math.min(bytes, MAX_IMAGE_METADATA_HEADER_BYTES));
|
|
215
|
-
const header = await readHeader(resolvedPath, headerBytes);
|
|
216
|
-
const parsed = parseImageHeaderMetadata(header, mimeType);
|
|
217
|
-
|
|
218
|
-
return {
|
|
219
|
-
mimeType,
|
|
220
|
-
bytes,
|
|
221
|
-
width: parsed.width,
|
|
222
|
-
height: parsed.height,
|
|
223
|
-
channels: parsed.channels,
|
|
224
|
-
hasAlpha: parsed.hasAlpha,
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
export async function loadImageInput(options: LoadImageInputOptions): Promise<LoadedImageInput | null> {
|
|
229
|
-
const maxBytes = options.maxBytes ?? MAX_IMAGE_INPUT_BYTES;
|
|
230
|
-
const resolvedPath = options.resolvedPath ?? resolveReadPath(options.path, options.cwd);
|
|
231
|
-
const mimeType = options.detectedMimeType ?? (await detectSupportedImageMimeTypeFromFile(resolvedPath));
|
|
232
|
-
if (!mimeType) return null;
|
|
233
|
-
|
|
234
|
-
const stat = await Bun.file(resolvedPath).stat();
|
|
235
|
-
if (stat.size > maxBytes) {
|
|
236
|
-
throw new ImageInputTooLargeError(stat.size, maxBytes);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const inputBuffer = await fs.readFile(resolvedPath);
|
|
240
|
-
if (inputBuffer.byteLength > maxBytes) {
|
|
241
|
-
throw new ImageInputTooLargeError(inputBuffer.byteLength, maxBytes);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
let outputData = Buffer.from(inputBuffer).toBase64();
|
|
245
|
-
let outputMimeType = mimeType;
|
|
246
|
-
let outputBytes = inputBuffer.byteLength;
|
|
247
|
-
let dimensionNote: string | undefined;
|
|
248
|
-
|
|
249
|
-
if (options.autoResize) {
|
|
250
|
-
try {
|
|
251
|
-
const resized = await resizeImage({ type: "image", data: outputData, mimeType });
|
|
252
|
-
outputData = resized.data;
|
|
253
|
-
outputMimeType = resized.mimeType;
|
|
254
|
-
outputBytes = resized.buffer.byteLength;
|
|
255
|
-
dimensionNote = formatDimensionNote(resized);
|
|
256
|
-
} catch {
|
|
257
|
-
// keep original image when resize fails
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
let textNote = `Read image file [${outputMimeType}]`;
|
|
262
|
-
if (dimensionNote) {
|
|
263
|
-
textNote += `\n${dimensionNote}`;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
return {
|
|
267
|
-
resolvedPath,
|
|
268
|
-
mimeType: outputMimeType,
|
|
269
|
-
data: outputData,
|
|
270
|
-
textNote,
|
|
271
|
-
dimensionNote,
|
|
272
|
-
bytes: outputBytes,
|
|
273
|
-
};
|
|
274
|
-
}
|
package/src/utils/mime.ts
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs/promises";
|
|
2
|
-
|
|
3
|
-
const FILE_TYPE_SNIFF_BYTES = 12;
|
|
4
|
-
|
|
5
|
-
function detectMimeFromBytes(buf: Buffer, bytesRead: number): string | null {
|
|
6
|
-
if (bytesRead >= 3 && buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff) {
|
|
7
|
-
return "image/jpeg";
|
|
8
|
-
}
|
|
9
|
-
if (
|
|
10
|
-
bytesRead >= 8 &&
|
|
11
|
-
buf[0] === 0x89 &&
|
|
12
|
-
buf[1] === 0x50 &&
|
|
13
|
-
buf[2] === 0x4e &&
|
|
14
|
-
buf[3] === 0x47 &&
|
|
15
|
-
buf[4] === 0x0d &&
|
|
16
|
-
buf[5] === 0x0a &&
|
|
17
|
-
buf[6] === 0x1a &&
|
|
18
|
-
buf[7] === 0x0a
|
|
19
|
-
) {
|
|
20
|
-
return "image/png";
|
|
21
|
-
}
|
|
22
|
-
if (bytesRead >= 4 && buf[0] === 0x47 && buf[1] === 0x49 && buf[2] === 0x46 && buf[3] === 0x38) {
|
|
23
|
-
return "image/gif";
|
|
24
|
-
}
|
|
25
|
-
if (
|
|
26
|
-
bytesRead >= 12 &&
|
|
27
|
-
buf[0] === 0x52 &&
|
|
28
|
-
buf[1] === 0x49 &&
|
|
29
|
-
buf[2] === 0x46 &&
|
|
30
|
-
buf[3] === 0x46 &&
|
|
31
|
-
buf[8] === 0x57 &&
|
|
32
|
-
buf[9] === 0x45 &&
|
|
33
|
-
buf[10] === 0x42 &&
|
|
34
|
-
buf[11] === 0x50
|
|
35
|
-
) {
|
|
36
|
-
return "image/webp";
|
|
37
|
-
}
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export async function detectSupportedImageMimeTypeFromFile(filePath: string): Promise<string | null> {
|
|
42
|
-
const fileHandle = await fs.open(filePath, "r");
|
|
43
|
-
try {
|
|
44
|
-
const buffer = Buffer.allocUnsafe(FILE_TYPE_SNIFF_BYTES);
|
|
45
|
-
const { bytesRead } = await fileHandle.read(buffer, 0, FILE_TYPE_SNIFF_BYTES, 0);
|
|
46
|
-
if (bytesRead === 0) {
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
return detectMimeFromBytes(buffer, bytesRead);
|
|
50
|
-
} finally {
|
|
51
|
-
await fileHandle.close();
|
|
52
|
-
}
|
|
53
|
-
}
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
export type PromptRenderPhase = "pre-render" | "post-render";
|
|
2
|
-
|
|
3
|
-
export interface PromptFormatOptions {
|
|
4
|
-
renderPhase?: PromptRenderPhase;
|
|
5
|
-
replaceAsciiSymbols?: boolean;
|
|
6
|
-
boldRfc2119Keywords?: boolean;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
// Opening XML tag (not self-closing, not closing)
|
|
10
|
-
const OPENING_XML = /^<([a-z_-]+)(?:\s+[^>]*)?>$/;
|
|
11
|
-
// Closing XML tag
|
|
12
|
-
const CLOSING_XML = /^<\/([a-z_-]+)>$/;
|
|
13
|
-
// Handlebars block start: {{#if}}, {{#has}}, {{#list}}, etc.
|
|
14
|
-
const OPENING_HBS = /^\{\{#/;
|
|
15
|
-
// Handlebars block end: {{/if}}, {{/has}}, {{/list}}, etc.
|
|
16
|
-
const CLOSING_HBS = /^\{\{\//;
|
|
17
|
-
// List item (- or * or 1.)
|
|
18
|
-
const LIST_ITEM = /^(?:[-*]\s|\d+\.\s)/;
|
|
19
|
-
// Code fence
|
|
20
|
-
const CODE_FENCE = /^```/;
|
|
21
|
-
// Table row
|
|
22
|
-
const TABLE_ROW = /^\|.*\|$/;
|
|
23
|
-
// Table separator (|---|---|)
|
|
24
|
-
const TABLE_SEP = /^\|[-:\s|]+\|$/;
|
|
25
|
-
|
|
26
|
-
/** RFC 2119 keywords used in prompts. */
|
|
27
|
-
const RFC2119_KEYWORDS = /\b(?:MUST NOT|SHOULD NOT|SHALL NOT|RECOMMENDED|REQUIRED|OPTIONAL|SHOULD|SHALL|MUST|MAY)\b/g;
|
|
28
|
-
|
|
29
|
-
function boldRfc2119Keywords(line: string): string {
|
|
30
|
-
return line.replace(RFC2119_KEYWORDS, (match, offset, source) => {
|
|
31
|
-
const isAlreadyBold =
|
|
32
|
-
source[offset - 2] === "*" &&
|
|
33
|
-
source[offset - 1] === "*" &&
|
|
34
|
-
source[offset + match.length] === "*" &&
|
|
35
|
-
source[offset + match.length + 1] === "*";
|
|
36
|
-
if (isAlreadyBold) {
|
|
37
|
-
return match;
|
|
38
|
-
}
|
|
39
|
-
return `**${match}**`;
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** Compact a table row by trimming cell padding */
|
|
44
|
-
function compactTableRow(line: string): string {
|
|
45
|
-
const cells = line.split("|");
|
|
46
|
-
return cells.map(c => c.trim()).join("|");
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** Compact a table separator row */
|
|
50
|
-
function compactTableSep(line: string): string {
|
|
51
|
-
const cells = line.split("|").filter(c => c.trim());
|
|
52
|
-
const normalized = cells.map(c => {
|
|
53
|
-
const trimmed = c.trim();
|
|
54
|
-
const left = trimmed.startsWith(":");
|
|
55
|
-
const right = trimmed.endsWith(":");
|
|
56
|
-
if (left && right) return ":---:";
|
|
57
|
-
if (left) return ":---";
|
|
58
|
-
if (right) return "---:";
|
|
59
|
-
return "---";
|
|
60
|
-
});
|
|
61
|
-
return `|${normalized.join("|")}|`;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function replaceCommonAsciiSymbols(line: string): string {
|
|
65
|
-
return line
|
|
66
|
-
.replace(/\.{3}/g, "…")
|
|
67
|
-
.replace(/<->/g, "↔")
|
|
68
|
-
.replace(/->/g, "→")
|
|
69
|
-
.replace(/<-/g, "←")
|
|
70
|
-
.replace(/!=/g, "≠")
|
|
71
|
-
.replace(/<=/g, "≤")
|
|
72
|
-
.replace(/>=/g, "≥");
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export function formatPromptContent(content: string, options: PromptFormatOptions = {}): string {
|
|
76
|
-
const {
|
|
77
|
-
renderPhase = "post-render",
|
|
78
|
-
replaceAsciiSymbols = false,
|
|
79
|
-
boldRfc2119Keywords: shouldBoldRfc2119 = false,
|
|
80
|
-
} = options;
|
|
81
|
-
const isPreRender = renderPhase === "pre-render";
|
|
82
|
-
const lines = content.split("\n");
|
|
83
|
-
const result: string[] = [];
|
|
84
|
-
let inCodeBlock = false;
|
|
85
|
-
const topLevelTags: string[] = [];
|
|
86
|
-
|
|
87
|
-
for (let i = 0; i < lines.length; i++) {
|
|
88
|
-
let line = lines[i].trimEnd();
|
|
89
|
-
let trimmedStart = line.trimStart();
|
|
90
|
-
if (CODE_FENCE.test(trimmedStart)) {
|
|
91
|
-
inCodeBlock = !inCodeBlock;
|
|
92
|
-
result.push(line);
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (inCodeBlock) {
|
|
97
|
-
result.push(line);
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (replaceAsciiSymbols) {
|
|
102
|
-
line = replaceCommonAsciiSymbols(line);
|
|
103
|
-
}
|
|
104
|
-
trimmedStart = line.trimStart();
|
|
105
|
-
const trimmed = line.trim();
|
|
106
|
-
|
|
107
|
-
const isOpeningXml = OPENING_XML.test(trimmedStart) && !trimmedStart.endsWith("/>");
|
|
108
|
-
if (isOpeningXml && line.length === trimmedStart.length) {
|
|
109
|
-
const match = OPENING_XML.exec(trimmedStart);
|
|
110
|
-
if (match) topLevelTags.push(match[1]);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const closingMatch = CLOSING_XML.exec(trimmedStart);
|
|
114
|
-
if (closingMatch) {
|
|
115
|
-
const tagName = closingMatch[1];
|
|
116
|
-
if (topLevelTags.length > 0 && topLevelTags[topLevelTags.length - 1] === tagName) {
|
|
117
|
-
topLevelTags.pop();
|
|
118
|
-
}
|
|
119
|
-
} else if (isPreRender && trimmedStart.startsWith("{{")) {
|
|
120
|
-
/* keep indentation as-is in pre-render for Handlebars markers */
|
|
121
|
-
} else if (TABLE_SEP.test(trimmedStart)) {
|
|
122
|
-
const leadingWhitespace = line.slice(0, line.length - trimmedStart.length);
|
|
123
|
-
line = `${leadingWhitespace}${compactTableSep(trimmedStart)}`;
|
|
124
|
-
} else if (TABLE_ROW.test(trimmedStart)) {
|
|
125
|
-
const leadingWhitespace = line.slice(0, line.length - trimmedStart.length);
|
|
126
|
-
line = `${leadingWhitespace}${compactTableRow(trimmedStart)}`;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (shouldBoldRfc2119) {
|
|
130
|
-
line = boldRfc2119Keywords(line);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const isBlank = trimmed === "";
|
|
134
|
-
if (isBlank) {
|
|
135
|
-
const prevLine = result[result.length - 1]?.trim() ?? "";
|
|
136
|
-
const nextLine = lines[i + 1]?.trim() ?? "";
|
|
137
|
-
|
|
138
|
-
if (LIST_ITEM.test(nextLine)) {
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (OPENING_XML.test(prevLine) || (isPreRender && OPENING_HBS.test(prevLine))) {
|
|
143
|
-
continue;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (CLOSING_XML.test(nextLine) || (isPreRender && CLOSING_HBS.test(nextLine))) {
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const prevIsBlank = prevLine === "";
|
|
151
|
-
if (prevIsBlank) {
|
|
152
|
-
continue;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if (CLOSING_XML.test(trimmed) || (isPreRender && CLOSING_HBS.test(trimmed))) {
|
|
157
|
-
while (result.length > 0 && result[result.length - 1].trim() === "") {
|
|
158
|
-
result.pop();
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
result.push(line);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
while (result.length > 0 && result[result.length - 1].trim() === "") {
|
|
166
|
-
result.pop();
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
return result.join("\n");
|
|
170
|
-
}
|