@jx-grxf/patchpilot 0.4.0 → 1.0.0
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/.env.example +17 -1
- package/README.md +69 -14
- package/SECURITY.md +7 -1
- package/dist/cli.js +59 -13
- package/dist/cli.js.map +1 -1
- package/dist/core/agent.d.ts +3 -0
- package/dist/core/agent.js +56 -12
- package/dist/core/agent.js.map +1 -1
- package/dist/core/cleanup.d.ts +3 -0
- package/dist/core/cleanup.js +29 -0
- package/dist/core/cleanup.js.map +1 -0
- package/dist/core/doctor.d.ts +4 -1
- package/dist/core/doctor.js +119 -1
- package/dist/core/doctor.js.map +1 -1
- package/dist/core/geminiWrapper.d.ts +51 -0
- package/dist/core/geminiWrapper.js +718 -0
- package/dist/core/geminiWrapper.js.map +1 -0
- package/dist/core/json.js +65 -1
- package/dist/core/json.js.map +1 -1
- package/dist/core/memory.d.ts +16 -0
- package/dist/core/memory.js +108 -0
- package/dist/core/memory.js.map +1 -0
- package/dist/core/modelClient.js +7 -0
- package/dist/core/modelClient.js.map +1 -1
- package/dist/core/nvidia.js +1 -1
- package/dist/core/nvidia.js.map +1 -1
- package/dist/core/projectInit.d.ts +6 -0
- package/dist/core/projectInit.js +44 -0
- package/dist/core/projectInit.js.map +1 -0
- package/dist/core/reasoning.js +3 -0
- package/dist/core/reasoning.js.map +1 -1
- package/dist/core/session.d.ts +1 -0
- package/dist/core/session.js +46 -0
- package/dist/core/session.js.map +1 -1
- package/dist/core/types.d.ts +9 -4
- package/dist/core/workspace.d.ts +8 -0
- package/dist/core/workspace.js +293 -21
- package/dist/core/workspace.js.map +1 -1
- package/dist/tui/App.js +536 -69
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/commands.js +35 -6
- package/dist/tui/commands.js.map +1 -1
- package/dist/tui/components/CommandSuggestions.js +8 -3
- package/dist/tui/components/CommandSuggestions.js.map +1 -1
- package/dist/tui/components/Composer.js +1 -1
- package/dist/tui/components/Composer.js.map +1 -1
- package/dist/tui/components/ExperimentalPanel.d.ts +10 -0
- package/dist/tui/components/ExperimentalPanel.js +33 -0
- package/dist/tui/components/ExperimentalPanel.js.map +1 -0
- package/dist/tui/components/Header.js +3 -3
- package/dist/tui/components/Header.js.map +1 -1
- package/dist/tui/components/OnboardingPanel.d.ts +13 -1
- package/dist/tui/components/OnboardingPanel.js +23 -9
- package/dist/tui/components/OnboardingPanel.js.map +1 -1
- package/dist/tui/components/Sidebar.js +17 -13
- package/dist/tui/components/Sidebar.js.map +1 -1
- package/dist/tui/components/Transcript.js +2 -2
- package/dist/tui/components/Transcript.js.map +1 -1
- package/dist/tui/format.js +7 -7
- package/dist/tui/format.js.map +1 -1
- package/dist/tui/modes.d.ts +1 -1
- package/dist/tui/modes.js +8 -2
- package/dist/tui/modes.js.map +1 -1
- package/docs/gemini-wrapper.md +87 -0
- package/docs/releases/v0.1.1-beta.md +18 -0
- package/docs/releases/v0.2.1.md +1 -1
- package/docs/releases/v0.3.1-beta.md +4 -0
- package/docs/releases/v0.4.0.md +1 -1
- package/docs/releases/v1.0.0.md +28 -0
- package/docs/showcase/patchpilot-banner.png +0 -0
- package/docs/showcase/patchpilot-logo.png +0 -0
- package/package.json +5 -2
package/dist/core/workspace.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ export type WorkspaceToolsOptions = {
|
|
|
3
3
|
root: string;
|
|
4
4
|
allowWrite: boolean;
|
|
5
5
|
allowShell: boolean;
|
|
6
|
+
allowExternalFileAnalysis?: boolean;
|
|
7
|
+
memoryEnabled?: boolean;
|
|
6
8
|
timeoutMs?: number;
|
|
7
9
|
signal?: AbortSignal;
|
|
8
10
|
approvalHandler?: (request: ApprovalRequest) => Promise<PermissionDecision>;
|
|
@@ -14,6 +16,8 @@ export declare class WorkspaceTools {
|
|
|
14
16
|
private readonly rootRealPath;
|
|
15
17
|
private readonly allowWrite;
|
|
16
18
|
private readonly allowShell;
|
|
19
|
+
private readonly allowExternalFileAnalysis;
|
|
20
|
+
private readonly memoryEnabled;
|
|
17
21
|
private readonly timeoutMs;
|
|
18
22
|
private readonly signal?;
|
|
19
23
|
private readonly approvalHandler?;
|
|
@@ -27,6 +31,9 @@ export declare class WorkspaceTools {
|
|
|
27
31
|
private readRange;
|
|
28
32
|
private fileInfo;
|
|
29
33
|
private inspectDocument;
|
|
34
|
+
private readTextDocument;
|
|
35
|
+
private memoryRemember;
|
|
36
|
+
private memorySearch;
|
|
30
37
|
private searchText;
|
|
31
38
|
private writeFile;
|
|
32
39
|
private gitStatus;
|
|
@@ -40,5 +47,6 @@ export declare class WorkspaceTools {
|
|
|
40
47
|
private readPackageScripts;
|
|
41
48
|
private requestApproval;
|
|
42
49
|
private resolveReadPath;
|
|
50
|
+
private resolveDocumentPath;
|
|
43
51
|
private resolveWritePath;
|
|
44
52
|
}
|
package/dist/core/workspace.js
CHANGED
|
@@ -5,6 +5,7 @@ import { platform } from "node:os";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
7
|
import { inflateRawSync } from "node:zlib";
|
|
8
|
+
import { MemoryStore } from "./memory.js";
|
|
8
9
|
const execFileAsync = promisify(execFile);
|
|
9
10
|
const ignoredDirectories = new Set([
|
|
10
11
|
".git",
|
|
@@ -73,6 +74,11 @@ const blockedPathNames = new Set([
|
|
|
73
74
|
"id_dsa",
|
|
74
75
|
"known_hosts"
|
|
75
76
|
]);
|
|
77
|
+
const blockedPathPatterns = [
|
|
78
|
+
/(^|\/)(cookies|network\/cookies|login data|web data)$/i,
|
|
79
|
+
/(^|\/)(chrome|chromium|brave-browser|brave|microsoft edge|edge|arc|firefox|safari)(\/|$)/i,
|
|
80
|
+
/(^|\/)(default|profile \d+|profiles?)\/(cookies|network\/cookies|login data|web data)$/i
|
|
81
|
+
];
|
|
76
82
|
export const toolSpecs = {
|
|
77
83
|
list_files: {
|
|
78
84
|
name: "list_files",
|
|
@@ -122,6 +128,22 @@ export const toolSpecs = {
|
|
|
122
128
|
permission: "none",
|
|
123
129
|
category: "document"
|
|
124
130
|
},
|
|
131
|
+
memory_remember: {
|
|
132
|
+
name: "memory_remember",
|
|
133
|
+
description: "Store a durable memory for this workspace.",
|
|
134
|
+
risk: "low",
|
|
135
|
+
sideEffects: "write",
|
|
136
|
+
permission: "write",
|
|
137
|
+
category: "memory"
|
|
138
|
+
},
|
|
139
|
+
memory_search: {
|
|
140
|
+
name: "memory_search",
|
|
141
|
+
description: "Search durable workspace memories.",
|
|
142
|
+
risk: "low",
|
|
143
|
+
sideEffects: "none",
|
|
144
|
+
permission: "none",
|
|
145
|
+
category: "memory"
|
|
146
|
+
},
|
|
125
147
|
git_status: {
|
|
126
148
|
name: "git_status",
|
|
127
149
|
description: "Read the current Git branch and dirty state.",
|
|
@@ -203,6 +225,8 @@ export class WorkspaceTools {
|
|
|
203
225
|
rootRealPath;
|
|
204
226
|
allowWrite;
|
|
205
227
|
allowShell;
|
|
228
|
+
allowExternalFileAnalysis;
|
|
229
|
+
memoryEnabled;
|
|
206
230
|
timeoutMs;
|
|
207
231
|
signal;
|
|
208
232
|
approvalHandler;
|
|
@@ -212,6 +236,8 @@ export class WorkspaceTools {
|
|
|
212
236
|
this.rootRealPath = realpath(this.root).catch(() => this.root);
|
|
213
237
|
this.allowWrite = options.allowWrite;
|
|
214
238
|
this.allowShell = options.allowShell;
|
|
239
|
+
this.allowExternalFileAnalysis = Boolean(options.allowExternalFileAnalysis);
|
|
240
|
+
this.memoryEnabled = Boolean(options.memoryEnabled);
|
|
215
241
|
this.timeoutMs = options.timeoutMs ?? 60_000;
|
|
216
242
|
this.signal = options.signal;
|
|
217
243
|
this.approvalHandler = options.approvalHandler;
|
|
@@ -231,6 +257,10 @@ export class WorkspaceTools {
|
|
|
231
257
|
return await this.searchText(readString(call.arguments.query, ""));
|
|
232
258
|
case "inspect_document":
|
|
233
259
|
return await this.inspectDocument(readString(call.arguments.path, ""));
|
|
260
|
+
case "memory_remember":
|
|
261
|
+
return await this.memoryRemember(readString(call.arguments.content, ""), readStringArray(call.arguments.tags));
|
|
262
|
+
case "memory_search":
|
|
263
|
+
return await this.memorySearch(readString(call.arguments.query, ""), readNumber(call.arguments.limit, 8));
|
|
234
264
|
case "git_status":
|
|
235
265
|
return await this.gitStatus();
|
|
236
266
|
case "git_diff":
|
|
@@ -398,10 +428,18 @@ export class WorkspaceTools {
|
|
|
398
428
|
if (isSensitivePath(requestedPath)) {
|
|
399
429
|
return denied(`inspect_document denied sensitive path: ${requestedPath}`);
|
|
400
430
|
}
|
|
401
|
-
const absolutePath = await this.
|
|
431
|
+
const { absolutePath, external } = await this.resolveDocumentPath(requestedPath);
|
|
432
|
+
if (external) {
|
|
433
|
+
const approval = await this.requestApproval("inspect_document", "external_file", {
|
|
434
|
+
path: absolutePath
|
|
435
|
+
}, `Inspect external file: ${absolutePath}`);
|
|
436
|
+
if (approval.decision === "deny") {
|
|
437
|
+
return denied("inspect_document denied by permission policy.", "inspect_document", approval);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
402
440
|
const extension = path.extname(absolutePath).toLowerCase();
|
|
403
441
|
if (isLikelyTextFile(absolutePath)) {
|
|
404
|
-
return await this.
|
|
442
|
+
return await this.readTextDocument(absolutePath);
|
|
405
443
|
}
|
|
406
444
|
if (extension === ".pdf") {
|
|
407
445
|
return await extractPdfText(absolutePath, this.timeoutMs, this.signal);
|
|
@@ -409,8 +447,66 @@ export class WorkspaceTools {
|
|
|
409
447
|
if (extension === ".docx") {
|
|
410
448
|
return await extractDocxText(absolutePath);
|
|
411
449
|
}
|
|
450
|
+
if (isImageFile(absolutePath)) {
|
|
451
|
+
return await inspectImageFile(absolutePath);
|
|
452
|
+
}
|
|
412
453
|
return denied(`inspect_document does not support ${extension || "this file type"} yet.`);
|
|
413
454
|
}
|
|
455
|
+
async readTextDocument(absolutePath) {
|
|
456
|
+
const content = await readFile(absolutePath, "utf8");
|
|
457
|
+
const relativePath = path.relative(this.root, absolutePath);
|
|
458
|
+
return {
|
|
459
|
+
ok: true,
|
|
460
|
+
summary: `inspected ${relativePath.startsWith("..") || path.isAbsolute(relativePath) ? absolutePath : relativePath}`,
|
|
461
|
+
content: clip(content, 20_000),
|
|
462
|
+
tool: "inspect_document",
|
|
463
|
+
category: toolSpecs.inspect_document.category
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
async memoryRemember(content, tags) {
|
|
467
|
+
if (!this.memoryEnabled) {
|
|
468
|
+
return denied("memory_remember requires /experimental memory.", "memory_remember");
|
|
469
|
+
}
|
|
470
|
+
if (!this.allowWrite) {
|
|
471
|
+
const approval = await this.requestApproval("memory_remember", "write", {
|
|
472
|
+
contentLength: content.length,
|
|
473
|
+
tags
|
|
474
|
+
}, `Store durable memory (${content.length} characters).`);
|
|
475
|
+
if (approval.decision === "deny") {
|
|
476
|
+
return denied("memory_remember denied by permission policy.", "memory_remember", approval);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
try {
|
|
480
|
+
const store = new MemoryStore();
|
|
481
|
+
const entry = store.remember(this.root, content, tags);
|
|
482
|
+
store.close();
|
|
483
|
+
return {
|
|
484
|
+
ok: true,
|
|
485
|
+
summary: `remembered memory #${entry.id}`,
|
|
486
|
+
content: `Stored memory #${entry.id}: ${entry.content}`,
|
|
487
|
+
tool: "memory_remember",
|
|
488
|
+
category: toolSpecs.memory_remember.category
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
catch (error) {
|
|
492
|
+
return denied(error instanceof Error ? error.message : String(error), "memory_remember");
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
async memorySearch(query, limit) {
|
|
496
|
+
if (!this.memoryEnabled) {
|
|
497
|
+
return denied("memory_search requires /experimental memory.", "memory_search");
|
|
498
|
+
}
|
|
499
|
+
const store = new MemoryStore();
|
|
500
|
+
const matches = store.search(this.root, query, limit);
|
|
501
|
+
store.close();
|
|
502
|
+
return {
|
|
503
|
+
ok: true,
|
|
504
|
+
summary: `found ${matches.length} memory match${matches.length === 1 ? "" : "es"}`,
|
|
505
|
+
content: matches.map((match) => `#${match.id} score ${match.score} ${match.createdAt}\n${match.content}`).join("\n\n") || "No matching memories.",
|
|
506
|
+
tool: "memory_search",
|
|
507
|
+
category: toolSpecs.memory_search.category
|
|
508
|
+
};
|
|
509
|
+
}
|
|
414
510
|
async searchText(query) {
|
|
415
511
|
if (!query.trim()) {
|
|
416
512
|
return denied("search_text requires a non-empty query.");
|
|
@@ -584,7 +680,7 @@ export class WorkspaceTools {
|
|
|
584
680
|
const approval = await this.requestApproval("run_script", "shell", {
|
|
585
681
|
script: normalizedScript,
|
|
586
682
|
command: scriptCommand
|
|
587
|
-
}, previewPackageScript(normalizedScript, scriptCommand));
|
|
683
|
+
}, previewPackageScript(normalizedScript, scriptCommand, this.root));
|
|
588
684
|
if (approval.decision === "deny") {
|
|
589
685
|
return denied("run_script denied by permission policy.", "run_script", approval);
|
|
590
686
|
}
|
|
@@ -596,7 +692,7 @@ export class WorkspaceTools {
|
|
|
596
692
|
content: clip(output.output, 20_000),
|
|
597
693
|
tool: "run_script",
|
|
598
694
|
category: toolSpecs.run_script.category,
|
|
599
|
-
preview: previewPackageScript(normalizedScript, scriptCommand)
|
|
695
|
+
preview: previewPackageScript(normalizedScript, scriptCommand, this.root)
|
|
600
696
|
};
|
|
601
697
|
}
|
|
602
698
|
async runTests() {
|
|
@@ -616,7 +712,7 @@ export class WorkspaceTools {
|
|
|
616
712
|
if (!command.trim()) {
|
|
617
713
|
return denied("run_shell requires a command.");
|
|
618
714
|
}
|
|
619
|
-
const shellSafetyError = validateShellCommand(command);
|
|
715
|
+
const shellSafetyError = validateShellCommand(command, this.root);
|
|
620
716
|
if (shellSafetyError) {
|
|
621
717
|
return denied(`run_shell denied. ${shellSafetyError}`);
|
|
622
718
|
}
|
|
@@ -682,6 +778,42 @@ export class WorkspaceTools {
|
|
|
682
778
|
await assertInsideWorkspace(await this.rootRealPath, resolvedPath, requestedPath);
|
|
683
779
|
return resolvedPath;
|
|
684
780
|
}
|
|
781
|
+
async resolveDocumentPath(requestedPath) {
|
|
782
|
+
const trimmedPath = requestedPath.trim();
|
|
783
|
+
if (!path.isAbsolute(trimmedPath)) {
|
|
784
|
+
return {
|
|
785
|
+
absolutePath: await this.resolveReadPath(trimmedPath),
|
|
786
|
+
external: false
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
if (isSensitivePath(trimmedPath)) {
|
|
790
|
+
throw new Error(`inspect_document denied sensitive path: ${requestedPath}`);
|
|
791
|
+
}
|
|
792
|
+
const relativePath = path.relative(this.root, trimmedPath);
|
|
793
|
+
if (!relativePath.startsWith("..") && !path.isAbsolute(relativePath)) {
|
|
794
|
+
return {
|
|
795
|
+
absolutePath: await this.resolveReadPath(trimmedPath),
|
|
796
|
+
external: false
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
if (!this.allowExternalFileAnalysis) {
|
|
800
|
+
throw new Error(`Path escapes workspace: ${requestedPath}. Enable /experimental file-analysis to inspect external files.`);
|
|
801
|
+
}
|
|
802
|
+
const extension = path.extname(trimmedPath).toLowerCase();
|
|
803
|
+
if (!isLikelyTextFile(trimmedPath) && extension !== ".pdf" && extension !== ".docx" && !isImageFile(trimmedPath)) {
|
|
804
|
+
throw new Error(`external file analysis does not support ${extension || "this file type"} yet.`);
|
|
805
|
+
}
|
|
806
|
+
const resolvedPath = await realpath(trimmedPath).catch((error) => {
|
|
807
|
+
throw new Error(`file not found or unreadable: ${requestedPath} (${error instanceof Error ? error.message : String(error)})`);
|
|
808
|
+
});
|
|
809
|
+
if (isSensitivePath(resolvedPath)) {
|
|
810
|
+
throw new Error(`inspect_document denied sensitive path: ${requestedPath}`);
|
|
811
|
+
}
|
|
812
|
+
return {
|
|
813
|
+
absolutePath: resolvedPath,
|
|
814
|
+
external: true
|
|
815
|
+
};
|
|
816
|
+
}
|
|
685
817
|
async resolveWritePath(requestedPath) {
|
|
686
818
|
const absolutePath = this.resolveInsideWorkspace(requestedPath);
|
|
687
819
|
const rootRealPath = await this.rootRealPath;
|
|
@@ -759,7 +891,18 @@ async function searchTextWithRipgrep(workspaceRoot, query, timeoutMs, signal) {
|
|
|
759
891
|
"!**/.netrc",
|
|
760
892
|
"!**/id_rsa",
|
|
761
893
|
"!**/id_ed25519",
|
|
762
|
-
"!**/known_hosts"
|
|
894
|
+
"!**/known_hosts",
|
|
895
|
+
"!**/Cookies",
|
|
896
|
+
"!**/Network/Cookies",
|
|
897
|
+
"!**/Login Data",
|
|
898
|
+
"!**/Web Data",
|
|
899
|
+
"!**/Chrome/**",
|
|
900
|
+
"!**/Chromium/**",
|
|
901
|
+
"!**/Brave*/**",
|
|
902
|
+
"!**/Microsoft Edge/**",
|
|
903
|
+
"!**/Arc/**",
|
|
904
|
+
"!**/Firefox/**",
|
|
905
|
+
"!**/Safari/**"
|
|
763
906
|
];
|
|
764
907
|
return new Promise((resolve) => {
|
|
765
908
|
const child = spawn("rg", [
|
|
@@ -925,6 +1068,15 @@ function readNumber(value, fallback) {
|
|
|
925
1068
|
}
|
|
926
1069
|
return fallback;
|
|
927
1070
|
}
|
|
1071
|
+
function readStringArray(value) {
|
|
1072
|
+
if (Array.isArray(value)) {
|
|
1073
|
+
return value.filter((item) => typeof item === "string");
|
|
1074
|
+
}
|
|
1075
|
+
if (typeof value === "string") {
|
|
1076
|
+
return value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
1077
|
+
}
|
|
1078
|
+
return [];
|
|
1079
|
+
}
|
|
928
1080
|
function isPlaceholderPath(value) {
|
|
929
1081
|
const normalizedValue = value.trim().toLowerCase().replaceAll("\\", "/");
|
|
930
1082
|
return ["relative/path", "path/to/file", "file/path", "<path>", "<file>", "filename"].includes(normalizedValue);
|
|
@@ -943,7 +1095,7 @@ function isSensitivePath(value) {
|
|
|
943
1095
|
normalizedPart.endsWith(".pfx") ||
|
|
944
1096
|
normalizedPart.startsWith("secrets.") ||
|
|
945
1097
|
normalizedPart.includes("credentials"));
|
|
946
|
-
});
|
|
1098
|
+
}) || blockedPathPatterns.some((pattern) => pattern.test(normalizedPath));
|
|
947
1099
|
}
|
|
948
1100
|
function denied(message, tool, approval) {
|
|
949
1101
|
return {
|
|
@@ -958,6 +1110,60 @@ function denied(message, tool, approval) {
|
|
|
958
1110
|
function isLikelyTextFile(filePath) {
|
|
959
1111
|
return textFileExtensions.has(path.extname(filePath).toLowerCase());
|
|
960
1112
|
}
|
|
1113
|
+
function isImageFile(filePath) {
|
|
1114
|
+
return [".png", ".jpg", ".jpeg", ".webp", ".gif"].includes(path.extname(filePath).toLowerCase());
|
|
1115
|
+
}
|
|
1116
|
+
async function inspectImageFile(filePath) {
|
|
1117
|
+
const buffer = await readFile(filePath);
|
|
1118
|
+
const dimensions = readImageDimensions(buffer, path.extname(filePath).toLowerCase());
|
|
1119
|
+
return {
|
|
1120
|
+
ok: true,
|
|
1121
|
+
summary: `inspected image ${path.basename(filePath)}`,
|
|
1122
|
+
content: [
|
|
1123
|
+
`image: ${path.basename(filePath)}`,
|
|
1124
|
+
`type: ${path.extname(filePath).toLowerCase().replace(".", "") || "unknown"}`,
|
|
1125
|
+
`size: ${buffer.length} bytes`,
|
|
1126
|
+
dimensions ? `dimensions: ${dimensions.width}x${dimensions.height}` : "dimensions: unknown"
|
|
1127
|
+
].join("\n"),
|
|
1128
|
+
tool: "inspect_document",
|
|
1129
|
+
category: toolSpecs.inspect_document.category
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
function readImageDimensions(buffer, extension) {
|
|
1133
|
+
if (extension === ".png" && buffer.length >= 24 && buffer.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))) {
|
|
1134
|
+
return {
|
|
1135
|
+
width: buffer.readUInt32BE(16),
|
|
1136
|
+
height: buffer.readUInt32BE(20)
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
if ((extension === ".jpg" || extension === ".jpeg") && buffer.length >= 4) {
|
|
1140
|
+
let offset = 2;
|
|
1141
|
+
while (offset + 9 < buffer.length) {
|
|
1142
|
+
if (buffer[offset] !== 0xff) {
|
|
1143
|
+
return null;
|
|
1144
|
+
}
|
|
1145
|
+
const marker = buffer[offset + 1];
|
|
1146
|
+
const length = buffer.readUInt16BE(offset + 2);
|
|
1147
|
+
if (marker >= 0xc0 && marker <= 0xc3) {
|
|
1148
|
+
return {
|
|
1149
|
+
height: buffer.readUInt16BE(offset + 5),
|
|
1150
|
+
width: buffer.readUInt16BE(offset + 7)
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
offset += 2 + length;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
if (extension === ".webp" && buffer.length >= 30 && buffer.subarray(0, 4).toString("ascii") === "RIFF" && buffer.subarray(8, 12).toString("ascii") === "WEBP") {
|
|
1157
|
+
const chunk = buffer.subarray(12, 16).toString("ascii");
|
|
1158
|
+
if (chunk === "VP8X") {
|
|
1159
|
+
return {
|
|
1160
|
+
width: 1 + buffer.readUIntLE(24, 3),
|
|
1161
|
+
height: 1 + buffer.readUIntLE(27, 3)
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
return null;
|
|
1166
|
+
}
|
|
961
1167
|
async function extractPdfText(filePath, timeoutMs, signal) {
|
|
962
1168
|
try {
|
|
963
1169
|
const { stdout } = await execFileAsync("pdftotext", ["-layout", filePath, "-"], {
|
|
@@ -1074,42 +1280,108 @@ function previewPatch(patchContent) {
|
|
|
1074
1280
|
const removed = patchContent.split(/\r?\n/).filter((line) => line.startsWith("-") && !line.startsWith("---")).length;
|
|
1075
1281
|
return `Apply patch to ${fileSummary} (+${added}/-${removed}).`;
|
|
1076
1282
|
}
|
|
1077
|
-
function validateShellCommand(command) {
|
|
1283
|
+
function validateShellCommand(command, workspaceRoot) {
|
|
1078
1284
|
const trimmedCommand = command.trim();
|
|
1079
|
-
if (/[
|
|
1080
|
-
return "shell metacharacters are blocked;
|
|
1285
|
+
if (/[;&<>`$\n\r]/.test(trimmedCommand)) {
|
|
1286
|
+
return "dangerous shell metacharacters are blocked; pipes are allowed, but command separators, redirects, expansion, and multiline commands are not.";
|
|
1287
|
+
}
|
|
1288
|
+
if (/(^|\s)\|\|(\s|$)/.test(trimmedCommand)) {
|
|
1289
|
+
return "shell command separators are blocked; use a single pipeline.";
|
|
1081
1290
|
}
|
|
1082
1291
|
const tokens = trimmedCommand.match(/"[^"]*"|'[^']*'|\S+/g) ?? [];
|
|
1083
1292
|
if (tokens.length === 0) {
|
|
1084
1293
|
return "command is empty.";
|
|
1085
1294
|
}
|
|
1295
|
+
for (const segment of splitPipeline(tokens)) {
|
|
1296
|
+
const segmentError = validateShellSegment(segment);
|
|
1297
|
+
if (segmentError) {
|
|
1298
|
+
return segmentError;
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
for (const token of tokens.filter((value) => value !== "|")) {
|
|
1302
|
+
const normalizedToken = stripQuotes(token);
|
|
1303
|
+
if (isSensitivePath(normalizedToken)) {
|
|
1304
|
+
return "sensitive path arguments are blocked.";
|
|
1305
|
+
}
|
|
1306
|
+
if (/(^|[\\/])\.\.([\\/]|$)/.test(normalizedToken)) {
|
|
1307
|
+
return "parent directory traversal is blocked.";
|
|
1308
|
+
}
|
|
1309
|
+
const absolutePath = toAbsoluteShellPath(normalizedToken);
|
|
1310
|
+
if (absolutePath) {
|
|
1311
|
+
const relativePath = path.relative(workspaceRoot, absolutePath);
|
|
1312
|
+
if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
|
|
1313
|
+
return "absolute path arguments outside the workspace are blocked. Use inspect_document with /experimental file-analysis for external files.";
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
return null;
|
|
1318
|
+
}
|
|
1319
|
+
function validateShellSegment(tokens) {
|
|
1320
|
+
if (tokens.length === 0) {
|
|
1321
|
+
return "empty shell pipeline segment.";
|
|
1322
|
+
}
|
|
1086
1323
|
const executable = stripQuotes(tokens[0] ?? "").toLowerCase();
|
|
1087
|
-
const subcommand =
|
|
1324
|
+
const subcommand = findCommandSubcommand(executable, tokens.slice(1));
|
|
1088
1325
|
if (["bash", "sh", "zsh", "fish", "pwsh", "powershell", "powershell.exe", "python", "python3", "node", "ruby", "perl"].includes(executable)) {
|
|
1089
1326
|
return `executable "${executable}" is blocked.`;
|
|
1090
1327
|
}
|
|
1091
1328
|
if (["rm", "rmdir", "mv", "cp"].includes(executable) && tokens.some((token) => /^-.*[fRr]/.test(stripQuotes(token)))) {
|
|
1092
1329
|
return `destructive ${executable} flags are blocked.`;
|
|
1093
1330
|
}
|
|
1094
|
-
if (executable === "git" && ["clean", "reset", "push", "checkout", "switch", "branch", "tag"].includes(subcommand)) {
|
|
1331
|
+
if (executable === "git" && subcommand && ["clean", "reset", "push", "checkout", "switch", "branch", "tag"].includes(subcommand)) {
|
|
1095
1332
|
return `git ${subcommand} is blocked in the shell tool.`;
|
|
1096
1333
|
}
|
|
1097
|
-
if (executable === "npm" && ["publish", "unpublish", "dist-tag"].includes(subcommand)) {
|
|
1334
|
+
if (executable === "npm" && subcommand && ["publish", "unpublish", "dist-tag"].includes(subcommand)) {
|
|
1098
1335
|
return `npm ${subcommand} is blocked in the shell tool.`;
|
|
1099
1336
|
}
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1337
|
+
return null;
|
|
1338
|
+
}
|
|
1339
|
+
function splitPipeline(tokens) {
|
|
1340
|
+
const segments = [[]];
|
|
1341
|
+
for (const token of tokens) {
|
|
1342
|
+
if (token === "|") {
|
|
1343
|
+
segments.push([]);
|
|
1344
|
+
continue;
|
|
1104
1345
|
}
|
|
1105
|
-
|
|
1106
|
-
|
|
1346
|
+
segments.at(-1)?.push(token);
|
|
1347
|
+
}
|
|
1348
|
+
return segments;
|
|
1349
|
+
}
|
|
1350
|
+
function findCommandSubcommand(executable, tokens) {
|
|
1351
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
1352
|
+
const token = stripQuotes(tokens[index] ?? "");
|
|
1353
|
+
if (!token || token === "--") {
|
|
1354
|
+
continue;
|
|
1355
|
+
}
|
|
1356
|
+
if (executable === "git" && ["-C", "-c", "--git-dir", "--work-tree"].includes(token)) {
|
|
1357
|
+
index += 1;
|
|
1358
|
+
continue;
|
|
1359
|
+
}
|
|
1360
|
+
if (executable === "npm" && ["--prefix", "--userconfig", "--cache"].includes(token)) {
|
|
1361
|
+
index += 1;
|
|
1362
|
+
continue;
|
|
1107
1363
|
}
|
|
1364
|
+
if (token.startsWith("-")) {
|
|
1365
|
+
continue;
|
|
1366
|
+
}
|
|
1367
|
+
return token.toLowerCase();
|
|
1368
|
+
}
|
|
1369
|
+
return null;
|
|
1370
|
+
}
|
|
1371
|
+
function toAbsoluteShellPath(value) {
|
|
1372
|
+
if (!value || value === "|" || value.startsWith("-")) {
|
|
1373
|
+
return null;
|
|
1374
|
+
}
|
|
1375
|
+
if (path.isAbsolute(value)) {
|
|
1376
|
+
return path.resolve(value);
|
|
1377
|
+
}
|
|
1378
|
+
if (value === "~" || value.startsWith("~/")) {
|
|
1379
|
+
return path.resolve(process.env.HOME ?? "", value === "~" ? "." : value.slice(2));
|
|
1108
1380
|
}
|
|
1109
1381
|
return null;
|
|
1110
1382
|
}
|
|
1111
|
-
function previewPackageScript(name, command) {
|
|
1112
|
-
const risk = validateShellCommand(command);
|
|
1383
|
+
function previewPackageScript(name, command, workspaceRoot) {
|
|
1384
|
+
const risk = validateShellCommand(command, workspaceRoot);
|
|
1113
1385
|
const prefix = risk ? `Risky package script (${risk})` : "Run package script";
|
|
1114
1386
|
return `${prefix}: npm run ${name} -> ${clip(command, 220)}`;
|
|
1115
1387
|
}
|