@oh-my-pi/pi-coding-agent 3.6.1337 → 3.8.1337
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 +27 -0
- package/package.json +4 -4
- package/src/core/bash-executor.ts +115 -154
- package/src/core/index.ts +2 -0
- package/src/core/session-manager.ts +16 -6
- package/src/core/tools/edit-diff.ts +45 -33
- package/src/core/tools/edit.ts +70 -182
- package/src/core/tools/find.ts +141 -160
- package/src/core/tools/index.ts +10 -9
- package/src/core/tools/ls.ts +64 -82
- package/src/core/tools/lsp/client.ts +63 -0
- package/src/core/tools/lsp/edits.ts +13 -4
- package/src/core/tools/lsp/index.ts +191 -85
- package/src/core/tools/notebook.ts +89 -144
- package/src/core/tools/read.ts +110 -158
- package/src/core/tools/write.ts +22 -115
- package/src/core/utils.ts +187 -0
- package/src/modes/interactive/components/tool-execution.ts +14 -14
- package/src/modes/interactive/interactive-mode.ts +23 -54
package/src/core/tools/index.ts
CHANGED
|
@@ -11,8 +11,6 @@ export {
|
|
|
11
11
|
createLspTool,
|
|
12
12
|
type FileDiagnosticsResult,
|
|
13
13
|
type FileFormatResult,
|
|
14
|
-
formatFile,
|
|
15
|
-
getDiagnosticsForFile,
|
|
16
14
|
getLspStatus,
|
|
17
15
|
type LspServerStatus,
|
|
18
16
|
type LspToolDetails,
|
|
@@ -61,7 +59,7 @@ import { createEditTool, editTool } from "./edit";
|
|
|
61
59
|
import { createFindTool, findTool } from "./find";
|
|
62
60
|
import { createGrepTool, grepTool } from "./grep";
|
|
63
61
|
import { createLsTool, lsTool } from "./ls";
|
|
64
|
-
import { createLspTool,
|
|
62
|
+
import { createLspTool, createLspWritethrough, lspTool } from "./lsp/index";
|
|
65
63
|
import { createNotebookTool, notebookTool } from "./notebook";
|
|
66
64
|
import { createOutputTool, outputTool } from "./output";
|
|
67
65
|
import { createReadTool, readTool } from "./read";
|
|
@@ -105,10 +103,12 @@ const toolDefs: Record<string, { tool: Tool; create: ToolFactory }> = {
|
|
|
105
103
|
tool: editTool,
|
|
106
104
|
create: (cwd, _ctx, options) => {
|
|
107
105
|
const enableDiagnostics = options?.lspDiagnosticsOnEdit ?? false;
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
106
|
+
const enableFormat = options?.lspFormatOnWrite ?? true;
|
|
107
|
+
const writethrough = createLspWritethrough(cwd, {
|
|
108
|
+
enableFormat,
|
|
109
|
+
enableDiagnostics,
|
|
111
110
|
});
|
|
111
|
+
return createEditTool(cwd, { fuzzyMatch: options?.editFuzzyMatch ?? true, writethrough });
|
|
112
112
|
},
|
|
113
113
|
},
|
|
114
114
|
write: {
|
|
@@ -116,10 +116,11 @@ const toolDefs: Record<string, { tool: Tool; create: ToolFactory }> = {
|
|
|
116
116
|
create: (cwd, _ctx, options) => {
|
|
117
117
|
const enableFormat = options?.lspFormatOnWrite ?? true;
|
|
118
118
|
const enableDiagnostics = options?.lspDiagnosticsOnWrite ?? true;
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
119
|
+
const writethrough = createLspWritethrough(cwd, {
|
|
120
|
+
enableFormat,
|
|
121
|
+
enableDiagnostics,
|
|
122
122
|
});
|
|
123
|
+
return createWriteTool(cwd, { writethrough });
|
|
123
124
|
},
|
|
124
125
|
},
|
|
125
126
|
grep: { tool: grepTool, create: createGrepTool },
|
package/src/core/tools/ls.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync, readdirSync, statSync } from "node:fs";
|
|
|
2
2
|
import nodePath from "node:path";
|
|
3
3
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
4
4
|
import { Type } from "@sinclair/typebox";
|
|
5
|
+
import { untilAborted } from "../utils";
|
|
5
6
|
import { resolveToCwd } from "./path-utils";
|
|
6
7
|
import { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from "./truncate";
|
|
7
8
|
|
|
@@ -28,109 +29,90 @@ export function createLsTool(cwd: string): AgentTool<typeof lsSchema> {
|
|
|
28
29
|
{ path, limit }: { path?: string; limit?: number },
|
|
29
30
|
signal?: AbortSignal,
|
|
30
31
|
) => {
|
|
31
|
-
return
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
return untilAborted(signal, async () => {
|
|
33
|
+
const dirPath = resolveToCwd(path || ".", cwd);
|
|
34
|
+
const effectiveLimit = limit ?? DEFAULT_LIMIT;
|
|
35
|
+
|
|
36
|
+
// Check if path exists
|
|
37
|
+
if (!existsSync(dirPath)) {
|
|
38
|
+
throw new Error(`Path not found: ${dirPath}`);
|
|
35
39
|
}
|
|
36
40
|
|
|
37
|
-
|
|
38
|
-
|
|
41
|
+
// Check if path is a directory
|
|
42
|
+
const stat = statSync(dirPath);
|
|
43
|
+
if (!stat.isDirectory()) {
|
|
44
|
+
throw new Error(`Not a directory: ${dirPath}`);
|
|
45
|
+
}
|
|
39
46
|
|
|
47
|
+
// Read directory entries
|
|
48
|
+
let entries: string[];
|
|
40
49
|
try {
|
|
41
|
-
|
|
42
|
-
|
|
50
|
+
entries = readdirSync(dirPath);
|
|
51
|
+
} catch (e: any) {
|
|
52
|
+
throw new Error(`Cannot read directory: ${e.message}`);
|
|
53
|
+
}
|
|
43
54
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
reject(new Error(`Path not found: ${dirPath}`));
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
55
|
+
// Sort alphabetically (case-insensitive)
|
|
56
|
+
entries.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
|
|
49
57
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
reject(new Error(`Not a directory: ${dirPath}`));
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
58
|
+
// Format entries with directory indicators
|
|
59
|
+
const results: string[] = [];
|
|
60
|
+
let entryLimitReached = false;
|
|
56
61
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
} catch (e: any) {
|
|
62
|
-
reject(new Error(`Cannot read directory: ${e.message}`));
|
|
63
|
-
return;
|
|
62
|
+
for (const entry of entries) {
|
|
63
|
+
if (results.length >= effectiveLimit) {
|
|
64
|
+
entryLimitReached = true;
|
|
65
|
+
break;
|
|
64
66
|
}
|
|
65
67
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
// Format entries with directory indicators
|
|
70
|
-
const results: string[] = [];
|
|
71
|
-
let entryLimitReached = false;
|
|
72
|
-
|
|
73
|
-
for (const entry of entries) {
|
|
74
|
-
if (results.length >= effectiveLimit) {
|
|
75
|
-
entryLimitReached = true;
|
|
76
|
-
break;
|
|
77
|
-
}
|
|
68
|
+
const fullPath = nodePath.join(dirPath, entry);
|
|
69
|
+
let suffix = "";
|
|
78
70
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const entryStat = statSync(fullPath);
|
|
84
|
-
if (entryStat.isDirectory()) {
|
|
85
|
-
suffix = "/";
|
|
86
|
-
}
|
|
87
|
-
} catch {
|
|
88
|
-
// Skip entries we can't stat
|
|
89
|
-
continue;
|
|
71
|
+
try {
|
|
72
|
+
const entryStat = statSync(fullPath);
|
|
73
|
+
if (entryStat.isDirectory()) {
|
|
74
|
+
suffix = "/";
|
|
90
75
|
}
|
|
91
|
-
|
|
92
|
-
|
|
76
|
+
} catch {
|
|
77
|
+
// Skip entries we can't stat
|
|
78
|
+
continue;
|
|
93
79
|
}
|
|
94
80
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (results.length === 0) {
|
|
98
|
-
resolve({ content: [{ type: "text", text: "(empty directory)" }], details: undefined });
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
81
|
+
results.push(entry + suffix);
|
|
82
|
+
}
|
|
101
83
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
84
|
+
if (results.length === 0) {
|
|
85
|
+
return { content: [{ type: "text", text: "(empty directory)" }], details: undefined };
|
|
86
|
+
}
|
|
105
87
|
|
|
106
|
-
|
|
107
|
-
|
|
88
|
+
// Apply byte truncation (no line limit since we already have entry limit)
|
|
89
|
+
const rawOutput = results.join("\n");
|
|
90
|
+
const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
|
|
108
91
|
|
|
109
|
-
|
|
110
|
-
|
|
92
|
+
let output = truncation.content;
|
|
93
|
+
const details: LsToolDetails = {};
|
|
111
94
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
details.entryLimitReached = effectiveLimit;
|
|
115
|
-
}
|
|
95
|
+
// Build notices
|
|
96
|
+
const notices: string[] = [];
|
|
116
97
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
98
|
+
if (entryLimitReached) {
|
|
99
|
+
notices.push(`${effectiveLimit} entries limit reached. Use limit=${effectiveLimit * 2} for more`);
|
|
100
|
+
details.entryLimitReached = effectiveLimit;
|
|
101
|
+
}
|
|
121
102
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
103
|
+
if (truncation.truncated) {
|
|
104
|
+
notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);
|
|
105
|
+
details.truncation = truncation;
|
|
106
|
+
}
|
|
125
107
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
details: Object.keys(details).length > 0 ? details : undefined,
|
|
129
|
-
});
|
|
130
|
-
} catch (e: any) {
|
|
131
|
-
signal?.removeEventListener("abort", onAbort);
|
|
132
|
-
reject(e);
|
|
108
|
+
if (notices.length > 0) {
|
|
109
|
+
output += `\n\n[${notices.join(". ")}]`;
|
|
133
110
|
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
content: [{ type: "text", text: output }],
|
|
114
|
+
details: Object.keys(details).length > 0 ? details : undefined,
|
|
115
|
+
};
|
|
134
116
|
});
|
|
135
117
|
},
|
|
136
118
|
};
|
|
@@ -512,6 +512,69 @@ export async function ensureFileOpen(client: LspClient, filePath: string): Promi
|
|
|
512
512
|
}
|
|
513
513
|
}
|
|
514
514
|
|
|
515
|
+
/**
|
|
516
|
+
* Sync in-memory content to the LSP client without reading from disk.
|
|
517
|
+
* Use this to provide instant feedback during edits before the file is saved.
|
|
518
|
+
*/
|
|
519
|
+
export async function syncContent(client: LspClient, filePath: string, content: string): Promise<void> {
|
|
520
|
+
const uri = fileToUri(filePath);
|
|
521
|
+
const lockKey = `${client.name}:${uri}`;
|
|
522
|
+
|
|
523
|
+
const existingLock = fileOperationLocks.get(lockKey);
|
|
524
|
+
if (existingLock) {
|
|
525
|
+
await existingLock;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const syncPromise = (async () => {
|
|
529
|
+
const info = client.openFiles.get(uri);
|
|
530
|
+
|
|
531
|
+
if (!info) {
|
|
532
|
+
// Open file with provided content instead of reading from disk
|
|
533
|
+
const languageId = detectLanguageId(filePath);
|
|
534
|
+
await sendNotification(client, "textDocument/didOpen", {
|
|
535
|
+
textDocument: {
|
|
536
|
+
uri,
|
|
537
|
+
languageId,
|
|
538
|
+
version: 1,
|
|
539
|
+
text: content,
|
|
540
|
+
},
|
|
541
|
+
});
|
|
542
|
+
client.openFiles.set(uri, { version: 1, languageId });
|
|
543
|
+
client.lastActivity = Date.now();
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const version = ++info.version;
|
|
548
|
+
await sendNotification(client, "textDocument/didChange", {
|
|
549
|
+
textDocument: { uri, version },
|
|
550
|
+
contentChanges: [{ text: content }],
|
|
551
|
+
});
|
|
552
|
+
client.lastActivity = Date.now();
|
|
553
|
+
})();
|
|
554
|
+
|
|
555
|
+
fileOperationLocks.set(lockKey, syncPromise);
|
|
556
|
+
try {
|
|
557
|
+
await syncPromise;
|
|
558
|
+
} finally {
|
|
559
|
+
fileOperationLocks.delete(lockKey);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Notify LSP that a file was saved.
|
|
565
|
+
* Assumes content was already synced via syncContent - just sends didSave.
|
|
566
|
+
*/
|
|
567
|
+
export async function notifySaved(client: LspClient, filePath: string): Promise<void> {
|
|
568
|
+
const uri = fileToUri(filePath);
|
|
569
|
+
const info = client.openFiles.get(uri);
|
|
570
|
+
if (!info) return; // File not open, nothing to notify
|
|
571
|
+
|
|
572
|
+
await sendNotification(client, "textDocument/didSave", {
|
|
573
|
+
textDocument: { uri },
|
|
574
|
+
});
|
|
575
|
+
client.lastActivity = Date.now();
|
|
576
|
+
}
|
|
577
|
+
|
|
515
578
|
/**
|
|
516
579
|
* Refresh a file in the LSP client.
|
|
517
580
|
* Increments version, sends didChange and didSave notifications.
|
|
@@ -8,11 +8,10 @@ import { uriToFile } from "./utils";
|
|
|
8
8
|
// =============================================================================
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* Apply text edits to a
|
|
11
|
+
* Apply text edits to a string in-memory.
|
|
12
12
|
* Edits are applied in reverse order (bottom-to-top) to preserve line/character indices.
|
|
13
13
|
*/
|
|
14
|
-
export
|
|
15
|
-
const content = await Bun.file(filePath).text();
|
|
14
|
+
export function applyTextEditsToString(content: string, edits: TextEdit[]): string {
|
|
16
15
|
const lines = content.split("\n");
|
|
17
16
|
|
|
18
17
|
// Sort edits in reverse order (bottom-to-top, right-to-left)
|
|
@@ -39,7 +38,17 @@ export async function applyTextEdits(filePath: string, edits: TextEdit[]): Promi
|
|
|
39
38
|
}
|
|
40
39
|
}
|
|
41
40
|
|
|
42
|
-
|
|
41
|
+
return lines.join("\n");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Apply text edits to a file.
|
|
46
|
+
* Edits are applied in reverse order (bottom-to-top) to preserve line/character indices.
|
|
47
|
+
*/
|
|
48
|
+
export async function applyTextEdits(filePath: string, edits: TextEdit[]): Promise<void> {
|
|
49
|
+
const content = await Bun.file(filePath).text();
|
|
50
|
+
const result = applyTextEditsToString(content, edits);
|
|
51
|
+
await Bun.write(filePath, result);
|
|
43
52
|
}
|
|
44
53
|
|
|
45
54
|
// =============================================================================
|