@gajae-code/coding-agent 0.11.2 → 0.11.4
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 +34 -0
- package/dist/types/cli/launch-disposition.d.ts +23 -0
- package/dist/types/cli/worktree-cli.d.ts +8 -25
- package/dist/types/commands/gc.d.ts +4 -0
- package/dist/types/commands/worktree.d.ts +7 -32
- package/dist/types/config/settings-schema.d.ts +85 -10
- package/dist/types/gjc-runtime/gc-runtime.d.ts +9 -0
- package/dist/types/gjc-runtime/state-argv.d.ts +38 -0
- package/dist/types/main.d.ts +1 -0
- package/dist/types/modes/components/custom-editor.d.ts +8 -3
- package/dist/types/modes/components/tool-transcript-format.d.ts +19 -0
- package/dist/types/modes/controllers/input-controller.d.ts +8 -2
- package/dist/types/modes/prompt-action-autocomplete.d.ts +4 -0
- package/dist/types/sdk/broker/broker.d.ts +1 -0
- package/dist/types/sdk/broker/discovery.d.ts +10 -0
- package/dist/types/sdk/broker/ensure.d.ts +24 -1
- package/dist/types/sdk/broker/internal.d.ts +3 -0
- package/dist/types/sdk/broker/session-index.d.ts +12 -0
- package/dist/types/sdk/broker/state-version.d.ts +2 -1
- package/dist/types/sdk/bus/index.d.ts +2 -0
- package/dist/types/sdk/session.d.ts +2 -0
- package/dist/types/tools/ask.d.ts +39 -12
- package/dist/types/tools/browser/screenshot-format.d.ts +8 -0
- package/dist/types/tools/browser/tab-protocol.d.ts +11 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +1 -1
- package/dist/types/tools/browser/tab-worker-path-resolver.d.ts +2 -0
- package/dist/types/tools/browser/tab-worker-smoke.d.ts +16 -0
- package/dist/types/tools/path-utils.d.ts +9 -14
- package/dist/types/tools/read.d.ts +2 -0
- package/dist/types/tools/render-utils.d.ts +1 -8
- package/dist/types/tools/resource-gc.d.ts +14 -0
- package/dist/types/tools/subagent.d.ts +1 -0
- package/dist/types/utils/file-mentions.d.ts +3 -3
- package/dist/types/utils/image-loading.d.ts +23 -0
- package/dist/types/utils/image-resize.d.ts +2 -0
- package/dist/types/utils/pasted-image-loading.d.ts +39 -0
- package/dist/types/utils/pasted-image-path.d.ts +21 -19
- package/dist/types/web/scrapers/html-to-markdown.d.ts +5 -0
- package/dist/types/web/scrapers/types.d.ts +1 -5
- package/package.json +24 -8
- package/scripts/run-threat-model-check.ts +60 -0
- package/src/cli/launch-disposition.ts +42 -0
- package/src/cli/worktree-cli.ts +9 -287
- package/src/cli.ts +10 -9
- package/src/commands/gc.ts +11 -1
- package/src/commands/sdk.ts +3 -2
- package/src/commands/worktree.ts +10 -53
- package/src/config/model-profiles.ts +15 -15
- package/src/config/settings-schema.ts +55 -10
- package/src/eval/js/shared/runtime.ts +1 -1
- package/src/gjc-runtime/gc-render.ts +19 -1
- package/src/gjc-runtime/gc-runtime.ts +68 -3
- package/src/gjc-runtime/state-argv.ts +292 -0
- package/src/gjc-runtime/state-runtime.ts +51 -212
- package/src/main.ts +99 -22
- package/src/modes/components/custom-editor.ts +103 -32
- package/src/modes/components/custom-provider-wizard.ts +26 -1
- package/src/modes/components/session-observer-overlay.ts +10 -23
- package/src/modes/components/tool-transcript-format.ts +72 -0
- package/src/modes/components/transcript-viewer-overlay.ts +42 -2
- package/src/modes/controllers/input-controller.ts +98 -37
- package/src/modes/controllers/selector-controller.ts +1 -1
- package/src/modes/interactive-mode.ts +13 -2
- package/src/modes/prompt-action-autocomplete.ts +7 -0
- package/src/sdk/broker/broker.ts +139 -32
- package/src/sdk/broker/discovery.ts +125 -0
- package/src/sdk/broker/ensure.ts +103 -19
- package/src/sdk/broker/internal.ts +10 -0
- package/src/sdk/broker/session-index.ts +228 -15
- package/src/sdk/broker/state-version.ts +5 -2
- package/src/sdk/bus/index.ts +4 -1
- package/src/sdk/bus/telegram-daemon.ts +50 -10
- package/src/sdk/host/host.ts +13 -1
- package/src/sdk/session.ts +4 -1
- package/src/session/internal/managed-session-scope.ts +15 -1
- package/src/tools/ask.ts +216 -41
- package/src/tools/bash-allowed-prefixes.ts +79 -29
- package/src/tools/browser/launch.ts +3 -1
- package/src/tools/browser/readable.ts +1 -1
- package/src/tools/browser/screenshot-format.ts +34 -0
- package/src/tools/browser/tab-protocol.ts +3 -0
- package/src/tools/browser/tab-supervisor.ts +125 -122
- package/src/tools/browser/tab-worker-entry.ts +33 -14
- package/src/tools/browser/tab-worker-path-resolver.ts +67 -0
- package/src/tools/browser/tab-worker-smoke.ts +87 -0
- package/src/tools/browser/tab-worker.ts +7 -5
- package/src/tools/output-meta.ts +6 -2
- package/src/tools/path-utils.ts +42 -35
- package/src/tools/read.ts +189 -64
- package/src/tools/render-utils.ts +1 -27
- package/src/tools/resource-gc.ts +123 -34
- package/src/tools/subagent.ts +43 -2
- package/src/utils/file-mentions.ts +4 -4
- package/src/utils/image-loading.ts +85 -34
- package/src/utils/image-resize.ts +16 -9
- package/src/utils/pasted-image-loading.ts +514 -0
- package/src/utils/pasted-image-path.ts +223 -98
- package/src/web/scrapers/html-to-markdown.ts +68 -0
- package/src/web/scrapers/readthedocs.ts +1 -1
- package/src/web/scrapers/types.ts +1 -68
- package/src/web/search/providers/openai-compatible.ts +76 -23
|
@@ -1,14 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Resolve pasted editor text to an image file path.
|
|
3
|
-
*
|
|
4
|
-
* Some terminal/clipboard integrations paste a temporary filesystem path after a
|
|
5
|
-
* copied image is pasted into the editor (for example `/tmp/clipboard-...png`).
|
|
6
|
-
* Only those recognized clipboard temp files are auto-attached and replaced with
|
|
7
|
-
* an `[image N] source="/path"` reference so the model receives both the image
|
|
8
|
-
* payload and the raw temp file path. Ordinary image paths remain literal prompt
|
|
9
|
-
* text; users attach saved files explicitly with `@path/to/image.png`.
|
|
10
|
-
*/
|
|
11
|
-
import * as fs from "node:fs";
|
|
12
1
|
import * as os from "node:os";
|
|
13
2
|
import * as path from "node:path";
|
|
14
3
|
import { fileURLToPath } from "node:url";
|
|
@@ -16,12 +5,12 @@ import { fileURLToPath } from "node:url";
|
|
|
16
5
|
const IMAGE_FILE_EXTENSION_PATTERN = /\.(?:png|jpe?g|gif|webp)$/i;
|
|
17
6
|
const CLIPBOARD_TEMP_BASENAME_PATTERN = /^clipboard-\d{4}-\d{2}-\d{2}-\d{6}-[A-Za-z0-9-]+\.(?:png|jpe?g|gif|webp)$/i;
|
|
18
7
|
|
|
8
|
+
export const MAX_PASTED_IMAGE_COUNT = 16;
|
|
9
|
+
export const MAX_PASTED_IMAGE_PASTE_CHARACTERS = 1024 * 1024;
|
|
10
|
+
export const MAX_PASTED_IMAGE_PATH_CHARACTERS = 32 * 1024;
|
|
11
|
+
|
|
19
12
|
export interface DecodePastedPathOptions {
|
|
20
|
-
/**
|
|
21
|
-
* Platform whose path semantics apply when decoding (shell unescaping,
|
|
22
|
-
* `file://` drive letters / UNC hosts). Defaults to `process.platform`;
|
|
23
|
-
* injectable so tests can pin the win32 contract from any host.
|
|
24
|
-
*/
|
|
13
|
+
/** Platform whose path and shell semantics apply. Defaults to `process.platform`. */
|
|
25
14
|
platform?: NodeJS.Platform;
|
|
26
15
|
/** Home directory for `~/` expansion. Defaults to `os.homedir()`. */
|
|
27
16
|
homedir?: string;
|
|
@@ -32,17 +21,30 @@ export interface ResolvePastedImagePathOptions extends DecodePastedPathOptions {
|
|
|
32
21
|
cwd?: string;
|
|
33
22
|
}
|
|
34
23
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
24
|
+
export type PastedImagePathParseResult =
|
|
25
|
+
| {
|
|
26
|
+
kind: "paths";
|
|
27
|
+
paths: string[];
|
|
28
|
+
requiresConfirmation: boolean;
|
|
29
|
+
}
|
|
30
|
+
| {
|
|
31
|
+
kind: "too-many";
|
|
32
|
+
maxCandidates: number;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type PastedPathListState = "normal" | "escape" | "single-quote" | "double-quote";
|
|
36
|
+
type EscapeReturnState = "normal" | "double-quote";
|
|
37
|
+
|
|
38
|
+
type TokenizePastedPathsResult =
|
|
39
|
+
| { kind: "candidates"; candidates: string[] }
|
|
40
|
+
| { kind: "too-many"; maxCandidates: number; candidates: string[] }
|
|
41
|
+
| { kind: "invalid" };
|
|
42
|
+
|
|
43
|
+
function pathApi(platform: NodeJS.Platform): typeof path.posix | typeof path.win32 {
|
|
44
|
+
return platform === "win32" ? path.win32 : path.posix;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function decodeFileUrl(candidate: string, platform: NodeJS.Platform, allowRemoteHost: boolean): string | undefined {
|
|
46
48
|
let url: URL;
|
|
47
49
|
try {
|
|
48
50
|
url = new URL(candidate);
|
|
@@ -50,6 +52,7 @@ function decodeFileUrl(candidate: string, platform: NodeJS.Platform): string | u
|
|
|
50
52
|
return undefined;
|
|
51
53
|
}
|
|
52
54
|
if (url.protocol !== "file:") return undefined;
|
|
55
|
+
if (!allowRemoteHost && url.hostname && url.hostname !== "localhost") return undefined;
|
|
53
56
|
|
|
54
57
|
if (platform === process.platform) {
|
|
55
58
|
try {
|
|
@@ -59,7 +62,6 @@ function decodeFileUrl(candidate: string, platform: NodeJS.Platform): string | u
|
|
|
59
62
|
}
|
|
60
63
|
}
|
|
61
64
|
|
|
62
|
-
// Foreign-platform mirror of fileURLToPath (tests only).
|
|
63
65
|
if (/%2f/i.test(url.pathname) || (platform === "win32" && /%5c/i.test(url.pathname))) return undefined;
|
|
64
66
|
let pathname: string;
|
|
65
67
|
try {
|
|
@@ -69,10 +71,8 @@ function decodeFileUrl(candidate: string, platform: NodeJS.Platform): string | u
|
|
|
69
71
|
}
|
|
70
72
|
if (platform === "win32") {
|
|
71
73
|
if (url.hostname && url.hostname !== "localhost") {
|
|
72
|
-
|
|
73
|
-
return `\\\\${url.hostname}${pathname.replaceAll("/", "\\")}`;
|
|
74
|
+
return allowRemoteHost ? `\\\\${url.hostname}${pathname.replaceAll("/", "\\")}` : undefined;
|
|
74
75
|
}
|
|
75
|
-
// Drive letter required: file:///C:/x -> C:\x
|
|
76
76
|
if (!/^\/[A-Za-z]:/.test(pathname)) return undefined;
|
|
77
77
|
return pathname.slice(1).replaceAll("/", "\\");
|
|
78
78
|
}
|
|
@@ -80,103 +80,228 @@ function decodeFileUrl(candidate: string, platform: NodeJS.Platform): string | u
|
|
|
80
80
|
return pathname;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
83
|
+
function isRemoteWindowsPath(candidate: string): boolean {
|
|
84
|
+
return candidate.startsWith("\\\\") || candidate.startsWith("//");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function expandHome(candidate: string, options: DecodePastedPathOptions, platform: NodeJS.Platform): string {
|
|
88
|
+
if (!candidate.startsWith("~/")) return candidate;
|
|
89
|
+
return pathApi(platform).join(options.homedir ?? os.homedir(), candidate.slice(2));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Decode one pasted path candidate without filesystem access. */
|
|
93
|
+
export function decodePastedPathCandidate(text: string, options: DecodePastedPathOptions = {}): string | undefined {
|
|
94
|
+
const platform = options.platform ?? process.platform;
|
|
94
95
|
let candidate = text.trim();
|
|
95
96
|
if (!candidate || /[\r\n]/.test(candidate)) return undefined;
|
|
96
97
|
|
|
97
|
-
// Quoted paths (some terminals quote instead of escaping).
|
|
98
98
|
if (
|
|
99
99
|
candidate.length >= 2 &&
|
|
100
100
|
(candidate.startsWith('"') || candidate.startsWith("'")) &&
|
|
101
|
-
candidate.endsWith(candidate[0])
|
|
101
|
+
candidate.endsWith(candidate[0] ?? "")
|
|
102
102
|
) {
|
|
103
103
|
candidate = candidate.slice(1, -1);
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
if (candidate.startsWith("file://")) {
|
|
107
|
-
const decoded = decodeFileUrl(candidate, platform);
|
|
107
|
+
const decoded = decodeFileUrl(candidate, platform, true);
|
|
108
108
|
if (decoded === undefined) return undefined;
|
|
109
109
|
candidate = decoded;
|
|
110
110
|
} else if (platform !== "win32") {
|
|
111
|
-
// Terminal drag-drop escapes shell-special characters (`\ `, `\(`, ...).
|
|
112
|
-
// Skipped on Windows where `\` is the path separator.
|
|
113
111
|
candidate = candidate.replace(/\\(.)/g, "$1");
|
|
114
112
|
}
|
|
113
|
+
return expandHome(candidate, options, platform);
|
|
114
|
+
}
|
|
115
115
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
116
|
+
function isAsciiWhitespace(character: string): boolean {
|
|
117
|
+
return (
|
|
118
|
+
character === " " ||
|
|
119
|
+
character === "\t" ||
|
|
120
|
+
character === "\n" ||
|
|
121
|
+
character === "\r" ||
|
|
122
|
+
character === "\v" ||
|
|
123
|
+
character === "\f"
|
|
124
|
+
);
|
|
125
|
+
}
|
|
119
126
|
|
|
127
|
+
function decodeListToken(
|
|
128
|
+
candidate: string,
|
|
129
|
+
options: DecodePastedPathOptions,
|
|
130
|
+
platform: NodeJS.Platform,
|
|
131
|
+
): string | undefined {
|
|
132
|
+
if (candidate.startsWith("file://")) {
|
|
133
|
+
const decoded = decodeFileUrl(candidate, platform, false);
|
|
134
|
+
if (decoded === undefined) return undefined;
|
|
135
|
+
candidate = decoded;
|
|
136
|
+
}
|
|
137
|
+
candidate = expandHome(candidate, options, platform);
|
|
138
|
+
if (platform === "win32" && isRemoteWindowsPath(candidate)) return undefined;
|
|
120
139
|
return candidate;
|
|
121
140
|
}
|
|
122
141
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
let
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
142
|
+
function tokenizePastedPathCandidates(
|
|
143
|
+
text: string,
|
|
144
|
+
options: DecodePastedPathOptions,
|
|
145
|
+
maxCandidates: number,
|
|
146
|
+
): TokenizePastedPathsResult {
|
|
147
|
+
if (text.length > MAX_PASTED_IMAGE_PASTE_CHARACTERS) return { kind: "invalid" };
|
|
148
|
+
const platform = options.platform ?? process.platform;
|
|
149
|
+
const candidates: string[] = [];
|
|
150
|
+
let state: PastedPathListState = "normal";
|
|
151
|
+
let escapeReturnState: EscapeReturnState = "normal";
|
|
152
|
+
let candidate = "";
|
|
153
|
+
let candidateStarted = false;
|
|
154
|
+
|
|
155
|
+
const startCandidate = (): TokenizePastedPathsResult | undefined => {
|
|
156
|
+
if (candidateStarted) return undefined;
|
|
157
|
+
if (candidates.length >= maxCandidates) return { kind: "too-many", maxCandidates, candidates: [...candidates] };
|
|
158
|
+
candidateStarted = true;
|
|
159
|
+
return undefined;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const appendCandidate = (character: string): boolean => {
|
|
163
|
+
candidate += character;
|
|
164
|
+
return candidate.length <= MAX_PASTED_IMAGE_PATH_CHARACTERS;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const finishCandidate = (): TokenizePastedPathsResult | undefined => {
|
|
168
|
+
if (!candidateStarted) return undefined;
|
|
169
|
+
if (!candidate) return { kind: "invalid" };
|
|
170
|
+
if (candidates.length >= maxCandidates) return { kind: "too-many", maxCandidates, candidates: [...candidates] };
|
|
171
|
+
const decoded = decodeListToken(candidate, options, platform);
|
|
172
|
+
if (decoded === undefined) return { kind: "invalid" };
|
|
173
|
+
candidates.push(decoded);
|
|
174
|
+
candidate = "";
|
|
175
|
+
candidateStarted = false;
|
|
176
|
+
return undefined;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
for (const character of text) {
|
|
180
|
+
if (state === "escape") {
|
|
181
|
+
if (!appendCandidate(character)) return { kind: "invalid" };
|
|
182
|
+
state = escapeReturnState;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (state === "single-quote") {
|
|
186
|
+
if (character === "'") state = "normal";
|
|
187
|
+
else if (!appendCandidate(character)) return { kind: "invalid" };
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (state === "double-quote") {
|
|
191
|
+
if (character === '"') {
|
|
192
|
+
state = "normal";
|
|
193
|
+
} else if (character === "\\" && platform !== "win32") {
|
|
194
|
+
escapeReturnState = "double-quote";
|
|
195
|
+
state = "escape";
|
|
196
|
+
} else if (!appendCandidate(character)) {
|
|
197
|
+
return { kind: "invalid" };
|
|
198
|
+
}
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (isAsciiWhitespace(character)) {
|
|
203
|
+
const result = finishCandidate();
|
|
204
|
+
if (result) return result;
|
|
205
|
+
} else if (character === "'") {
|
|
206
|
+
const result = startCandidate();
|
|
207
|
+
if (result) return result;
|
|
208
|
+
state = "single-quote";
|
|
209
|
+
} else if (character === '"') {
|
|
210
|
+
const result = startCandidate();
|
|
211
|
+
if (result) return result;
|
|
212
|
+
state = "double-quote";
|
|
213
|
+
} else if (character === "\\" && platform !== "win32") {
|
|
214
|
+
const result = startCandidate();
|
|
215
|
+
if (result) return result;
|
|
216
|
+
escapeReturnState = "normal";
|
|
217
|
+
state = "escape";
|
|
218
|
+
} else {
|
|
219
|
+
const result = startCandidate();
|
|
220
|
+
if (result) return result;
|
|
221
|
+
if (!appendCandidate(character)) return { kind: "invalid" };
|
|
222
|
+
}
|
|
151
223
|
}
|
|
224
|
+
|
|
225
|
+
if (state !== "normal") return { kind: "invalid" };
|
|
226
|
+
const finalResult = finishCandidate();
|
|
227
|
+
if (finalResult) return finalResult;
|
|
228
|
+
return candidates.length === 0 ? { kind: "invalid" } : { kind: "candidates", candidates };
|
|
152
229
|
}
|
|
153
230
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
231
|
+
/** Tokenize a bounded complete path list without filesystem access. */
|
|
232
|
+
export function decodePastedPathCandidates(
|
|
233
|
+
text: string,
|
|
234
|
+
options: DecodePastedPathOptions = {},
|
|
235
|
+
maxCandidates = MAX_PASTED_IMAGE_COUNT,
|
|
236
|
+
): string[] | undefined {
|
|
237
|
+
const result = tokenizePastedPathCandidates(text, options, maxCandidates);
|
|
238
|
+
return result.kind === "candidates" ? result.candidates : undefined;
|
|
159
239
|
}
|
|
160
240
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
241
|
+
function resolveCandidate(
|
|
242
|
+
candidate: string,
|
|
243
|
+
options: ResolvePastedImagePathOptions,
|
|
244
|
+
platform: NodeJS.Platform,
|
|
245
|
+
): string {
|
|
246
|
+
return pathApi(platform).resolve(options.cwd ?? process.cwd(), candidate);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function isRecognizedClipboardTempPath(filePath: string, platform: NodeJS.Platform): boolean {
|
|
250
|
+
const api = pathApi(platform);
|
|
251
|
+
const resolved = api.resolve(filePath);
|
|
252
|
+
const tempRoot = api.resolve(os.tmpdir());
|
|
253
|
+
if (resolved !== tempRoot && !resolved.startsWith(`${tempRoot}${api.sep}`)) return false;
|
|
254
|
+
return CLIPBOARD_TEMP_BASENAME_PATTERN.test(api.basename(resolved));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Resolve one recognized clipboard-temp image path lexically, before any filesystem access. */
|
|
258
|
+
export function resolvePastedImagePath(text: string, options: ResolvePastedImagePathOptions = {}): string | undefined {
|
|
259
|
+
const platform = options.platform ?? process.platform;
|
|
168
260
|
const candidate = decodePastedPathCandidate(text, options);
|
|
169
261
|
if (!candidate || !IMAGE_FILE_EXTENSION_PATTERN.test(candidate)) return undefined;
|
|
262
|
+
if (platform === "win32" && isRemoteWindowsPath(candidate)) return undefined;
|
|
263
|
+
const resolved = resolveCandidate(candidate, options, platform);
|
|
264
|
+
return isRecognizedClipboardTempPath(resolved, platform) ? resolved : undefined;
|
|
265
|
+
}
|
|
170
266
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
267
|
+
/**
|
|
268
|
+
* Parse a complete image path paste without filesystem access. A single path
|
|
269
|
+
* keeps the clipboard-temp-only policy; multi-path saved-image pastes require
|
|
270
|
+
* explicit confirmation before any path is opened.
|
|
271
|
+
*/
|
|
272
|
+
export function parsePastedImagePaths(
|
|
273
|
+
text: string,
|
|
274
|
+
options: ResolvePastedImagePathOptions = {},
|
|
275
|
+
): PastedImagePathParseResult | undefined {
|
|
276
|
+
const platform = options.platform ?? process.platform;
|
|
277
|
+
const tokenized = tokenizePastedPathCandidates(text, options, MAX_PASTED_IMAGE_COUNT);
|
|
278
|
+
if (tokenized.kind === "too-many") {
|
|
279
|
+
if (
|
|
280
|
+
tokenized.candidates.length !== MAX_PASTED_IMAGE_COUNT ||
|
|
281
|
+
tokenized.candidates.some(
|
|
282
|
+
candidate =>
|
|
283
|
+
!IMAGE_FILE_EXTENSION_PATTERN.test(candidate) ||
|
|
284
|
+
(platform === "win32" && isRemoteWindowsPath(candidate)),
|
|
285
|
+
)
|
|
286
|
+
) {
|
|
287
|
+
return undefined;
|
|
288
|
+
}
|
|
289
|
+
return { kind: "too-many", maxCandidates: tokenized.maxCandidates };
|
|
290
|
+
}
|
|
291
|
+
if (tokenized.kind !== "candidates") return undefined;
|
|
292
|
+
|
|
293
|
+
if (tokenized.candidates.length === 1) {
|
|
294
|
+
const resolved = resolvePastedImagePath(text, options);
|
|
295
|
+
return resolved ? { kind: "paths", paths: [resolved], requiresConfirmation: false } : undefined;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const paths: string[] = [];
|
|
299
|
+
for (const candidate of tokenized.candidates) {
|
|
300
|
+
if (!IMAGE_FILE_EXTENSION_PATTERN.test(candidate)) return undefined;
|
|
301
|
+
if (platform === "win32" && isRemoteWindowsPath(candidate)) return undefined;
|
|
302
|
+
paths.push(resolveCandidate(candidate, options, platform));
|
|
177
303
|
}
|
|
178
|
-
|
|
179
|
-
return resolved;
|
|
304
|
+
return { kind: "paths", paths, requiresConfirmation: true };
|
|
180
305
|
}
|
|
181
306
|
|
|
182
307
|
export function formatPastedImageReference(placeholder: string, imagePath: string): string {
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type TurndownService from "turndown";
|
|
2
|
+
|
|
3
|
+
/** Module-level Turndown instance — built lazily on first use. */
|
|
4
|
+
let turndownPromise: Promise<TurndownService> | undefined;
|
|
5
|
+
|
|
6
|
+
type TurndownListParent = {
|
|
7
|
+
nodeName: string;
|
|
8
|
+
getAttribute(name: string): string | null;
|
|
9
|
+
children: ArrayLike<unknown>;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function getTurndown(): Promise<TurndownService> {
|
|
13
|
+
turndownPromise ||= initTurndown();
|
|
14
|
+
return turndownPromise;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function initTurndown(): Promise<TurndownService> {
|
|
18
|
+
const [{ default: TurndownService }, { gfm }] = await Promise.all([
|
|
19
|
+
import("turndown"),
|
|
20
|
+
import("turndown-plugin-gfm"),
|
|
21
|
+
]);
|
|
22
|
+
const turndown = new TurndownService({
|
|
23
|
+
headingStyle: "atx",
|
|
24
|
+
codeBlockStyle: "fenced",
|
|
25
|
+
bulletListMarker: "-",
|
|
26
|
+
});
|
|
27
|
+
turndown.use(gfm);
|
|
28
|
+
turndown.addRule("strikethrough", {
|
|
29
|
+
filter: ["del", "s", "strike"],
|
|
30
|
+
replacement(content) {
|
|
31
|
+
return `~~${content}~~`;
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
turndown.addRule("heading", {
|
|
35
|
+
filter: ["h1", "h2", "h3", "h4", "h5", "h6"],
|
|
36
|
+
replacement(content, node) {
|
|
37
|
+
const level = Number(node.nodeName.charAt(1));
|
|
38
|
+
const prefix = "#".repeat(level);
|
|
39
|
+
const cleaned = content.replace(/\\([.])/g, "$1").trim();
|
|
40
|
+
return `\n\n${prefix} ${cleaned}\n\n`;
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
turndown.addRule("listItem", {
|
|
44
|
+
filter: "li",
|
|
45
|
+
replacement(content, node, options) {
|
|
46
|
+
content = content.replace(/^\n+/, "").replace(/\n+$/, "\n").replace(/\n/gm, "\n ");
|
|
47
|
+
const parent = node.parentNode as unknown as TurndownListParent | null;
|
|
48
|
+
let prefix = `${options.bulletListMarker} `;
|
|
49
|
+
if (parent?.nodeName === "OL") {
|
|
50
|
+
const start = parent.getAttribute("start");
|
|
51
|
+
const index = Array.prototype.indexOf.call(parent.children, node);
|
|
52
|
+
prefix = `${(start ? Number(start) : 1) + index}. `;
|
|
53
|
+
}
|
|
54
|
+
return prefix + content + (node.nextSibling ? "\n" : "");
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
return turndown;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Convert HTML to markdown using Turndown with GFM support.
|
|
62
|
+
* Strips script/style tags before conversion.
|
|
63
|
+
*/
|
|
64
|
+
export async function htmlToBasicMarkdown(html: string): Promise<string> {
|
|
65
|
+
const cleaned = html.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "");
|
|
66
|
+
const turndown = await getTurndown();
|
|
67
|
+
return turndown.turndown(cleaned).trim();
|
|
68
|
+
}
|
|
@@ -60,7 +60,7 @@ export const handleReadTheDocs: SpecialHandler = async (
|
|
|
60
60
|
?.querySelectorAll(
|
|
61
61
|
".headerlink, .viewcode-link, nav, .sidebar, footer, .related, .sphinxsidebar, .toctree-wrapper",
|
|
62
62
|
)
|
|
63
|
-
.forEach((el:
|
|
63
|
+
.forEach((el: { remove(): void }) => {
|
|
64
64
|
el.remove();
|
|
65
65
|
});
|
|
66
66
|
|
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
* Shared types and utilities for web-fetch handlers
|
|
3
3
|
*/
|
|
4
4
|
import { ptree } from "@gajae-code/utils";
|
|
5
|
-
import type TurndownService from "turndown";
|
|
6
|
-
|
|
7
5
|
import type { AgentStorage } from "../../session/agent-storage";
|
|
8
6
|
import { ToolAbortError } from "../../tools/tool-errors";
|
|
9
7
|
import type { AddressResolver } from "../insane/url-guard";
|
|
@@ -259,72 +257,7 @@ export async function loadPage(url: string, options: LoadPageOptions = {}): Prom
|
|
|
259
257
|
return { content: "", contentType: "", finalUrl: initialUrl, ok: false };
|
|
260
258
|
}
|
|
261
259
|
|
|
262
|
-
|
|
263
|
-
let turndownPromise: Promise<TurndownService> | undefined;
|
|
264
|
-
|
|
265
|
-
type TurndownListParent = {
|
|
266
|
-
nodeName: string;
|
|
267
|
-
getAttribute(name: string): string | null;
|
|
268
|
-
children: ArrayLike<unknown>;
|
|
269
|
-
};
|
|
270
|
-
|
|
271
|
-
function getTurndown(): Promise<TurndownService> {
|
|
272
|
-
turndownPromise ||= initTurndown();
|
|
273
|
-
return turndownPromise;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
async function initTurndown(): Promise<TurndownService> {
|
|
277
|
-
const [{ default: TurndownService }, { gfm }] = await Promise.all([
|
|
278
|
-
import("turndown"),
|
|
279
|
-
import("turndown-plugin-gfm"),
|
|
280
|
-
]);
|
|
281
|
-
const turndown = new TurndownService({
|
|
282
|
-
headingStyle: "atx",
|
|
283
|
-
codeBlockStyle: "fenced",
|
|
284
|
-
bulletListMarker: "-",
|
|
285
|
-
});
|
|
286
|
-
turndown.use(gfm);
|
|
287
|
-
turndown.addRule("strikethrough", {
|
|
288
|
-
filter: ["del", "s", "strike"],
|
|
289
|
-
replacement(content) {
|
|
290
|
-
return `~~${content}~~`;
|
|
291
|
-
},
|
|
292
|
-
});
|
|
293
|
-
turndown.addRule("heading", {
|
|
294
|
-
filter: ["h1", "h2", "h3", "h4", "h5", "h6"],
|
|
295
|
-
replacement(content, node) {
|
|
296
|
-
const level = Number(node.nodeName.charAt(1));
|
|
297
|
-
const prefix = "#".repeat(level);
|
|
298
|
-
const cleaned = content.replace(/\\([.])/g, "$1").trim();
|
|
299
|
-
return `\n\n${prefix} ${cleaned}\n\n`;
|
|
300
|
-
},
|
|
301
|
-
});
|
|
302
|
-
turndown.addRule("listItem", {
|
|
303
|
-
filter: "li",
|
|
304
|
-
replacement(content, node, options) {
|
|
305
|
-
content = content.replace(/^\n+/, "").replace(/\n+$/, "\n").replace(/\n/gm, "\n ");
|
|
306
|
-
const parent = node.parentNode as unknown as TurndownListParent | null;
|
|
307
|
-
let prefix = `${options.bulletListMarker} `;
|
|
308
|
-
if (parent?.nodeName === "OL") {
|
|
309
|
-
const start = parent.getAttribute("start");
|
|
310
|
-
const index = Array.prototype.indexOf.call(parent.children, node);
|
|
311
|
-
prefix = `${(start ? Number(start) : 1) + index}. `;
|
|
312
|
-
}
|
|
313
|
-
return prefix + content + (node.nextSibling ? "\n" : "");
|
|
314
|
-
},
|
|
315
|
-
});
|
|
316
|
-
return turndown;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
/**
|
|
320
|
-
* Convert HTML to markdown using Turndown with GFM support.
|
|
321
|
-
* Strips script/style tags before conversion.
|
|
322
|
-
*/
|
|
323
|
-
export async function htmlToBasicMarkdown(html: string): Promise<string> {
|
|
324
|
-
const cleaned = html.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "");
|
|
325
|
-
const turndown = await getTurndown();
|
|
326
|
-
return turndown.turndown(cleaned).trim();
|
|
327
|
-
}
|
|
260
|
+
export { htmlToBasicMarkdown } from "./html-to-markdown";
|
|
328
261
|
|
|
329
262
|
/**
|
|
330
263
|
* Build a RenderResult from markdown content. Calls finalizeOutput internally.
|