pi-readseek 0.4.25 → 0.4.27
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/README.md +35 -2
- package/index.ts +6 -0
- package/package.json +1 -1
- package/prompts/def.md +24 -0
- package/prompts/hover.md +19 -0
- package/prompts/read.md +1 -1
- package/prompts/rename.md +31 -0
- package/src/def.ts +148 -0
- package/src/edit.ts +4 -14
- package/src/fs-error.ts +27 -24
- package/src/hover.ts +133 -0
- package/src/read.ts +51 -17
- package/src/readseek-client.ts +303 -2
- package/src/readseek-settings.ts +24 -0
- package/src/rename.ts +167 -0
- package/src/stat-search-path.ts +3 -8
- package/src/write.ts +10 -66
package/src/readseek-client.ts
CHANGED
|
@@ -118,6 +118,11 @@ export interface ReadSeekTranscript {
|
|
|
118
118
|
regions: ReadSeekTranscriptRegion[];
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
export interface ReadSeekDetectedObject {
|
|
122
|
+
label: string;
|
|
123
|
+
bbox: [number, number, number, number];
|
|
124
|
+
}
|
|
125
|
+
|
|
121
126
|
export type ReadSeekDetection =
|
|
122
127
|
| {
|
|
123
128
|
kind: "source";
|
|
@@ -139,6 +144,8 @@ export type ReadSeekDetection =
|
|
|
139
144
|
height: number;
|
|
140
145
|
animated: boolean;
|
|
141
146
|
transcribe?: ReadSeekTranscript;
|
|
147
|
+
caption?: string;
|
|
148
|
+
objects?: ReadSeekDetectedObject[];
|
|
142
149
|
}
|
|
143
150
|
| {
|
|
144
151
|
kind: "text";
|
|
@@ -155,6 +162,13 @@ interface ReadSeekSearchOptions {
|
|
|
155
162
|
signal?: AbortSignal;
|
|
156
163
|
}
|
|
157
164
|
|
|
165
|
+
interface ReadSeekDetectOptions {
|
|
166
|
+
transcribe?: boolean;
|
|
167
|
+
caption?: boolean;
|
|
168
|
+
objects?: boolean;
|
|
169
|
+
signal?: AbortSignal;
|
|
170
|
+
}
|
|
171
|
+
|
|
158
172
|
function normalizeLanguage(language: string): string {
|
|
159
173
|
return language === "java" ? "Java" : language;
|
|
160
174
|
}
|
|
@@ -348,10 +362,11 @@ async function spawnReadSeekRaw(args: string[], options: RunReadSeekOptions = {}
|
|
|
348
362
|
});
|
|
349
363
|
childStdin.end(stdin, "utf-8");
|
|
350
364
|
}
|
|
351
|
-
child.on("close", (code) => {
|
|
365
|
+
child.on("close", (code, signal) => {
|
|
352
366
|
const stdout = Buffer.concat(stdoutChunks).toString("utf-8");
|
|
353
367
|
const stderr = Buffer.concat(stderrChunks).toString("utf-8").trim();
|
|
354
368
|
if (code === 0) succeed(stdout);
|
|
369
|
+
else if (signal) fail(new Error((stderr || `readseek killed by signal ${signal}`).replace(/^error:\s*/i, "")));
|
|
355
370
|
else fail(new Error((stderr || `readseek exited with status ${code}`).replace(/^error:\s*/i, "")));
|
|
356
371
|
});
|
|
357
372
|
});
|
|
@@ -659,6 +674,21 @@ function parseTranscript(value: unknown): ReadSeekTranscript | undefined {
|
|
|
659
674
|
};
|
|
660
675
|
}
|
|
661
676
|
|
|
677
|
+
function parseDetectedObjects(value: unknown): ReadSeekDetectedObject[] | undefined {
|
|
678
|
+
if (value === undefined || value === null) return undefined;
|
|
679
|
+
if (!Array.isArray(value)) throw new Error("invalid readseek detect objects");
|
|
680
|
+
return value.map((object) => {
|
|
681
|
+
if (!object || typeof object !== "object") throw new Error("invalid readseek detect object");
|
|
682
|
+
const item = object as Record<string, unknown>;
|
|
683
|
+
const bbox = item.bbox;
|
|
684
|
+
if (!Array.isArray(bbox) || bbox.length !== 4) throw new Error("invalid readseek detect object.bbox");
|
|
685
|
+
return {
|
|
686
|
+
label: requireString(item.label, "object.label"),
|
|
687
|
+
bbox: bbox.map((n, i) => requireNumber(n, `object.bbox[${i}]`)) as ReadSeekDetectedObject["bbox"],
|
|
688
|
+
};
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
|
|
662
692
|
function parseDetectOutput(value: unknown): ReadSeekDetection {
|
|
663
693
|
if (!value || typeof value !== "object") throw new Error("invalid readseek detect output");
|
|
664
694
|
const output = value as Record<string, unknown>;
|
|
@@ -682,6 +712,8 @@ function parseDetectOutput(value: unknown): ReadSeekDetection {
|
|
|
682
712
|
height: requireNumber(output.height, "height"),
|
|
683
713
|
animated: requireBoolean(output.animated, "animated"),
|
|
684
714
|
transcribe: parseTranscript(output.transcribe),
|
|
715
|
+
caption: optionalString(output.caption, "caption"),
|
|
716
|
+
objects: parseDetectedObjects(output.objects),
|
|
685
717
|
};
|
|
686
718
|
}
|
|
687
719
|
if (output.language !== undefined) {
|
|
@@ -701,10 +733,279 @@ function parseDetectOutput(value: unknown): ReadSeekDetection {
|
|
|
701
733
|
|
|
702
734
|
export async function readseekDetect(
|
|
703
735
|
filePath: string,
|
|
704
|
-
options:
|
|
736
|
+
options: ReadSeekDetectOptions = {},
|
|
705
737
|
): Promise<ReadSeekDetection> {
|
|
706
738
|
const args = ["detect"];
|
|
707
739
|
if (options.transcribe) args.push("--transcribe");
|
|
740
|
+
if (options.caption) args.push("--caption");
|
|
741
|
+
if (options.objects) args.push("--objects");
|
|
708
742
|
args.push(filePath);
|
|
709
743
|
return parseDetectOutput(await runReadSeek(args, { signal: options.signal }));
|
|
710
744
|
}
|
|
745
|
+
|
|
746
|
+
// --- Rename ---
|
|
747
|
+
|
|
748
|
+
export interface RenameConflict {
|
|
749
|
+
line: number;
|
|
750
|
+
column: number;
|
|
751
|
+
reason: string;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
export interface RenameEdit {
|
|
755
|
+
line: number;
|
|
756
|
+
start_column: number;
|
|
757
|
+
end_column: number;
|
|
758
|
+
start_byte: number;
|
|
759
|
+
end_byte: number;
|
|
760
|
+
occurrence: string;
|
|
761
|
+
line_hash: string;
|
|
762
|
+
text: string;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
export interface RenameFileOutput {
|
|
766
|
+
file: string;
|
|
767
|
+
language: string;
|
|
768
|
+
engine?: string;
|
|
769
|
+
file_hash: string;
|
|
770
|
+
conflicts: RenameConflict[];
|
|
771
|
+
edits: RenameEdit[];
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
export interface RenameOutput {
|
|
775
|
+
file: string;
|
|
776
|
+
language: string;
|
|
777
|
+
engine?: string;
|
|
778
|
+
file_hash: string;
|
|
779
|
+
old_name: string;
|
|
780
|
+
new_name: string;
|
|
781
|
+
applied: boolean;
|
|
782
|
+
conflicts: RenameConflict[];
|
|
783
|
+
edits: RenameEdit[];
|
|
784
|
+
others: RenameFileOutput[];
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
interface RenameOptions {
|
|
788
|
+
to: string;
|
|
789
|
+
line: number;
|
|
790
|
+
column?: number;
|
|
791
|
+
workspace?: string;
|
|
792
|
+
apply?: boolean;
|
|
793
|
+
language?: string;
|
|
794
|
+
cached?: boolean;
|
|
795
|
+
others?: boolean;
|
|
796
|
+
ignored?: boolean;
|
|
797
|
+
signal?: AbortSignal;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function parseRenameOutput(value: unknown): RenameOutput {
|
|
801
|
+
if (!value || typeof value !== "object") throw new Error("invalid readseek rename output");
|
|
802
|
+
const output = value as Record<string, unknown>;
|
|
803
|
+
return {
|
|
804
|
+
file: requireString(output.file, "file"),
|
|
805
|
+
language: requireString(output.language, "language"),
|
|
806
|
+
engine: optionalString(output.engine, "engine"),
|
|
807
|
+
file_hash: requireString(output.file_hash, "file_hash"),
|
|
808
|
+
old_name: requireString(output.old_name, "old_name"),
|
|
809
|
+
new_name: requireString(output.new_name, "new_name"),
|
|
810
|
+
applied: requireBoolean(output.applied, "applied"),
|
|
811
|
+
conflicts: (output.conflicts as any[] | undefined)?.map((c: Record<string, unknown>) => ({
|
|
812
|
+
line: requireNumber(c.line, "conflict.line"),
|
|
813
|
+
column: requireNumber(c.column, "conflict.column"),
|
|
814
|
+
reason: requireString(c.reason, "conflict.reason"),
|
|
815
|
+
})) ?? [],
|
|
816
|
+
edits: (output.edits as any[] | undefined)?.map((e: Record<string, unknown>) => ({
|
|
817
|
+
line: requireNumber(e.line, "edit.line"),
|
|
818
|
+
start_column: requireNumber(e.start_column, "edit.start_column"),
|
|
819
|
+
end_column: requireNumber(e.end_column, "edit.end_column"),
|
|
820
|
+
start_byte: requireNumber(e.start_byte, "edit.start_byte"),
|
|
821
|
+
end_byte: requireNumber(e.end_byte, "edit.end_byte"),
|
|
822
|
+
occurrence: requireString(e.occurrence, "edit.occurrence"),
|
|
823
|
+
line_hash: requireString(e.line_hash, "edit.line_hash"),
|
|
824
|
+
text: requireString(e.text, "edit.text"),
|
|
825
|
+
})) ?? [],
|
|
826
|
+
others: (output.others as any[] | undefined)?.map((o: Record<string, unknown>) => ({
|
|
827
|
+
file: requireString(o.file, "other.file"),
|
|
828
|
+
language: requireString(o.language, "other.language"),
|
|
829
|
+
engine: optionalString(o.engine, "other.engine"),
|
|
830
|
+
file_hash: requireString(o.file_hash, "other.file_hash"),
|
|
831
|
+
conflicts: (o.conflicts as any[] | undefined)?.map((c: Record<string, unknown>) => ({
|
|
832
|
+
line: requireNumber(c.line, "conflict.line"),
|
|
833
|
+
column: requireNumber(c.column, "conflict.column"),
|
|
834
|
+
reason: requireString(c.reason, "conflict.reason"),
|
|
835
|
+
})) ?? [],
|
|
836
|
+
edits: (o.edits as any[] | undefined)?.map((e: Record<string, unknown>) => ({
|
|
837
|
+
line: requireNumber(e.line, "edit.line"),
|
|
838
|
+
start_column: requireNumber(e.start_column, "edit.start_column"),
|
|
839
|
+
end_column: requireNumber(e.end_column, "edit.end_column"),
|
|
840
|
+
start_byte: requireNumber(e.start_byte, "edit.start_byte"),
|
|
841
|
+
end_byte: requireNumber(e.end_byte, "edit.end_byte"),
|
|
842
|
+
occurrence: requireString(e.occurrence, "edit.occurrence"),
|
|
843
|
+
line_hash: requireString(e.line_hash, "edit.line_hash"),
|
|
844
|
+
text: requireString(e.text, "edit.text"),
|
|
845
|
+
})) ?? [],
|
|
846
|
+
})) ?? [],
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
export async function readseekRename(
|
|
851
|
+
filePath: string,
|
|
852
|
+
options: RenameOptions,
|
|
853
|
+
): Promise<RenameOutput> {
|
|
854
|
+
const args = ["rename", filePath, "--line", String(options.line)];
|
|
855
|
+
if (options.column !== undefined) args.push("--column", String(options.column));
|
|
856
|
+
args.push("--to", options.to);
|
|
857
|
+
if (options.apply) args.push("--apply");
|
|
858
|
+
if (options.workspace) args.push("--workspace", options.workspace);
|
|
859
|
+
if (options.language) args.push("--language", options.language);
|
|
860
|
+
if (options.cached) args.push("--cached");
|
|
861
|
+
if (options.others) args.push("--others");
|
|
862
|
+
if (options.ignored) args.push("--ignored");
|
|
863
|
+
return parseRenameOutput(await runReadSeek(args, { signal: options.signal }));
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// --- Identify ---
|
|
867
|
+
|
|
868
|
+
export interface IdentifierOutput {
|
|
869
|
+
text: string;
|
|
870
|
+
start_column: number;
|
|
871
|
+
end_column: number;
|
|
872
|
+
start_byte: number;
|
|
873
|
+
end_byte: number;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
export interface IdentifyOutput {
|
|
877
|
+
file: string;
|
|
878
|
+
language: string;
|
|
879
|
+
engine?: string;
|
|
880
|
+
line_count: number;
|
|
881
|
+
file_hash: string;
|
|
882
|
+
line: number;
|
|
883
|
+
column: number;
|
|
884
|
+
line_hash: string;
|
|
885
|
+
identifier?: IdentifierOutput;
|
|
886
|
+
symbol?: {
|
|
887
|
+
name: string;
|
|
888
|
+
kind: string;
|
|
889
|
+
qualified_name: string;
|
|
890
|
+
start_line: number;
|
|
891
|
+
end_line: number;
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
interface IdentifyOptions {
|
|
896
|
+
line?: number;
|
|
897
|
+
column?: number;
|
|
898
|
+
language?: string;
|
|
899
|
+
signal?: AbortSignal;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function parseIdentifyOutput(value: unknown): IdentifyOutput {
|
|
903
|
+
if (!value || typeof value !== "object") throw new Error("invalid readseek identify output");
|
|
904
|
+
const output = value as Record<string, unknown>;
|
|
905
|
+
const identifier = output.identifier;
|
|
906
|
+
const symbol = output.symbol;
|
|
907
|
+
return {
|
|
908
|
+
file: requireString(output.file, "file"),
|
|
909
|
+
language: requireString(output.language, "language"),
|
|
910
|
+
engine: optionalString(output.engine, "engine"),
|
|
911
|
+
line_count: requireNumber(output.line_count, "line_count"),
|
|
912
|
+
file_hash: requireString(output.file_hash, "file_hash"),
|
|
913
|
+
line: requireNumber(output.line, "line"),
|
|
914
|
+
column: requireNumber(output.column, "column"),
|
|
915
|
+
line_hash: requireString(output.line_hash, "line_hash"),
|
|
916
|
+
identifier: identifier && typeof identifier === "object"
|
|
917
|
+
? {
|
|
918
|
+
text: requireString((identifier as Record<string, unknown>).text, "identifier.text"),
|
|
919
|
+
start_column: requireNumber((identifier as Record<string, unknown>).start_column, "identifier.start_column"),
|
|
920
|
+
end_column: requireNumber((identifier as Record<string, unknown>).end_column, "identifier.end_column"),
|
|
921
|
+
start_byte: requireNumber((identifier as Record<string, unknown>).start_byte, "identifier.start_byte"),
|
|
922
|
+
end_byte: requireNumber((identifier as Record<string, unknown>).end_byte, "identifier.end_byte"),
|
|
923
|
+
}
|
|
924
|
+
: undefined,
|
|
925
|
+
symbol: symbol && typeof symbol === "object"
|
|
926
|
+
? {
|
|
927
|
+
name: requireString((symbol as Record<string, unknown>).name, "symbol.name"),
|
|
928
|
+
kind: requireString((symbol as Record<string, unknown>).kind, "symbol.kind"),
|
|
929
|
+
qualified_name: requireString((symbol as Record<string, unknown>).qualified_name, "symbol.qualified_name"),
|
|
930
|
+
start_line: requireNumber((symbol as Record<string, unknown>).start_line, "symbol.start_line"),
|
|
931
|
+
end_line: requireNumber((symbol as Record<string, unknown>).end_line, "symbol.end_line"),
|
|
932
|
+
}
|
|
933
|
+
: undefined,
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
export async function readseekIdentify(
|
|
938
|
+
filePath: string,
|
|
939
|
+
content: string,
|
|
940
|
+
options: IdentifyOptions = {},
|
|
941
|
+
): Promise<IdentifyOutput> {
|
|
942
|
+
const args = ["identify", "--stdin", filePath];
|
|
943
|
+
if (options.line !== undefined) args.push("--line", String(options.line));
|
|
944
|
+
if (options.column !== undefined) args.push("--column", String(options.column));
|
|
945
|
+
if (options.language) args.push("--language", options.language);
|
|
946
|
+
return parseIdentifyOutput(await runReadSeek(args, { signal: options.signal, stdin: content }));
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// --- Def ---
|
|
950
|
+
|
|
951
|
+
export interface DefLocation {
|
|
952
|
+
file: string;
|
|
953
|
+
line: number;
|
|
954
|
+
column: number;
|
|
955
|
+
line_hash: string;
|
|
956
|
+
text: string;
|
|
957
|
+
kind?: string;
|
|
958
|
+
name?: string;
|
|
959
|
+
qualified_name?: string;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
interface DefOptions {
|
|
963
|
+
name?: string;
|
|
964
|
+
fromIdentify?: boolean;
|
|
965
|
+
identifyInput?: string;
|
|
966
|
+
language?: string;
|
|
967
|
+
cached?: boolean;
|
|
968
|
+
others?: boolean;
|
|
969
|
+
ignored?: boolean;
|
|
970
|
+
signal?: AbortSignal;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
function parseDefCompact(value: unknown): DefLocation[] {
|
|
974
|
+
if (!value || typeof value !== "object") throw new Error("invalid readseek def output");
|
|
975
|
+
const output = value as Record<string, unknown>;
|
|
976
|
+
const locations = output.locations;
|
|
977
|
+
if (!Array.isArray(locations)) throw new Error("invalid readseek def locations");
|
|
978
|
+
return locations.map((loc) => {
|
|
979
|
+
if (!loc || typeof loc !== "object") throw new Error("invalid readseek def location");
|
|
980
|
+
const item = loc as Record<string, unknown>;
|
|
981
|
+
return {
|
|
982
|
+
file: requireString(item.file, "location.file"),
|
|
983
|
+
line: requireNumber(item.line, "location.line"),
|
|
984
|
+
column: requireNumber(item.column, "location.column"),
|
|
985
|
+
line_hash: requireString(item.line_hash, "location.line_hash"),
|
|
986
|
+
text: requireString(item.text, "location.text"),
|
|
987
|
+
kind: optionalString(item.kind, "location.kind"),
|
|
988
|
+
name: optionalString(item.name, "location.name"),
|
|
989
|
+
qualified_name: optionalString(item.qualified_name, "location.qualified_name"),
|
|
990
|
+
};
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
export async function readseekDef(
|
|
995
|
+
target: string,
|
|
996
|
+
options: DefOptions = {},
|
|
997
|
+
): Promise<DefLocation[]> {
|
|
998
|
+
const args = ["def", target, "--format", "plain"];
|
|
999
|
+
if (options.fromIdentify) {
|
|
1000
|
+
args.push("--from-identify");
|
|
1001
|
+
}
|
|
1002
|
+
if (options.name) {
|
|
1003
|
+
args.push(options.name);
|
|
1004
|
+
}
|
|
1005
|
+
if (options.language) args.push("--language", options.language);
|
|
1006
|
+
if (options.cached) args.push("--cached");
|
|
1007
|
+
if (options.others) args.push("--others");
|
|
1008
|
+
if (options.ignored) args.push("--ignored");
|
|
1009
|
+
const stdin = options.fromIdentify && options.identifyInput ? options.identifyInput : undefined;
|
|
1010
|
+
return parseDefCompact(await runReadSeek(args, { signal: options.signal, stdin }));
|
|
1011
|
+
}
|
package/src/readseek-settings.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { join } from "node:path";
|
|
|
5
5
|
interface ReadSeekJsonSettings {
|
|
6
6
|
grep?: { maxLines?: number; maxBytes?: number };
|
|
7
7
|
edit?: { diffDisplay?: "collapsed" | "expanded" };
|
|
8
|
+
read?: { ocrMode?: "on" | "off" | "auto" };
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
interface ReadSeekSettingsWarning {
|
|
@@ -75,6 +76,16 @@ function validateSettings(raw: unknown, source: string): ReadSeekSettingsResult
|
|
|
75
76
|
if (Object.keys(edit).length > 0) settings.edit = edit;
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
if (isRecord(raw.read)) {
|
|
80
|
+
const read: NonNullable<ReadSeekJsonSettings["read"]> = {};
|
|
81
|
+
if ("ocrMode" in raw.read) {
|
|
82
|
+
const value = raw.read.ocrMode;
|
|
83
|
+
if (value === "on" || value === "off" || value === "auto") read.ocrMode = value;
|
|
84
|
+
else warnings.push(invalid(source, "read.ocrMode"));
|
|
85
|
+
}
|
|
86
|
+
if (Object.keys(read).length > 0) settings.read = read;
|
|
87
|
+
}
|
|
88
|
+
|
|
78
89
|
return { settings, warnings };
|
|
79
90
|
}
|
|
80
91
|
|
|
@@ -96,6 +107,8 @@ function mergeSettings(base: ReadSeekJsonSettings, override: ReadSeekJsonSetting
|
|
|
96
107
|
if (Object.keys(grep).length > 0) merged.grep = grep;
|
|
97
108
|
const edit = { ...(base.edit ?? {}), ...(override.edit ?? {}) };
|
|
98
109
|
if (Object.keys(edit).length > 0) merged.edit = edit;
|
|
110
|
+
const read = { ...(base.read ?? {}), ...(override.read ?? {}) };
|
|
111
|
+
if (Object.keys(read).length > 0) merged.read = read;
|
|
99
112
|
return merged;
|
|
100
113
|
}
|
|
101
114
|
|
|
@@ -118,3 +131,14 @@ export function resolveEditDiffDisplay(env: NodeJS.ProcessEnv = process.env): "c
|
|
|
118
131
|
if (json === "expanded" || json === "collapsed") return json;
|
|
119
132
|
return "collapsed";
|
|
120
133
|
}
|
|
134
|
+
|
|
135
|
+
export function resolveReadSeekOcrMode(env: NodeJS.ProcessEnv = process.env): "on" | "off" | "auto" {
|
|
136
|
+
const raw = env.READSEEK_READ_OCR_MODE;
|
|
137
|
+
if (typeof raw === "string") {
|
|
138
|
+
const normalized = raw.trim().toLowerCase();
|
|
139
|
+
if (normalized === "on" || normalized === "off" || normalized === "auto") return normalized;
|
|
140
|
+
}
|
|
141
|
+
const json = resolveReadSeekJsonSettings().settings.read?.ocrMode;
|
|
142
|
+
if (json === "on" || json === "off" || json === "auto") return json;
|
|
143
|
+
return "on";
|
|
144
|
+
}
|
package/src/rename.ts
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import type { ExtensionAPI, ToolRenderResultOptions } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "@sinclair/typebox";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
5
|
+
|
|
6
|
+
import { defineToolPromptMetadata } from "./tool-prompt-metadata.js";
|
|
7
|
+
import { buildToolErrorResult } from "./readseek-value.js";
|
|
8
|
+
import { resolveToCwd } from "./path-utils.js";
|
|
9
|
+
import { classifyReadSeekFailure, readseekRename, type RenameOutput } from "./readseek-client.js";
|
|
10
|
+
import { filePathParam, registerReadSeekTool } from "./register-tool.js";
|
|
11
|
+
|
|
12
|
+
import { clampLinesToWidth, linkToolPath, renderPendingResult, resolveRenderResultContext, summaryLine } from "./tui-render-utils.js";
|
|
13
|
+
|
|
14
|
+
const RENAME_PROMPT_METADATA = defineToolPromptMetadata({
|
|
15
|
+
promptUrl: new URL("../prompts/rename.md", import.meta.url),
|
|
16
|
+
promptSnippet: "Rename an identifier with binding-accurate readseek rename",
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const renameSchema = Type.Object({
|
|
20
|
+
path: filePathParam(),
|
|
21
|
+
line: Type.Number({ description: "One-based cursor line of the binding to rename" }),
|
|
22
|
+
column: Type.Optional(Type.Number({ description: "One-based cursor byte column of the binding to rename" })),
|
|
23
|
+
to: Type.String({ description: "New name for the binding" }),
|
|
24
|
+
workspace: Type.Optional(Type.Boolean({ description: "Expand rename across project root (name-based outside cursor file)" })),
|
|
25
|
+
apply: Type.Optional(Type.Boolean({ description: "Write the planned edits to disk after verifying line hashes" })),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
interface RenameParams {
|
|
29
|
+
path: string;
|
|
30
|
+
line: number;
|
|
31
|
+
column?: number;
|
|
32
|
+
to: string;
|
|
33
|
+
workspace?: boolean;
|
|
34
|
+
apply?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ExecuteRenameOptions {
|
|
38
|
+
params: unknown;
|
|
39
|
+
signal: AbortSignal | undefined;
|
|
40
|
+
cwd: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function executeRename(opts: ExecuteRenameOptions): Promise<any> {
|
|
44
|
+
const { params, signal, cwd } = opts;
|
|
45
|
+
const p = params as RenameParams;
|
|
46
|
+
|
|
47
|
+
if (!p.to.trim()) {
|
|
48
|
+
return buildToolErrorResult("rename", "invalid-parameter", "rename parameter 'to' must not be empty");
|
|
49
|
+
}
|
|
50
|
+
if (!Number.isSafeInteger(p.line) || p.line < 1) {
|
|
51
|
+
return buildToolErrorResult("rename", "invalid-parameter", "rename parameter 'line' must be a positive integer");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const filePath = resolveToCwd(p.path, cwd);
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const output = await readseekRename(filePath, {
|
|
58
|
+
to: p.to,
|
|
59
|
+
line: p.line,
|
|
60
|
+
column: p.column,
|
|
61
|
+
workspace: p.workspace ? cwd : undefined,
|
|
62
|
+
apply: p.apply ?? true,
|
|
63
|
+
signal,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const files: string[] = [output.file];
|
|
67
|
+
for (const other of output.others) {
|
|
68
|
+
const abs = path.isAbsolute(other.file) ? other.file : path.resolve(cwd, other.file);
|
|
69
|
+
if (!files.includes(abs)) files.push(abs);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const totalEdits = output.edits.length + output.others.reduce((sum, o) => sum + o.edits.length, 0);
|
|
73
|
+
const totalConflicts = output.conflicts.length + output.others.reduce((sum, o) => sum + o.conflicts.length, 0);
|
|
74
|
+
|
|
75
|
+
const parts: string[] = [];
|
|
76
|
+
if (totalConflicts > 0) {
|
|
77
|
+
parts.push(`${totalConflicts} naming conflict(s)`);
|
|
78
|
+
}
|
|
79
|
+
parts.push(`renamed ${output.old_name} to ${output.new_name} in ${totalEdits} location(s) across ${files.length} file(s)`);
|
|
80
|
+
|
|
81
|
+
let text = parts.join("; ");
|
|
82
|
+
if (output.applied) text += " (applied)";
|
|
83
|
+
else text += " (dry-run)";
|
|
84
|
+
|
|
85
|
+
// Surface first conflict reason for visibility
|
|
86
|
+
const firstConflict = output.conflicts[0]
|
|
87
|
+
?? output.others.find((o) => o.conflicts.length > 0)?.conflicts[0];
|
|
88
|
+
if (firstConflict) {
|
|
89
|
+
text += `\nFirst conflict at ${firstConflict.line}:${firstConflict.column}: ${firstConflict.reason}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
content: [{ type: "text", text }],
|
|
94
|
+
details: {
|
|
95
|
+
readseekValue: {
|
|
96
|
+
tool: "rename",
|
|
97
|
+
ok: true,
|
|
98
|
+
path: filePath,
|
|
99
|
+
output,
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
} catch (err: any) {
|
|
104
|
+
const failure = classifyReadSeekFailure(err);
|
|
105
|
+
return buildToolErrorResult("rename", failure.code, failure.message, failure.hint ? { hint: failure.hint } : {});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function registerRenameTool(pi: ExtensionAPI) {
|
|
110
|
+
registerReadSeekTool(pi, {
|
|
111
|
+
policy: "mutating",
|
|
112
|
+
pythonName: "rename",
|
|
113
|
+
defaultExposure: "not-safe-by-default",
|
|
114
|
+
}, {
|
|
115
|
+
name: "rename",
|
|
116
|
+
label: "Rename",
|
|
117
|
+
description: RENAME_PROMPT_METADATA.description,
|
|
118
|
+
promptSnippet: RENAME_PROMPT_METADATA.promptSnippet,
|
|
119
|
+
promptGuidelines: RENAME_PROMPT_METADATA.promptGuidelines,
|
|
120
|
+
parameters: renameSchema,
|
|
121
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
122
|
+
return executeRename({ params, signal, cwd: ctx.cwd });
|
|
123
|
+
},
|
|
124
|
+
renderCall(args: any, theme: any, ...rest: any[]) {
|
|
125
|
+
const context = rest[0] ?? {};
|
|
126
|
+
const cwd = context.cwd ?? process.cwd();
|
|
127
|
+
const displayPath = typeof args?.path === "string" ? args.path : "?";
|
|
128
|
+
const oldName = typeof args?.to === "string" ? "" : "";
|
|
129
|
+
let text = theme.fg("toolTitle", theme.bold("rename"));
|
|
130
|
+
text += ` ${linkToolPath(theme.fg("accent", displayPath), displayPath, cwd)}`;
|
|
131
|
+
if (args?.to) text += theme.fg("dim", ` → ${args.to}`);
|
|
132
|
+
return text;
|
|
133
|
+
},
|
|
134
|
+
renderResult(result: any, options: ToolRenderResultOptions, theme: any, ...rest: any[]) {
|
|
135
|
+
const { isPartial, isError, expanded, width } = resolveRenderResultContext(options, rest);
|
|
136
|
+
|
|
137
|
+
if (isPartial) return renderPendingResult("pending rename", width);
|
|
138
|
+
|
|
139
|
+
const content = result.content?.[0];
|
|
140
|
+
const textContent = content?.type === "text" ? content.text : "";
|
|
141
|
+
const readseekValue = (result.details as any)?.readseekValue;
|
|
142
|
+
const output = readseekValue?.output as RenameOutput | undefined;
|
|
143
|
+
|
|
144
|
+
if (isError || result.isError) {
|
|
145
|
+
return new Text(textContent || "rename failed", 0, 0);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let text = summaryLine(
|
|
149
|
+
output?.applied
|
|
150
|
+
? `renamed ${output.old_name} → ${output.new_name}`
|
|
151
|
+
: `rename plan for ${output?.old_name ?? "?"} → ${output?.new_name ?? "?"} (dry-run)`,
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
if (expanded && output) {
|
|
155
|
+
const files = new Set<string>();
|
|
156
|
+
files.add(output.file);
|
|
157
|
+
for (const o of output.others) files.add(o.file);
|
|
158
|
+
for (const f of files) {
|
|
159
|
+
text += `\n ${f}`;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const lines = clampLinesToWidth(text.split("\n"), width);
|
|
164
|
+
return new Text(lines.join("\n"), 0, 0);
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
}
|
package/src/stat-search-path.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { stat as fsStat } from "node:fs/promises";
|
|
|
2
2
|
import type { Stats } from "node:fs";
|
|
3
3
|
|
|
4
4
|
import { buildToolErrorResult, type ToolErrorResult } from "./readseek-value.js";
|
|
5
|
+
import { formatFsError } from "./fs-error.js";
|
|
5
6
|
|
|
6
7
|
export type StatSearchPathResult =
|
|
7
8
|
| { ok: true; stats: Stats }
|
|
@@ -22,13 +23,7 @@ export async function statSearchPathOrError(
|
|
|
22
23
|
} catch (err: any) {
|
|
23
24
|
const display = rawPath ?? ".";
|
|
24
25
|
const path = rawPath ?? searchPath;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
if (err?.code === "EACCES" || err?.code === "EPERM") {
|
|
29
|
-
return { ok: false, error: buildToolErrorResult(tool, "permission-denied", `Permission denied for path '${display}'`, { path }) };
|
|
30
|
-
}
|
|
31
|
-
const message = `Could not access path '${display}': ${err?.message ?? String(err)}`;
|
|
32
|
-
return { ok: false, error: buildToolErrorResult(tool, "fs-error", message, { path, details: { fsCode: err?.code, fsMessage: err?.message } }) };
|
|
26
|
+
const { code, message } = formatFsError(err, "stat-error");
|
|
27
|
+
return { ok: false, error: buildToolErrorResult(tool, code, message, { path }) };
|
|
33
28
|
}
|
|
34
29
|
}
|
package/src/write.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { resolveToCwd } from "./path-utils.js";
|
|
|
9
9
|
import { ensureHashInit, formatHashlineDisplay } from "./hashline.js";
|
|
10
10
|
import { buildReadSeekError, buildReadSeekLine, buildReadSeekWarning, buildToolErrorResult, type ReadSeekLine, type ReadSeekWarning } from "./readseek-value.js";
|
|
11
11
|
import { looksLikeBinary } from "./binary-detect.js";
|
|
12
|
-
import {
|
|
12
|
+
import { formatFsError } from "./fs-error.js";
|
|
13
13
|
import { getOrGenerateMap } from "./file-map.js";
|
|
14
14
|
import { formatFileMapWithBudget } from "./readseek/formatter.js";
|
|
15
15
|
|
|
@@ -120,57 +120,12 @@ export interface WriteToolOptions {
|
|
|
120
120
|
onFileAnchored?: FileAnchoredCallback;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
type MappedFsError = {
|
|
124
|
-
code: FsErrorCode | "fs-error";
|
|
125
|
-
message: string;
|
|
126
|
-
hint?: string;
|
|
127
|
-
includeMeta: boolean;
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
function mapFsWriteError(err: any, path: string): MappedFsError {
|
|
131
|
-
const phase: "mkdir" | "write" | undefined = err?.__phase;
|
|
132
|
-
const fsCode = err?.code as string | undefined;
|
|
133
|
-
|
|
134
|
-
if (fsCode === "ENOENT" && phase === "mkdir") {
|
|
135
|
-
return {
|
|
136
|
-
code: "fs-error",
|
|
137
|
-
message: `Cannot create parent directories for ${path}: ${err?.message ?? String(err)}`,
|
|
138
|
-
includeMeta: true,
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
if (fsCode === "ENOSPC") {
|
|
142
|
-
return {
|
|
143
|
-
code: "fs-error",
|
|
144
|
-
message: `No space left on device — cannot write: ${path}`,
|
|
145
|
-
includeMeta: true,
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
if (fsCode === "EROFS") {
|
|
149
|
-
return {
|
|
150
|
-
code: "fs-error",
|
|
151
|
-
message: `Read-only filesystem — cannot write: ${path}`,
|
|
152
|
-
includeMeta: true,
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
const fsError = classifyFsError(err, path);
|
|
156
|
-
if (fsError) {
|
|
157
|
-
return { code: fsError.code, message: fsError.message, hint: fsError.hint, includeMeta: false };
|
|
158
|
-
}
|
|
159
|
-
return {
|
|
160
|
-
code: "fs-error",
|
|
161
|
-
message: `Error writing ${path}: ${err?.message ?? String(err)}`,
|
|
162
|
-
includeMeta: true,
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
|
|
166
123
|
function buildWriteFsErrorResult(err: any, absolutePath: string) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
details: mapped.includeMeta ? { fsCode: err?.code, fsMessage: err?.message } : undefined,
|
|
173
|
-
});
|
|
124
|
+
const { code, message } = formatFsError(err, "write-error");
|
|
125
|
+
return buildToolErrorResult("write", code, message, {
|
|
126
|
+
path: absolutePath,
|
|
127
|
+
extra: { lines: [], warnings: [] },
|
|
128
|
+
});
|
|
174
129
|
}
|
|
175
130
|
|
|
176
131
|
export async function executeWrite(opts: {
|
|
@@ -218,21 +173,10 @@ export async function executeWrite(opts: {
|
|
|
218
173
|
const previousContent = await readPreviousTextForDiff(filePath);
|
|
219
174
|
const existedBeforeWrite = await access(filePath).then(() => true, () => false);
|
|
220
175
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
err.__phase = "mkdir";
|
|
226
|
-
throw err;
|
|
227
|
-
}
|
|
228
|
-
// Write file
|
|
229
|
-
try {
|
|
230
|
-
await writeFile(filePath, content, "utf-8");
|
|
231
|
-
} catch (err: any) {
|
|
232
|
-
err.__phase = "write";
|
|
233
|
-
throw err;
|
|
234
|
-
}
|
|
235
|
-
|
|
176
|
+
// Create parent directories
|
|
177
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
178
|
+
// Write file
|
|
179
|
+
await writeFile(filePath, content, "utf-8");
|
|
236
180
|
// Compute hashlines
|
|
237
181
|
const rawLines = content.split("\n");
|
|
238
182
|
const readseekLines: ReadSeekLine[] = [];
|