mini-coder 0.5.12 → 0.5.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/BENCHMARK.md +15 -316
- package/PROGRESS.md +3 -3
- package/README.md +54 -21
- package/benchmark-baseline.sh +15 -0
- package/bun.lock +265 -90
- package/package.json +8 -7
- package/skills-lock.json +15 -0
- package/src/agent.ts +526 -13
- package/src/assistant-output.ts +73 -0
- package/src/cli.ts +2 -1
- package/src/delegation.ts +238 -0
- package/src/headless.ts +90 -44
- package/src/index.ts +267 -102
- package/src/input.ts +13 -1
- package/src/mcp.ts +609 -0
- package/src/prompt.ts +11 -13
- package/src/session-message.ts +57 -65
- package/src/session.ts +389 -42
- package/src/settings.ts +199 -7
- package/src/skills.ts +12 -3
- package/src/submit.ts +24 -2
- package/src/theme.ts +186 -3
- package/src/tool-common.ts +2 -0
- package/src/tool-delegate.ts +125 -0
- package/src/tool-shell.ts +190 -8
- package/src/tools.ts +335 -6
- package/src/ui/agent.ts +10 -0
- package/src/ui/commands.test.ts +525 -10
- package/src/ui/commands.ts +224 -8
- package/src/ui/conversation.test.ts +252 -27
- package/src/ui/conversation.ts +468 -390
- package/src/ui/help.ts +27 -11
- package/src/ui.ts +230 -75
- package/src/plugins.ts +0 -183
package/src/tools.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Built-in tool implementations: `shell`, `read`, `grep`, `edit`,
|
|
3
|
-
* `todoRead`, and `readImage`.
|
|
2
|
+
* Built-in tool implementations: `shell`, `delegate`, `read`, `grep`, `edit`,
|
|
3
|
+
* `todoWrite`, `todoRead`, and `readImage`.
|
|
4
4
|
*
|
|
5
|
-
* Shell, read, and grep live in dedicated modules and are re-exported
|
|
6
|
-
* the rest of the codebase can keep a single built-in-tools import
|
|
5
|
+
* Shell, delegate, read, and grep live in dedicated modules and are re-exported
|
|
6
|
+
* here so the rest of the codebase can keep a single built-in-tools import
|
|
7
|
+
* surface.
|
|
7
8
|
*
|
|
8
9
|
* Each tool is exposed as a pure-ish execute function that takes typed
|
|
9
10
|
* arguments and a working directory, returning a result object. The pi-ai
|
|
@@ -15,6 +16,7 @@
|
|
|
15
16
|
|
|
16
17
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
17
18
|
import { dirname, extname, isAbsolute, join } from "node:path";
|
|
19
|
+
import { crc32, inflateSync } from "node:zlib";
|
|
18
20
|
import type {
|
|
19
21
|
Message,
|
|
20
22
|
Static,
|
|
@@ -33,6 +35,15 @@ import {
|
|
|
33
35
|
} from "./tool-common.ts";
|
|
34
36
|
|
|
35
37
|
export type { ToolExecResult } from "./tool-common.ts";
|
|
38
|
+
export {
|
|
39
|
+
type CreateDelegateToolHandlerOpts,
|
|
40
|
+
createDelegateToolHandler,
|
|
41
|
+
type DelegateArgs,
|
|
42
|
+
type DelegateResultDetails,
|
|
43
|
+
type DelegateRunResult,
|
|
44
|
+
delegateTool,
|
|
45
|
+
formatDelegateResultText,
|
|
46
|
+
} from "./tool-delegate.ts";
|
|
36
47
|
export {
|
|
37
48
|
DEFAULT_GREP_LIMIT,
|
|
38
49
|
executeGrep,
|
|
@@ -58,9 +69,15 @@ export {
|
|
|
58
69
|
readToolHandler,
|
|
59
70
|
} from "./tool-read.ts";
|
|
60
71
|
export {
|
|
72
|
+
createDelegationAwareShellToolHandler,
|
|
73
|
+
type DelegationAwareShellToolHandlerOpts,
|
|
61
74
|
executeShell,
|
|
75
|
+
formatShellResultText,
|
|
76
|
+
parseLegacyShellResult,
|
|
77
|
+
parseShellResultDetails,
|
|
62
78
|
type ShellArgs,
|
|
63
79
|
type ShellOpts,
|
|
80
|
+
type ShellResultDetails,
|
|
64
81
|
shellTool,
|
|
65
82
|
shellToolHandler,
|
|
66
83
|
truncateOutput,
|
|
@@ -699,6 +716,312 @@ const IMAGE_MIME_TYPES: Record<string, string> = {
|
|
|
699
716
|
".webp": "image/webp",
|
|
700
717
|
};
|
|
701
718
|
|
|
719
|
+
const PNG_SIGNATURE = Buffer.from([
|
|
720
|
+
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
|
|
721
|
+
]);
|
|
722
|
+
const JPEG_SIGNATURE = Buffer.from([0xff, 0xd8]);
|
|
723
|
+
const GIF_SIGNATURES = ["GIF87a", "GIF89a"];
|
|
724
|
+
const WEBP_RIFF_SIGNATURE = "RIFF";
|
|
725
|
+
const WEBP_FILE_SIGNATURE = "WEBP";
|
|
726
|
+
|
|
727
|
+
interface PngChunk {
|
|
728
|
+
typeBytes: Buffer;
|
|
729
|
+
type: string;
|
|
730
|
+
data: Buffer;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
interface PngValidationState {
|
|
734
|
+
sawIHDR: boolean;
|
|
735
|
+
sawIDAT: boolean;
|
|
736
|
+
sawIEND: boolean;
|
|
737
|
+
width: number;
|
|
738
|
+
height: number;
|
|
739
|
+
bitDepth: number;
|
|
740
|
+
colorType: number;
|
|
741
|
+
interlaceMethod: number;
|
|
742
|
+
compressedParts: Buffer[];
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function createPngValidationState(): PngValidationState {
|
|
746
|
+
return {
|
|
747
|
+
sawIHDR: false,
|
|
748
|
+
sawIDAT: false,
|
|
749
|
+
sawIEND: false,
|
|
750
|
+
width: 0,
|
|
751
|
+
height: 0,
|
|
752
|
+
bitDepth: 0,
|
|
753
|
+
colorType: 0,
|
|
754
|
+
interlaceMethod: 0,
|
|
755
|
+
compressedParts: [],
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function isValidPngColorFormat(bitDepth: number, colorType: number): boolean {
|
|
760
|
+
switch (colorType) {
|
|
761
|
+
case 0:
|
|
762
|
+
return [1, 2, 4, 8, 16].includes(bitDepth);
|
|
763
|
+
case 2:
|
|
764
|
+
case 4:
|
|
765
|
+
case 6:
|
|
766
|
+
return bitDepth === 8 || bitDepth === 16;
|
|
767
|
+
case 3:
|
|
768
|
+
return [1, 2, 4, 8].includes(bitDepth);
|
|
769
|
+
default:
|
|
770
|
+
return false;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function getPngScanlineByteLength(
|
|
775
|
+
width: number,
|
|
776
|
+
bitDepth: number,
|
|
777
|
+
colorType: number,
|
|
778
|
+
): number {
|
|
779
|
+
const samplesPerPixel =
|
|
780
|
+
colorType === 0 || colorType === 3
|
|
781
|
+
? 1
|
|
782
|
+
: colorType === 4
|
|
783
|
+
? 2
|
|
784
|
+
: colorType === 2
|
|
785
|
+
? 3
|
|
786
|
+
: 4;
|
|
787
|
+
return Math.ceil((width * bitDepth * samplesPerPixel) / 8);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function readPngChunk(
|
|
791
|
+
data: Buffer,
|
|
792
|
+
offset: number,
|
|
793
|
+
): { chunk: PngChunk; nextOffset: number } | string {
|
|
794
|
+
if (offset + 12 > data.length) {
|
|
795
|
+
return "truncated PNG chunk header";
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const length = data.readUInt32BE(offset);
|
|
799
|
+
const typeBytes = data.subarray(offset + 4, offset + 8);
|
|
800
|
+
const type = typeBytes.toString("ascii");
|
|
801
|
+
const dataOffset = offset + 8;
|
|
802
|
+
const nextOffset = dataOffset + length + 4;
|
|
803
|
+
|
|
804
|
+
if (nextOffset > data.length) {
|
|
805
|
+
return `truncated PNG chunk ${type}`;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
const chunkData = data.subarray(dataOffset, dataOffset + length);
|
|
809
|
+
const storedCrc = data.readUInt32BE(dataOffset + length);
|
|
810
|
+
const computedCrc = crc32(Buffer.concat([typeBytes, chunkData]));
|
|
811
|
+
|
|
812
|
+
if (storedCrc !== computedCrc) {
|
|
813
|
+
return `invalid PNG CRC for chunk ${type}`;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
return {
|
|
817
|
+
chunk: { typeBytes, type, data: chunkData },
|
|
818
|
+
nextOffset,
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function validatePngHeaderChunk(
|
|
823
|
+
state: PngValidationState,
|
|
824
|
+
chunk: PngChunk,
|
|
825
|
+
): string | null {
|
|
826
|
+
if (state.sawIHDR || state.sawIDAT || chunk.data.length !== 13) {
|
|
827
|
+
return "invalid IHDR chunk";
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const compressionMethod = chunk.data[10] ?? 0;
|
|
831
|
+
const filterMethod = chunk.data[11] ?? 0;
|
|
832
|
+
const interlaceMethod = chunk.data[12] ?? 0;
|
|
833
|
+
|
|
834
|
+
state.sawIHDR = true;
|
|
835
|
+
state.width = chunk.data.readUInt32BE(0);
|
|
836
|
+
state.height = chunk.data.readUInt32BE(4);
|
|
837
|
+
state.bitDepth = chunk.data[8] ?? 0;
|
|
838
|
+
state.colorType = chunk.data[9] ?? 0;
|
|
839
|
+
state.interlaceMethod = interlaceMethod;
|
|
840
|
+
|
|
841
|
+
if (state.width === 0 || state.height === 0) {
|
|
842
|
+
return "invalid PNG image size";
|
|
843
|
+
}
|
|
844
|
+
if (!isValidPngColorFormat(state.bitDepth, state.colorType)) {
|
|
845
|
+
return "unsupported PNG color format";
|
|
846
|
+
}
|
|
847
|
+
if (compressionMethod !== 0 || filterMethod !== 0) {
|
|
848
|
+
return "unsupported PNG header values";
|
|
849
|
+
}
|
|
850
|
+
if (interlaceMethod !== 0 && interlaceMethod !== 1) {
|
|
851
|
+
return "unsupported PNG interlace method";
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
return null;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function applyPngChunk(
|
|
858
|
+
state: PngValidationState,
|
|
859
|
+
chunk: PngChunk,
|
|
860
|
+
nextOffset: number,
|
|
861
|
+
totalLength: number,
|
|
862
|
+
): string | null {
|
|
863
|
+
if (chunk.type === "IHDR") {
|
|
864
|
+
return validatePngHeaderChunk(state, chunk);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
if (!state.sawIHDR) {
|
|
868
|
+
return "missing IHDR chunk";
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
if (chunk.type === "IDAT") {
|
|
872
|
+
if (state.sawIEND) {
|
|
873
|
+
return "invalid PNG chunk order";
|
|
874
|
+
}
|
|
875
|
+
state.sawIDAT = true;
|
|
876
|
+
state.compressedParts.push(chunk.data);
|
|
877
|
+
return null;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
if (chunk.type !== "IEND") {
|
|
881
|
+
return null;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
if (!state.sawIDAT) {
|
|
885
|
+
return "missing IDAT chunk";
|
|
886
|
+
}
|
|
887
|
+
if (chunk.data.length !== 0) {
|
|
888
|
+
return "invalid IEND chunk length";
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
state.sawIEND = true;
|
|
892
|
+
if (nextOffset !== totalLength) {
|
|
893
|
+
return "unexpected trailing data after IEND chunk";
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
return null;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function validateInflatedPngData(state: PngValidationState): string | null {
|
|
900
|
+
try {
|
|
901
|
+
const inflated = inflateSync(Buffer.concat(state.compressedParts));
|
|
902
|
+
if (state.interlaceMethod !== 0) {
|
|
903
|
+
return null;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
const expectedLength =
|
|
907
|
+
state.height *
|
|
908
|
+
(1 +
|
|
909
|
+
getPngScanlineByteLength(state.width, state.bitDepth, state.colorType));
|
|
910
|
+
if (inflated.length !== expectedLength) {
|
|
911
|
+
return "decoded PNG payload does not match image dimensions";
|
|
912
|
+
}
|
|
913
|
+
return null;
|
|
914
|
+
} catch (error) {
|
|
915
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
916
|
+
return `corrupt PNG image data: ${message}`;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
function validatePngImageData(data: Buffer): string | null {
|
|
921
|
+
if (
|
|
922
|
+
data.length < PNG_SIGNATURE.length ||
|
|
923
|
+
!data.subarray(0, PNG_SIGNATURE.length).equals(PNG_SIGNATURE)
|
|
924
|
+
) {
|
|
925
|
+
return "invalid PNG signature";
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
const state = createPngValidationState();
|
|
929
|
+
let offset = PNG_SIGNATURE.length;
|
|
930
|
+
|
|
931
|
+
while (offset < data.length) {
|
|
932
|
+
const parsedChunk = readPngChunk(data, offset);
|
|
933
|
+
if (typeof parsedChunk === "string") {
|
|
934
|
+
return parsedChunk;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
const chunkError = applyPngChunk(
|
|
938
|
+
state,
|
|
939
|
+
parsedChunk.chunk,
|
|
940
|
+
parsedChunk.nextOffset,
|
|
941
|
+
data.length,
|
|
942
|
+
);
|
|
943
|
+
if (chunkError) {
|
|
944
|
+
return chunkError;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
offset = parsedChunk.nextOffset;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
if (!state.sawIHDR) {
|
|
951
|
+
return "missing IHDR chunk";
|
|
952
|
+
}
|
|
953
|
+
if (!state.sawIDAT) {
|
|
954
|
+
return "missing IDAT chunk";
|
|
955
|
+
}
|
|
956
|
+
if (!state.sawIEND) {
|
|
957
|
+
return "missing IEND chunk";
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
return validateInflatedPngData(state);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function validateJpegImageData(data: Buffer): string | null {
|
|
964
|
+
if (
|
|
965
|
+
data.length < 4 ||
|
|
966
|
+
!data.subarray(0, JPEG_SIGNATURE.length).equals(JPEG_SIGNATURE) ||
|
|
967
|
+
data.at(-2) !== 0xff ||
|
|
968
|
+
data.at(-1) !== 0xd9
|
|
969
|
+
) {
|
|
970
|
+
return "invalid JPEG markers";
|
|
971
|
+
}
|
|
972
|
+
return null;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
function validateGifImageData(data: Buffer): string | null {
|
|
976
|
+
if (data.length < 14) {
|
|
977
|
+
return "truncated GIF file";
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
const header = data.subarray(0, 6).toString("ascii");
|
|
981
|
+
if (!GIF_SIGNATURES.includes(header)) {
|
|
982
|
+
return "invalid GIF signature";
|
|
983
|
+
}
|
|
984
|
+
if (data.at(-1) !== 0x3b) {
|
|
985
|
+
return "missing GIF trailer";
|
|
986
|
+
}
|
|
987
|
+
return null;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
function validateWebpImageData(data: Buffer): string | null {
|
|
991
|
+
if (data.length < 16) {
|
|
992
|
+
return "truncated WebP file";
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
if (
|
|
996
|
+
data.subarray(0, 4).toString("ascii") !== WEBP_RIFF_SIGNATURE ||
|
|
997
|
+
data.subarray(8, 12).toString("ascii") !== WEBP_FILE_SIGNATURE
|
|
998
|
+
) {
|
|
999
|
+
return "invalid WebP signature";
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
const riffSize = data.readUInt32LE(4);
|
|
1003
|
+
if (riffSize + 8 > data.length) {
|
|
1004
|
+
return "truncated WebP file";
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
return null;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
function validateImageData(mimeType: string, data: Buffer): string | null {
|
|
1011
|
+
switch (mimeType) {
|
|
1012
|
+
case "image/png":
|
|
1013
|
+
return validatePngImageData(data);
|
|
1014
|
+
case "image/jpeg":
|
|
1015
|
+
return validateJpegImageData(data);
|
|
1016
|
+
case "image/gif":
|
|
1017
|
+
return validateGifImageData(data);
|
|
1018
|
+
case "image/webp":
|
|
1019
|
+
return validateWebpImageData(data);
|
|
1020
|
+
default:
|
|
1021
|
+
return `unsupported image MIME type ${mimeType}`;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
|
|
702
1025
|
const readImageToolParameters = Type.Object({
|
|
703
1026
|
path: Type.String({
|
|
704
1027
|
description: "File path (absolute or relative to cwd)",
|
|
@@ -740,10 +1063,16 @@ export function executeReadImage(
|
|
|
740
1063
|
|
|
741
1064
|
try {
|
|
742
1065
|
const data = readFileSync(filePath);
|
|
743
|
-
const
|
|
1066
|
+
const validationError = validateImageData(mimeType, data);
|
|
1067
|
+
if (validationError) {
|
|
1068
|
+
return textResult(
|
|
1069
|
+
`Invalid image file ${args.path}: ${validationError}`,
|
|
1070
|
+
true,
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
744
1073
|
|
|
745
1074
|
return {
|
|
746
|
-
content: [{ type: "image", data: base64, mimeType }],
|
|
1075
|
+
content: [{ type: "image", data: data.toString("base64"), mimeType }],
|
|
747
1076
|
isError: false,
|
|
748
1077
|
};
|
|
749
1078
|
} catch (error) {
|
package/src/ui/agent.ts
CHANGED
|
@@ -46,6 +46,8 @@ interface UiAgentRuntime {
|
|
|
46
46
|
requestRender: (priority?: UiRenderPriority) => void;
|
|
47
47
|
/** Re-enable stick-to-bottom behavior for the conversation log. */
|
|
48
48
|
scrollConversationToBottom: () => void;
|
|
49
|
+
/** Clear the readonly queued-input draft after it is committed. */
|
|
50
|
+
clearQueuedInputDraft: () => void;
|
|
49
51
|
/** Start the active-turn divider animation. */
|
|
50
52
|
startDividerAnimation: () => void;
|
|
51
53
|
/** Stop the active-turn divider animation. */
|
|
@@ -116,12 +118,14 @@ export function createUiAgentController(
|
|
|
116
118
|
if (
|
|
117
119
|
pending.toolName === event.name &&
|
|
118
120
|
pending.content === event.result.content &&
|
|
121
|
+
pending.details === event.result.details &&
|
|
119
122
|
pending.isError === event.result.isError
|
|
120
123
|
) {
|
|
121
124
|
return false;
|
|
122
125
|
}
|
|
123
126
|
pending.toolName = event.name;
|
|
124
127
|
pending.content = event.result.content;
|
|
128
|
+
pending.details = event.result.details;
|
|
125
129
|
pending.isError = event.result.isError;
|
|
126
130
|
return true;
|
|
127
131
|
}
|
|
@@ -130,6 +134,7 @@ export function createUiAgentController(
|
|
|
130
134
|
toolCallId: event.toolCallId,
|
|
131
135
|
toolName: event.name,
|
|
132
136
|
content: event.result.content,
|
|
137
|
+
details: event.result.details,
|
|
133
138
|
isError: event.result.isError,
|
|
134
139
|
});
|
|
135
140
|
return true;
|
|
@@ -195,6 +200,7 @@ export function createUiAgentController(
|
|
|
195
200
|
): void => {
|
|
196
201
|
switch (event.type) {
|
|
197
202
|
case "user_message":
|
|
203
|
+
runtime.clearQueuedInputDraft();
|
|
198
204
|
runtime.scrollConversationToBottom();
|
|
199
205
|
runtime.requestRender("normal");
|
|
200
206
|
return;
|
|
@@ -215,6 +221,9 @@ export function createUiAgentController(
|
|
|
215
221
|
runtime.requestRender("normal");
|
|
216
222
|
}
|
|
217
223
|
return;
|
|
224
|
+
case "context_compacted":
|
|
225
|
+
runtime.requestRender("normal");
|
|
226
|
+
return;
|
|
218
227
|
case "tool_start":
|
|
219
228
|
return;
|
|
220
229
|
}
|
|
@@ -280,6 +289,7 @@ export function createUiAgentController(
|
|
|
280
289
|
onEvent: (event, currentState) => handleAgentEvent(event, currentState),
|
|
281
290
|
onTurnEnd: () => {
|
|
282
291
|
resetStreamingState();
|
|
292
|
+
runtime.clearQueuedInputDraft();
|
|
283
293
|
runtime.stopDividerAnimation();
|
|
284
294
|
runtime.requestRender("normal");
|
|
285
295
|
},
|