@charzhu/openjaw-agent 0.2.9 → 0.3.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/dist/main.js +845 -227
- package/dist/main.js.map +4 -4
- package/package.json +1 -1
- package/prompts/REASONING.md +12 -3
- package/skills/create-docx.md +2 -0
- package/skills/create-pdf-report.md +2 -0
- package/skills/create-pptx.md +2 -0
- package/skills/daily-briefing.md +2 -0
- package/skills/data-analysis.md +2 -0
- package/skills/deep-research.md +4 -1
- package/skills/web-research.md +2 -1
package/dist/main.js
CHANGED
|
@@ -2393,9 +2393,6 @@ var init_copilot = __esm({
|
|
|
2393
2393
|
}
|
|
2394
2394
|
async chat(options) {
|
|
2395
2395
|
const modelInfo = await this.resolveModelInfo(this.config.model);
|
|
2396
|
-
if (hasImageContent(options.messages) && modelInfo?.supportsVision === false) {
|
|
2397
|
-
throw new Error(`model ${this.config.model} does not support image input; switch to a vision-capable model before submitting an image`);
|
|
2398
|
-
}
|
|
2399
2396
|
if (this.shouldRouteToResponses(modelInfo)) {
|
|
2400
2397
|
return this.chatResponses(options, modelInfo);
|
|
2401
2398
|
}
|
|
@@ -4008,7 +4005,7 @@ function categoryForTool(toolName) {
|
|
|
4008
4005
|
if (toolName.startsWith("word_") || toolName.startsWith("excel_") || toolName.startsWith("powerpoint_") || toolName.startsWith("office_")) return "office";
|
|
4009
4006
|
if (toolName.startsWith("memory_") || toolName === "todo_write") return "memory";
|
|
4010
4007
|
if (toolName.startsWith("file_") || toolName.startsWith("image_") || toolName === "grep" || toolName === "glob") return "files";
|
|
4011
|
-
if (toolName.startsWith("system_") || toolName.startsWith("clipboard_") || ["code_execute", "web_fetch", "web_search", "notify", "sleep", "ask_user", "config"].includes(toolName)) return "system";
|
|
4008
|
+
if (toolName.startsWith("system_") || toolName.startsWith("clipboard_") || ["code_execute", "web_fetch", "web_search", "web_extract", "notify", "sleep", "ask_user", "config"].includes(toolName)) return "system";
|
|
4012
4009
|
return "mcp";
|
|
4013
4010
|
}
|
|
4014
4011
|
var DEFAULT_OPENAI_MAX_TOOLS, MCP_AUTO_GROW_HARD_CAP, BUILTIN_HEADROOM, FOUNDATION_TOOL_NAMES, PROFILE_CATEGORIES, CATEGORY_KEYWORDS;
|
|
@@ -4034,9 +4031,9 @@ var init_tool_exposure = __esm({
|
|
|
4034
4031
|
CATEGORY_KEYWORDS = [
|
|
4035
4032
|
{ category: "email", patterns: [/\b(email|mail|outlook|inbox|calendar|schedule|meeting|invite|today|tomorrow)\b/i] },
|
|
4036
4033
|
{ category: "teams", patterns: [/\b(teams|chat|channel|message|dm|meeting|standup|today|mention)\b/i] },
|
|
4037
|
-
{ category: "browser", patterns: [/\b(browser|page|website|web|navigate|click|screenshot|search online)\b/i] },
|
|
4034
|
+
{ category: "browser", patterns: [/\b(browser|page|website|web|navigate|click|screenshot|snapshot|console|image|search online)\b/i] },
|
|
4038
4035
|
{ category: "files", patterns: [/\b(file|folder|directory|read|write|edit|grep|glob|find in repo|codebase)\b/i] },
|
|
4039
|
-
{ category: "system", patterns: [/\b(shell|command|terminal|run|execute|clipboard|notify|sleep|web search|fetch url)\b/i] },
|
|
4036
|
+
{ category: "system", patterns: [/\b(shell|command|terminal|run|execute|clipboard|notify|sleep|web search|fetch url|extract url|read url|article|docs?|paper|source page|news|latest|headlines|current events|breaking news)\b/i] },
|
|
4040
4037
|
{ category: "office", patterns: [/\b(word|excel|powerpoint|spreadsheet|document|presentation|slide)\b/i] },
|
|
4041
4038
|
{ category: "wechat", patterns: [/\b(wechat|weixin)\b/i] },
|
|
4042
4039
|
{ category: "memory", patterns: [/\b(memory|remember|recall|todo|preference)\b/i] }
|
|
@@ -5094,9 +5091,7 @@ ${summary}
|
|
|
5094
5091
|
if (imageData) {
|
|
5095
5092
|
const contentBlocks = [
|
|
5096
5093
|
{ type: "image", source: { type: "base64", media_type: imageData.mimeType, data: imageData.base64 } },
|
|
5097
|
-
{ type: "text", text:
|
|
5098
|
-
|
|
5099
|
-
[ATTACHED IMAGE]: The image is attached as visual input in this message. Inspect it directly; do not search the filesystem for an uploaded image path unless the user explicitly asks for a file.` }
|
|
5094
|
+
{ type: "text", text: taskText }
|
|
5100
5095
|
];
|
|
5101
5096
|
this.conversationHistory.push({ role: "user", content: contentBlocks });
|
|
5102
5097
|
} else {
|
|
@@ -5838,18 +5833,135 @@ var init_logger = __esm({
|
|
|
5838
5833
|
}
|
|
5839
5834
|
});
|
|
5840
5835
|
|
|
5836
|
+
// ../openjaw-mcp/dist/tools/url-safety.js
|
|
5837
|
+
import { isIP } from "node:net";
|
|
5838
|
+
function hasTokenLikeSecret(value) {
|
|
5839
|
+
const decoded = safeDecode(value);
|
|
5840
|
+
return TOKEN_PATTERNS.some((pattern) => pattern.test(value) || pattern.test(decoded));
|
|
5841
|
+
}
|
|
5842
|
+
function safeDecode(value) {
|
|
5843
|
+
try {
|
|
5844
|
+
return decodeURIComponent(value);
|
|
5845
|
+
} catch {
|
|
5846
|
+
return value;
|
|
5847
|
+
}
|
|
5848
|
+
}
|
|
5849
|
+
function normalizeHostname(hostname) {
|
|
5850
|
+
return hostname.replace(/^\[|\]$/g, "").replace(/\.+$/g, "").toLowerCase();
|
|
5851
|
+
}
|
|
5852
|
+
function isPrivateIPv4(host) {
|
|
5853
|
+
const parts = host.split(".").map((part) => Number(part));
|
|
5854
|
+
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255))
|
|
5855
|
+
return false;
|
|
5856
|
+
const [a, b] = parts;
|
|
5857
|
+
return a === 10 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 127 || a === 169 && b === 254 || a === 0;
|
|
5858
|
+
}
|
|
5859
|
+
function ipv4FromMappedIPv6(host) {
|
|
5860
|
+
const lower = host.toLowerCase();
|
|
5861
|
+
const dotted = /^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/.exec(lower);
|
|
5862
|
+
if (dotted)
|
|
5863
|
+
return dotted[1];
|
|
5864
|
+
const hex = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/.exec(lower);
|
|
5865
|
+
if (!hex)
|
|
5866
|
+
return void 0;
|
|
5867
|
+
const high = Number.parseInt(hex[1], 16);
|
|
5868
|
+
const low = Number.parseInt(hex[2], 16);
|
|
5869
|
+
if (!Number.isFinite(high) || !Number.isFinite(low))
|
|
5870
|
+
return void 0;
|
|
5871
|
+
return [high >> 8, high & 255, low >> 8, low & 255].join(".");
|
|
5872
|
+
}
|
|
5873
|
+
function isPrivateIPv6(host) {
|
|
5874
|
+
const lower = host.toLowerCase();
|
|
5875
|
+
const mapped = ipv4FromMappedIPv6(lower);
|
|
5876
|
+
if (mapped)
|
|
5877
|
+
return isPrivateIPv4(mapped);
|
|
5878
|
+
return lower === "::1" || lower.startsWith("fc") || lower.startsWith("fd") || lower.startsWith("fe80:");
|
|
5879
|
+
}
|
|
5880
|
+
function isMetadataUrl(input) {
|
|
5881
|
+
const url = typeof input === "string" ? new URL(input) : input;
|
|
5882
|
+
const host = normalizeHostname(url.hostname);
|
|
5883
|
+
const mapped = ipv4FromMappedIPv6(host);
|
|
5884
|
+
return METADATA_HOSTS.has(host) || METADATA_IPS.has(host) || (mapped ? METADATA_IPS.has(mapped) : false);
|
|
5885
|
+
}
|
|
5886
|
+
function isPrivateHost(hostname) {
|
|
5887
|
+
const host = normalizeHostname(hostname);
|
|
5888
|
+
if (host === "localhost" || host.endsWith(".localhost"))
|
|
5889
|
+
return true;
|
|
5890
|
+
const ipKind = isIP(host);
|
|
5891
|
+
if (ipKind === 4)
|
|
5892
|
+
return isPrivateIPv4(host);
|
|
5893
|
+
if (ipKind === 6)
|
|
5894
|
+
return isPrivateIPv6(host);
|
|
5895
|
+
return false;
|
|
5896
|
+
}
|
|
5897
|
+
function validateHttpUrl(input, options = {}) {
|
|
5898
|
+
let url;
|
|
5899
|
+
try {
|
|
5900
|
+
url = new URL(input);
|
|
5901
|
+
} catch {
|
|
5902
|
+
return { ok: false, error: "Invalid URL" };
|
|
5903
|
+
}
|
|
5904
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
5905
|
+
return { ok: false, error: "Only http:// and https:// URLs are supported" };
|
|
5906
|
+
}
|
|
5907
|
+
if (url.username || url.password) {
|
|
5908
|
+
return { ok: false, error: "Blocked: URL contains embedded credentials" };
|
|
5909
|
+
}
|
|
5910
|
+
if (hasTokenLikeSecret(url.href)) {
|
|
5911
|
+
return { ok: false, error: "Blocked: URL contains what appears to be an API key, token, password, or secret" };
|
|
5912
|
+
}
|
|
5913
|
+
if (isMetadataUrl(url)) {
|
|
5914
|
+
return { ok: false, error: "Blocked: URL targets a cloud metadata endpoint" };
|
|
5915
|
+
}
|
|
5916
|
+
if (!options.allowPrivate && isPrivateHost(url.hostname)) {
|
|
5917
|
+
return { ok: false, error: "Blocked: URL targets a private, loopback, or internal network address" };
|
|
5918
|
+
}
|
|
5919
|
+
return { ok: true, url };
|
|
5920
|
+
}
|
|
5921
|
+
var TOKEN_PATTERNS, METADATA_HOSTS, METADATA_IPS;
|
|
5922
|
+
var init_url_safety = __esm({
|
|
5923
|
+
"../openjaw-mcp/dist/tools/url-safety.js"() {
|
|
5924
|
+
"use strict";
|
|
5925
|
+
TOKEN_PATTERNS = [
|
|
5926
|
+
/\b(?:sk|ghp|github_pat|gho|ghu|ghs|glpat|xox[baprs]|ya29|AIza)[A-Za-z0-9_\-]{12,}\b/i,
|
|
5927
|
+
/(?:api[_-]?key|access[_-]?token|auth[_-]?token|bearer|secret|password|passwd|pwd|token)=([^&\s]{8,})/i
|
|
5928
|
+
];
|
|
5929
|
+
METADATA_HOSTS = /* @__PURE__ */ new Set([
|
|
5930
|
+
"metadata.google.internal",
|
|
5931
|
+
"metadata.azure.internal"
|
|
5932
|
+
]);
|
|
5933
|
+
METADATA_IPS = /* @__PURE__ */ new Set([
|
|
5934
|
+
"169.254.169.254",
|
|
5935
|
+
"100.100.100.200"
|
|
5936
|
+
]);
|
|
5937
|
+
__name(hasTokenLikeSecret, "hasTokenLikeSecret");
|
|
5938
|
+
__name(safeDecode, "safeDecode");
|
|
5939
|
+
__name(normalizeHostname, "normalizeHostname");
|
|
5940
|
+
__name(isPrivateIPv4, "isPrivateIPv4");
|
|
5941
|
+
__name(ipv4FromMappedIPv6, "ipv4FromMappedIPv6");
|
|
5942
|
+
__name(isPrivateIPv6, "isPrivateIPv6");
|
|
5943
|
+
__name(isMetadataUrl, "isMetadataUrl");
|
|
5944
|
+
__name(isPrivateHost, "isPrivateHost");
|
|
5945
|
+
__name(validateHttpUrl, "validateHttpUrl");
|
|
5946
|
+
}
|
|
5947
|
+
});
|
|
5948
|
+
|
|
5841
5949
|
// ../openjaw-mcp/dist/channels/browser.js
|
|
5842
5950
|
import * as chromeLauncher from "chrome-launcher";
|
|
5843
5951
|
import CDP from "chrome-remote-interface";
|
|
5844
5952
|
function escapeForJs(str) {
|
|
5845
5953
|
return str.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/"/g, '\\"').replace(/`/g, "\\`").replace(/\$/g, "\\$").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\0/g, "\\0").replace(/</g, "\\x3c").replace(/>/g, "\\x3e");
|
|
5846
5954
|
}
|
|
5847
|
-
var BrowserChannel;
|
|
5955
|
+
var MAX_CONSOLE_MESSAGES, MAX_JS_ERRORS, MAX_CONSOLE_TEXT, BrowserChannel;
|
|
5848
5956
|
var init_browser = __esm({
|
|
5849
5957
|
"../openjaw-mcp/dist/channels/browser.js"() {
|
|
5850
5958
|
"use strict";
|
|
5851
5959
|
init_logger();
|
|
5960
|
+
init_url_safety();
|
|
5852
5961
|
__name(escapeForJs, "escapeForJs");
|
|
5962
|
+
MAX_CONSOLE_MESSAGES = 200;
|
|
5963
|
+
MAX_JS_ERRORS = 100;
|
|
5964
|
+
MAX_CONSOLE_TEXT = 4e3;
|
|
5853
5965
|
BrowserChannel = class {
|
|
5854
5966
|
static {
|
|
5855
5967
|
__name(this, "BrowserChannel");
|
|
@@ -5859,6 +5971,10 @@ var init_browser = __esm({
|
|
|
5859
5971
|
client = null;
|
|
5860
5972
|
page = null;
|
|
5861
5973
|
launchPromise = null;
|
|
5974
|
+
currentSnapshotId = null;
|
|
5975
|
+
currentRefs = /* @__PURE__ */ new Map();
|
|
5976
|
+
consoleBuffer = [];
|
|
5977
|
+
jsErrorBuffer = [];
|
|
5862
5978
|
constructor(config) {
|
|
5863
5979
|
this.config = config;
|
|
5864
5980
|
}
|
|
@@ -5905,6 +6021,8 @@ var init_browser = __esm({
|
|
|
5905
6021
|
this.client = null;
|
|
5906
6022
|
this.page = null;
|
|
5907
6023
|
this.launchPromise = null;
|
|
6024
|
+
this.consoleBuffer = [];
|
|
6025
|
+
this.jsErrorBuffer = [];
|
|
5908
6026
|
logger_default.info("Browser connection state reset");
|
|
5909
6027
|
}
|
|
5910
6028
|
/**
|
|
@@ -5957,13 +6075,71 @@ var init_browser = __esm({
|
|
|
5957
6075
|
DOM.enable(),
|
|
5958
6076
|
Network.enable()
|
|
5959
6077
|
]);
|
|
6078
|
+
this.attachRuntimeEventBuffers(Runtime);
|
|
6079
|
+
await this.attachRequestSafetyInterception(this.client.Fetch);
|
|
5960
6080
|
this.page = { Runtime, Page, DOM, Input };
|
|
5961
6081
|
logger_default.info("Browser launched", { port: this.chrome.port });
|
|
5962
6082
|
}
|
|
6083
|
+
async attachRequestSafetyInterception(Fetch) {
|
|
6084
|
+
const fetchDomain = Fetch;
|
|
6085
|
+
if (!fetchDomain?.enable || !fetchDomain.requestPaused || !fetchDomain.failRequest || !fetchDomain.continueRequest) {
|
|
6086
|
+
logger_default.debug("Browser Fetch domain unavailable; navigation safety falls back to pre/final URL checks");
|
|
6087
|
+
return;
|
|
6088
|
+
}
|
|
6089
|
+
fetchDomain.requestPaused((event) => {
|
|
6090
|
+
void (async () => {
|
|
6091
|
+
try {
|
|
6092
|
+
const url = new URL(event.request.url);
|
|
6093
|
+
if (url.protocol === "http:" || url.protocol === "https:") {
|
|
6094
|
+
const safety = validateHttpUrl(url.href, { allowPrivate: true });
|
|
6095
|
+
if (!safety.ok) {
|
|
6096
|
+
await fetchDomain.failRequest({ requestId: event.requestId, errorReason: "BlockedByClient" });
|
|
6097
|
+
return;
|
|
6098
|
+
}
|
|
6099
|
+
}
|
|
6100
|
+
await fetchDomain.continueRequest({ requestId: event.requestId });
|
|
6101
|
+
} catch {
|
|
6102
|
+
await fetchDomain.continueRequest({ requestId: event.requestId }).catch(() => void 0);
|
|
6103
|
+
}
|
|
6104
|
+
})();
|
|
6105
|
+
});
|
|
6106
|
+
await fetchDomain.enable({ patterns: [{ urlPattern: "*", requestStage: "Request" }] });
|
|
6107
|
+
}
|
|
6108
|
+
attachRuntimeEventBuffers(Runtime) {
|
|
6109
|
+
const runtime = Runtime;
|
|
6110
|
+
runtime.consoleAPICalled?.((event) => {
|
|
6111
|
+
const text = (event.args ?? []).map((arg) => String(arg.value ?? arg.description ?? "")).join(" ").slice(0, MAX_CONSOLE_TEXT);
|
|
6112
|
+
this.consoleBuffer.push({
|
|
6113
|
+
type: event.type ?? "log",
|
|
6114
|
+
text,
|
|
6115
|
+
timestamp: event.timestamp ?? Date.now()
|
|
6116
|
+
});
|
|
6117
|
+
if (this.consoleBuffer.length > MAX_CONSOLE_MESSAGES) {
|
|
6118
|
+
this.consoleBuffer.splice(0, this.consoleBuffer.length - MAX_CONSOLE_MESSAGES);
|
|
6119
|
+
}
|
|
6120
|
+
});
|
|
6121
|
+
runtime.exceptionThrown?.((event) => {
|
|
6122
|
+
const details = event.exceptionDetails ?? {};
|
|
6123
|
+
this.jsErrorBuffer.push({
|
|
6124
|
+
message: String(details.exception?.description ?? details.exception?.value ?? details.text ?? "JavaScript exception").slice(0, MAX_CONSOLE_TEXT),
|
|
6125
|
+
url: details.url,
|
|
6126
|
+
line: details.lineNumber,
|
|
6127
|
+
column: details.columnNumber,
|
|
6128
|
+
timestamp: event.timestamp ?? Date.now()
|
|
6129
|
+
});
|
|
6130
|
+
if (this.jsErrorBuffer.length > MAX_JS_ERRORS) {
|
|
6131
|
+
this.jsErrorBuffer.splice(0, this.jsErrorBuffer.length - MAX_JS_ERRORS);
|
|
6132
|
+
}
|
|
6133
|
+
});
|
|
6134
|
+
}
|
|
5963
6135
|
async navigate(options) {
|
|
5964
6136
|
await this.ensureBrowser();
|
|
5965
6137
|
const { Page, Runtime } = this.page;
|
|
5966
|
-
|
|
6138
|
+
const initialSafety = validateHttpUrl(options.url, { allowPrivate: true });
|
|
6139
|
+
if (!initialSafety.ok) {
|
|
6140
|
+
return { url: options.url, title: "", snapshot: "", error: initialSafety.error };
|
|
6141
|
+
}
|
|
6142
|
+
await Page.navigate({ url: initialSafety.url.href });
|
|
5967
6143
|
if (options.waitFor === "load") {
|
|
5968
6144
|
await Page.loadEventFired();
|
|
5969
6145
|
} else if (options.waitFor === "domcontentloaded") {
|
|
@@ -5975,15 +6151,23 @@ var init_browser = __esm({
|
|
|
5975
6151
|
const result = await Runtime.evaluate({
|
|
5976
6152
|
expression: "document.title"
|
|
5977
6153
|
});
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
6154
|
+
const url = await this.getCurrentUrl() ?? initialSafety.url.href;
|
|
6155
|
+
const finalSafety = validateHttpUrl(url, { allowPrivate: true });
|
|
6156
|
+
if (!finalSafety.ok) {
|
|
6157
|
+
return { url, title: result.result.value, snapshot: "", error: `Blocked final URL: ${finalSafety.error}` };
|
|
6158
|
+
}
|
|
6159
|
+
const title = result.result.value;
|
|
6160
|
+
const snapshot = await this.snapshot({ full: false });
|
|
6161
|
+
return { ...snapshot, url, title };
|
|
5982
6162
|
}
|
|
5983
6163
|
async click(options) {
|
|
5984
6164
|
await this.ensureBrowser();
|
|
5985
6165
|
const { Runtime } = this.page;
|
|
5986
|
-
const
|
|
6166
|
+
const resolvedSelector = options.ref ? this.resolveRef(options.ref, options.snapshotId) : options.selector;
|
|
6167
|
+
if (options.ref && !resolvedSelector) {
|
|
6168
|
+
return { success: false, element: `stale or unknown ref ${options.ref}; call browser_snapshot again` };
|
|
6169
|
+
}
|
|
6170
|
+
const findExpr = resolvedSelector ? `document.querySelector('${escapeForJs(resolvedSelector)}')` : options.text ? `(() => {
|
|
5987
6171
|
const isVis = (e) => {
|
|
5988
6172
|
if (!e.offsetParent && e.tagName !== 'BODY' && e.tagName !== 'HTML') return false;
|
|
5989
6173
|
const s = getComputedStyle(e);
|
|
@@ -6107,9 +6291,13 @@ var init_browser = __esm({
|
|
|
6107
6291
|
async type(options) {
|
|
6108
6292
|
await this.ensureBrowser();
|
|
6109
6293
|
const { Runtime } = this.page;
|
|
6294
|
+
const resolvedSelector = options.ref ? this.resolveRef(options.ref, options.snapshotId) : options.selector;
|
|
6295
|
+
if (!resolvedSelector) {
|
|
6296
|
+
return { success: false, error: options.ref ? `stale or unknown ref ${options.ref}; call browser_snapshot again` : "No selector or ref provided" };
|
|
6297
|
+
}
|
|
6110
6298
|
const script = `
|
|
6111
6299
|
(() => {
|
|
6112
|
-
const el = document.querySelector('${escapeForJs(
|
|
6300
|
+
const el = document.querySelector('${escapeForJs(resolvedSelector)}');
|
|
6113
6301
|
if (el) {
|
|
6114
6302
|
${options.clear ? "el.value = '';" : ""}
|
|
6115
6303
|
el.value = '${escapeForJs(options.text)}';
|
|
@@ -6191,6 +6379,141 @@ var init_browser = __esm({
|
|
|
6191
6379
|
const result = await Runtime.evaluate({ expression: script, returnByValue: true });
|
|
6192
6380
|
return { content: result.result.value };
|
|
6193
6381
|
}
|
|
6382
|
+
async snapshot(options) {
|
|
6383
|
+
await this.ensureBrowser();
|
|
6384
|
+
const { Runtime } = this.page;
|
|
6385
|
+
const scope = options?.selector ? `'${escapeForJs(options.selector)}'` : `'body'`;
|
|
6386
|
+
const maxElements = Math.min(Math.max(options?.maxElements ?? 80, 1), 200);
|
|
6387
|
+
const textLimit = options?.full ? 8e3 : 1200;
|
|
6388
|
+
const script = `(() => {
|
|
6389
|
+
const root = document.querySelector(${scope});
|
|
6390
|
+
const cssEscape = (value) => globalThis.CSS?.escape
|
|
6391
|
+
? globalThis.CSS.escape(String(value))
|
|
6392
|
+
: String(value).replace(/[^a-zA-Z0-9_-]/g, ch => '\\\\' + ch.charCodeAt(0).toString(16) + ' ');
|
|
6393
|
+
const selectorFor = (el) => {
|
|
6394
|
+
if (el.id) return '#' + cssEscape(el.id);
|
|
6395
|
+
const parts = [];
|
|
6396
|
+
let cur = el;
|
|
6397
|
+
while (cur && cur.nodeType === 1 && cur !== document.body) {
|
|
6398
|
+
let part = cur.tagName.toLowerCase();
|
|
6399
|
+
const parent = cur.parentElement;
|
|
6400
|
+
if (!parent) break;
|
|
6401
|
+
const same = Array.from(parent.children).filter(child => child.tagName === cur.tagName);
|
|
6402
|
+
if (same.length > 1) part += ':nth-of-type(' + (same.indexOf(cur) + 1) + ')';
|
|
6403
|
+
parts.unshift(part);
|
|
6404
|
+
cur = parent;
|
|
6405
|
+
}
|
|
6406
|
+
return parts.length ? parts.join(' > ') : 'body';
|
|
6407
|
+
};
|
|
6408
|
+
const isVisible = (el) => {
|
|
6409
|
+
const rect = el.getBoundingClientRect();
|
|
6410
|
+
const style = getComputedStyle(el);
|
|
6411
|
+
return rect.width > 0 && rect.height > 0 && style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0';
|
|
6412
|
+
};
|
|
6413
|
+
const disabled = (el) => Boolean(el.disabled || el.getAttribute('aria-disabled') === 'true');
|
|
6414
|
+
if (!root) return JSON.stringify({ url: location.href, title: document.title, elements: [], text: '' });
|
|
6415
|
+
const nodes = Array.from(root.querySelectorAll('button, a[href], input, select, textarea, summary, [role="button"], [role="link"], [role="textbox"], [onclick], [tabindex]'));
|
|
6416
|
+
const elements = [];
|
|
6417
|
+
for (const el of nodes) {
|
|
6418
|
+
if (elements.length >= ${maxElements}) break;
|
|
6419
|
+
const tag = el.tagName.toLowerCase();
|
|
6420
|
+
const type = el.getAttribute('type') || '';
|
|
6421
|
+
const role = el.getAttribute('role') || (type ? tag + '[' + type + ']' : tag);
|
|
6422
|
+
const text = (el.innerText || el.textContent || '').replace(/s+/g, ' ').trim().slice(0, 120);
|
|
6423
|
+
const label = (el.getAttribute('aria-label') || el.getAttribute('title') || el.getAttribute('placeholder') || text || el.getAttribute('value') || '').replace(/s+/g, ' ').trim().slice(0, 120);
|
|
6424
|
+
elements.push({
|
|
6425
|
+
ref: '@e' + (elements.length + 1),
|
|
6426
|
+
selector: selectorFor(el),
|
|
6427
|
+
role,
|
|
6428
|
+
tag,
|
|
6429
|
+
label,
|
|
6430
|
+
text,
|
|
6431
|
+
visible: isVisible(el),
|
|
6432
|
+
disabled: disabled(el),
|
|
6433
|
+
});
|
|
6434
|
+
}
|
|
6435
|
+
const pageText = (root.innerText || root.textContent || '').replace(/s+/g, ' ').trim().slice(0, ${textLimit});
|
|
6436
|
+
return JSON.stringify({ url: location.href, title: document.title, elements, text: pageText });
|
|
6437
|
+
})()`;
|
|
6438
|
+
const result = await Runtime.evaluate({ expression: script, returnByValue: true });
|
|
6439
|
+
const parsed = JSON.parse(result.result.value || "{}");
|
|
6440
|
+
const snapshotId = `snap_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
6441
|
+
const elements = parsed.elements ?? [];
|
|
6442
|
+
const refs = {};
|
|
6443
|
+
this.currentRefs.clear();
|
|
6444
|
+
for (const element of elements) {
|
|
6445
|
+
refs[element.ref] = element.selector;
|
|
6446
|
+
this.currentRefs.set(element.ref, element.selector);
|
|
6447
|
+
}
|
|
6448
|
+
this.currentSnapshotId = snapshotId;
|
|
6449
|
+
const lines = [
|
|
6450
|
+
`URL: ${parsed.url ?? ""}`,
|
|
6451
|
+
`Title: ${parsed.title ?? ""}`,
|
|
6452
|
+
`Snapshot: ${snapshotId}`,
|
|
6453
|
+
"",
|
|
6454
|
+
`Interactive elements (${elements.length}):`,
|
|
6455
|
+
...elements.map((element) => `${element.ref} <${element.role}>${element.visible ? "" : " [hidden]"}${element.disabled ? " [disabled]" : ""} ${JSON.stringify(element.label || element.text || element.selector)}`),
|
|
6456
|
+
"",
|
|
6457
|
+
"Visible text:",
|
|
6458
|
+
parsed.text ?? ""
|
|
6459
|
+
];
|
|
6460
|
+
return {
|
|
6461
|
+
url: parsed.url ?? "",
|
|
6462
|
+
title: parsed.title ?? "",
|
|
6463
|
+
snapshot: lines.join("\n").trim(),
|
|
6464
|
+
snapshot_id: snapshotId,
|
|
6465
|
+
refs,
|
|
6466
|
+
elements,
|
|
6467
|
+
text: parsed.text ?? "",
|
|
6468
|
+
element_count: elements.length
|
|
6469
|
+
};
|
|
6470
|
+
}
|
|
6471
|
+
resolveRef(ref, snapshotId) {
|
|
6472
|
+
if (snapshotId && this.currentSnapshotId && snapshotId !== this.currentSnapshotId)
|
|
6473
|
+
return void 0;
|
|
6474
|
+
return this.currentRefs.get(ref);
|
|
6475
|
+
}
|
|
6476
|
+
async back() {
|
|
6477
|
+
await this.ensureBrowser();
|
|
6478
|
+
const { Page, Runtime } = this.page;
|
|
6479
|
+
const history = await Page.getNavigationHistory();
|
|
6480
|
+
if (history.currentIndex > 0) {
|
|
6481
|
+
await Page.navigateToHistoryEntry({ entryId: history.entries[history.currentIndex - 1].id });
|
|
6482
|
+
await Page.loadEventFired().catch(() => void 0);
|
|
6483
|
+
}
|
|
6484
|
+
const title = await Runtime.evaluate({ expression: "document.title", returnByValue: true });
|
|
6485
|
+
return { url: await this.getCurrentUrl(), title: title.result.value };
|
|
6486
|
+
}
|
|
6487
|
+
async press(key) {
|
|
6488
|
+
await this.sendKey(key);
|
|
6489
|
+
return { success: true, key };
|
|
6490
|
+
}
|
|
6491
|
+
async getImages() {
|
|
6492
|
+
await this.ensureBrowser();
|
|
6493
|
+
const { Runtime } = this.page;
|
|
6494
|
+
const script = `JSON.stringify(Array.from(document.images).slice(0, 100).map(img => {
|
|
6495
|
+
const rect = img.getBoundingClientRect();
|
|
6496
|
+
return { src: img.currentSrc || img.src, alt: img.alt || '', width: img.naturalWidth || rect.width || 0, height: img.naturalHeight || rect.height || 0, visible: rect.width > 0 && rect.height > 0 };
|
|
6497
|
+
}).filter(img => img.src && !img.src.startsWith('data:')))`;
|
|
6498
|
+
const result = await Runtime.evaluate({ expression: script, returnByValue: true });
|
|
6499
|
+
const images = JSON.parse(result.result.value || "[]");
|
|
6500
|
+
return { images, count: images.length };
|
|
6501
|
+
}
|
|
6502
|
+
async consoleMessages(clear2 = false) {
|
|
6503
|
+
const consoleMessages = [...this.consoleBuffer];
|
|
6504
|
+
const jsErrors = [...this.jsErrorBuffer];
|
|
6505
|
+
if (clear2) {
|
|
6506
|
+
this.consoleBuffer = [];
|
|
6507
|
+
this.jsErrorBuffer = [];
|
|
6508
|
+
}
|
|
6509
|
+
return {
|
|
6510
|
+
console_messages: consoleMessages,
|
|
6511
|
+
js_errors: jsErrors,
|
|
6512
|
+
total_messages: consoleMessages.length,
|
|
6513
|
+
total_errors: jsErrors.length,
|
|
6514
|
+
note: clear2 ? "Returned and cleared buffered console messages." : "Returned buffered console messages."
|
|
6515
|
+
};
|
|
6516
|
+
}
|
|
6194
6517
|
async evaluate(script) {
|
|
6195
6518
|
await this.ensureBrowser();
|
|
6196
6519
|
const { Runtime } = this.page;
|
|
@@ -6625,18 +6948,48 @@ function createBrowseTools(config, sharedBrowser) {
|
|
|
6625
6948
|
required: ["url"]
|
|
6626
6949
|
},
|
|
6627
6950
|
execute: /* @__PURE__ */ __name(async (input) => {
|
|
6951
|
+
const safety = validateHttpUrl(input.url, { allowPrivate: true });
|
|
6952
|
+
if (!safety.ok)
|
|
6953
|
+
return { error: safety.error, url: input.url };
|
|
6628
6954
|
return await browser.navigate({
|
|
6629
6955
|
url: input.url,
|
|
6630
6956
|
waitFor: input.wait_for ?? "load"
|
|
6631
6957
|
});
|
|
6632
6958
|
}, "execute")
|
|
6633
6959
|
},
|
|
6960
|
+
{
|
|
6961
|
+
name: "browser_snapshot",
|
|
6962
|
+
description: "Get a compact snapshot of the current page with stable refs like @e1 for browser_click and browser_type. Use after navigation or after interactions that changed the page.",
|
|
6963
|
+
parameters: {
|
|
6964
|
+
type: "object",
|
|
6965
|
+
properties: {
|
|
6966
|
+
full: { type: "boolean", description: "Include more visible page text (default false)", default: false },
|
|
6967
|
+
selector: { type: "string", description: "Optional CSS selector to scope the snapshot" },
|
|
6968
|
+
max_elements: { type: "number", description: "Maximum interactive elements to include (default 80, max 200)" }
|
|
6969
|
+
}
|
|
6970
|
+
},
|
|
6971
|
+
execute: /* @__PURE__ */ __name(async (input) => {
|
|
6972
|
+
return await browser.snapshot({
|
|
6973
|
+
full: input.full === true,
|
|
6974
|
+
selector: input.selector,
|
|
6975
|
+
maxElements: input.max_elements
|
|
6976
|
+
});
|
|
6977
|
+
}, "execute")
|
|
6978
|
+
},
|
|
6634
6979
|
{
|
|
6635
6980
|
name: "browser_click",
|
|
6636
|
-
description: "Click an element on the page",
|
|
6981
|
+
description: "Click an element on the page. Prefer ref from browser_snapshot (e.g. @e5). Legacy selector/text targeting is still supported.",
|
|
6637
6982
|
parameters: {
|
|
6638
6983
|
type: "object",
|
|
6639
6984
|
properties: {
|
|
6985
|
+
ref: {
|
|
6986
|
+
type: "string",
|
|
6987
|
+
description: "Element ref from browser_snapshot, e.g. @e5"
|
|
6988
|
+
},
|
|
6989
|
+
snapshot_id: {
|
|
6990
|
+
type: "string",
|
|
6991
|
+
description: "Optional snapshot_id associated with the ref. If stale, refresh with browser_snapshot."
|
|
6992
|
+
},
|
|
6640
6993
|
selector: {
|
|
6641
6994
|
type: "string",
|
|
6642
6995
|
description: "CSS selector of element to click"
|
|
@@ -6649,6 +7002,8 @@ function createBrowseTools(config, sharedBrowser) {
|
|
|
6649
7002
|
},
|
|
6650
7003
|
execute: /* @__PURE__ */ __name(async (input) => {
|
|
6651
7004
|
return await browser.click({
|
|
7005
|
+
ref: input.ref,
|
|
7006
|
+
snapshotId: input.snapshot_id,
|
|
6652
7007
|
selector: input.selector,
|
|
6653
7008
|
text: input.text
|
|
6654
7009
|
});
|
|
@@ -6656,10 +7011,18 @@ function createBrowseTools(config, sharedBrowser) {
|
|
|
6656
7011
|
},
|
|
6657
7012
|
{
|
|
6658
7013
|
name: "browser_type",
|
|
6659
|
-
description: "Type text into an input element",
|
|
7014
|
+
description: "Type text into an input element. Prefer ref from browser_snapshot (e.g. @e3). Legacy selector targeting is still supported.",
|
|
6660
7015
|
parameters: {
|
|
6661
7016
|
type: "object",
|
|
6662
7017
|
properties: {
|
|
7018
|
+
ref: {
|
|
7019
|
+
type: "string",
|
|
7020
|
+
description: "Element ref from browser_snapshot, e.g. @e3"
|
|
7021
|
+
},
|
|
7022
|
+
snapshot_id: {
|
|
7023
|
+
type: "string",
|
|
7024
|
+
description: "Optional snapshot_id associated with the ref. If stale, refresh with browser_snapshot."
|
|
7025
|
+
},
|
|
6663
7026
|
selector: {
|
|
6664
7027
|
type: "string",
|
|
6665
7028
|
description: "CSS selector of input element"
|
|
@@ -6679,10 +7042,12 @@ function createBrowseTools(config, sharedBrowser) {
|
|
|
6679
7042
|
default: false
|
|
6680
7043
|
}
|
|
6681
7044
|
},
|
|
6682
|
-
required: ["
|
|
7045
|
+
required: ["text"]
|
|
6683
7046
|
},
|
|
6684
7047
|
execute: /* @__PURE__ */ __name(async (input) => {
|
|
6685
7048
|
return await browser.type({
|
|
7049
|
+
ref: input.ref,
|
|
7050
|
+
snapshotId: input.snapshot_id,
|
|
6686
7051
|
selector: input.selector,
|
|
6687
7052
|
text: input.text,
|
|
6688
7053
|
clear: input.clear ?? true,
|
|
@@ -6690,6 +7055,24 @@ function createBrowseTools(config, sharedBrowser) {
|
|
|
6690
7055
|
});
|
|
6691
7056
|
}, "execute")
|
|
6692
7057
|
},
|
|
7058
|
+
{
|
|
7059
|
+
name: "browser_back",
|
|
7060
|
+
description: "Navigate back to the previous page in browser history.",
|
|
7061
|
+
parameters: { type: "object", properties: {} },
|
|
7062
|
+
execute: /* @__PURE__ */ __name(async () => await browser.back(), "execute")
|
|
7063
|
+
},
|
|
7064
|
+
{
|
|
7065
|
+
name: "browser_press",
|
|
7066
|
+
description: "Press a keyboard key in the browser, e.g. Enter, Tab, Escape, ArrowDown.",
|
|
7067
|
+
parameters: {
|
|
7068
|
+
type: "object",
|
|
7069
|
+
properties: {
|
|
7070
|
+
key: { type: "string", description: "Key to press, e.g. Enter, Tab, Escape, ArrowDown" }
|
|
7071
|
+
},
|
|
7072
|
+
required: ["key"]
|
|
7073
|
+
},
|
|
7074
|
+
execute: /* @__PURE__ */ __name(async (input) => await browser.press(input.key), "execute")
|
|
7075
|
+
},
|
|
6693
7076
|
{
|
|
6694
7077
|
name: "browser_extract",
|
|
6695
7078
|
description: "Extract text content from the page",
|
|
@@ -6734,6 +7117,51 @@ function createBrowseTools(config, sharedBrowser) {
|
|
|
6734
7117
|
return await browser.evaluate(input.script);
|
|
6735
7118
|
}, "execute")
|
|
6736
7119
|
},
|
|
7120
|
+
{
|
|
7121
|
+
name: "browser_console",
|
|
7122
|
+
description: "Read browser console logs and JavaScript errors. This tool is read-only; use browser_evaluate for JavaScript execution.",
|
|
7123
|
+
parameters: {
|
|
7124
|
+
type: "object",
|
|
7125
|
+
properties: {
|
|
7126
|
+
clear: { type: "boolean", description: "Clear buffered logs after reading, if buffering is active", default: false }
|
|
7127
|
+
}
|
|
7128
|
+
},
|
|
7129
|
+
execute: /* @__PURE__ */ __name(async (input) => await browser.consoleMessages(input.clear === true), "execute")
|
|
7130
|
+
},
|
|
7131
|
+
{
|
|
7132
|
+
name: "browser_get_images",
|
|
7133
|
+
description: "List images on the current page with URLs, alt text, dimensions, and visibility. Use before visual analysis or downloading images.",
|
|
7134
|
+
parameters: { type: "object", properties: {} },
|
|
7135
|
+
execute: /* @__PURE__ */ __name(async () => await browser.getImages(), "execute")
|
|
7136
|
+
},
|
|
7137
|
+
{
|
|
7138
|
+
name: "browser_vision",
|
|
7139
|
+
description: "Take a browser screenshot for visual inspection. Returns a screenshot path plus page metadata. Use for visual layouts, CAPTCHAs, screenshots, or image-heavy pages.",
|
|
7140
|
+
parameters: {
|
|
7141
|
+
type: "object",
|
|
7142
|
+
properties: {
|
|
7143
|
+
question: { type: "string", description: "What you want to inspect visually" },
|
|
7144
|
+
annotate: { type: "boolean", description: "Reserved for future ref overlays; currently returns a normal screenshot", default: false }
|
|
7145
|
+
}
|
|
7146
|
+
},
|
|
7147
|
+
execute: /* @__PURE__ */ __name(async (input) => {
|
|
7148
|
+
const { join: join47 } = await import("node:path");
|
|
7149
|
+
const { tmpdir: tmpdir13 } = await import("node:os");
|
|
7150
|
+
const { randomUUID: randomUUID14 } = await import("node:crypto");
|
|
7151
|
+
const screenshotPath = join47(tmpdir13(), `openjaw-browser-${randomUUID14().slice(0, 8)}.png`);
|
|
7152
|
+
const screenshot = await browser.screenshot({ fullPage: false, path: screenshotPath });
|
|
7153
|
+
const snapshot = await browser.snapshot({ full: false });
|
|
7154
|
+
return {
|
|
7155
|
+
message: "Browser screenshot captured for visual inspection",
|
|
7156
|
+
question: input.question,
|
|
7157
|
+
snapshot_id: snapshot.snapshot_id,
|
|
7158
|
+
url: snapshot.url,
|
|
7159
|
+
title: snapshot.title,
|
|
7160
|
+
screenshotPath: screenshot.path ?? screenshotPath,
|
|
7161
|
+
imagePayload: "not_attached: screenshot path returned to avoid sending images to non-vision providers"
|
|
7162
|
+
};
|
|
7163
|
+
}, "execute")
|
|
7164
|
+
},
|
|
6737
7165
|
{
|
|
6738
7166
|
name: "browser_wait",
|
|
6739
7167
|
description: "Wait for an element to appear, text to change, or element to hide. Use before interacting with dynamic/slow-loading pages.",
|
|
@@ -6819,6 +7247,7 @@ var init_browse = __esm({
|
|
|
6819
7247
|
"../openjaw-mcp/dist/tools/browse.js"() {
|
|
6820
7248
|
"use strict";
|
|
6821
7249
|
init_browser();
|
|
7250
|
+
init_url_safety();
|
|
6822
7251
|
__name(createBrowseTools, "createBrowseTools");
|
|
6823
7252
|
}
|
|
6824
7253
|
});
|
|
@@ -15750,6 +16179,175 @@ function urlMatchesDomain(url, domain) {
|
|
|
15750
16179
|
return false;
|
|
15751
16180
|
}
|
|
15752
16181
|
}
|
|
16182
|
+
function normalizeSearchResults(results, limit) {
|
|
16183
|
+
return results.slice(0, limit).map((result, index) => ({
|
|
16184
|
+
...result,
|
|
16185
|
+
snippet: result.snippet ?? "",
|
|
16186
|
+
description: result.snippet ?? "",
|
|
16187
|
+
position: index + 1
|
|
16188
|
+
}));
|
|
16189
|
+
}
|
|
16190
|
+
function clampNumber(value, fallback, min, max) {
|
|
16191
|
+
const numberValue = typeof value === "number" ? value : Number(value);
|
|
16192
|
+
if (!Number.isFinite(numberValue))
|
|
16193
|
+
return fallback;
|
|
16194
|
+
return Math.min(Math.max(Math.trunc(numberValue), min), max);
|
|
16195
|
+
}
|
|
16196
|
+
function extractHtmlTitle(html) {
|
|
16197
|
+
const match = /<title[^>]*>([\s\S]*?)<\/title>/i.exec(html);
|
|
16198
|
+
if (!match)
|
|
16199
|
+
return void 0;
|
|
16200
|
+
return DECODE_ENTITIES(match[1].replace(/\s+/g, " ").trim()) || void 0;
|
|
16201
|
+
}
|
|
16202
|
+
function cleanHtmlForMarkdown(html) {
|
|
16203
|
+
return html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "").replace(/<noscript[^>]*>[\s\S]*?<\/noscript>/gi, "").replace(/<svg[^>]*>[\s\S]*?<\/svg>/gi, "").replace(/<nav[^>]*>[\s\S]*?<\/nav>/gi, "").replace(/<footer[^>]*>[\s\S]*?<\/footer>/gi, "").replace(/<header[^>]*>[\s\S]*?<\/header>/gi, "");
|
|
16204
|
+
}
|
|
16205
|
+
function htmlToText(html) {
|
|
16206
|
+
return DECODE_ENTITIES(cleanHtmlForMarkdown(html).replace(/<br\s*\/?>/gi, "\n").replace(/<\/p>/gi, "\n\n").replace(/<\/h[1-6]>/gi, "\n\n").replace(/<[^>]+>/g, " ").replace(/[ \t]+/g, " ").replace(/\n\s+/g, "\n").replace(/\n{3,}/g, "\n\n").trim());
|
|
16207
|
+
}
|
|
16208
|
+
function isPdfLike(url, contentType) {
|
|
16209
|
+
if (/application\/pdf/i.test(contentType))
|
|
16210
|
+
return true;
|
|
16211
|
+
try {
|
|
16212
|
+
return new URL(url).pathname.toLowerCase().endsWith(".pdf");
|
|
16213
|
+
} catch {
|
|
16214
|
+
return /\.pdf(?:$|[?#])/i.test(url);
|
|
16215
|
+
}
|
|
16216
|
+
}
|
|
16217
|
+
async function htmlToMarkdown(html) {
|
|
16218
|
+
try {
|
|
16219
|
+
const TurndownModule = await import("turndown");
|
|
16220
|
+
const TurndownService = TurndownModule.default || TurndownModule;
|
|
16221
|
+
const turndown = new TurndownService({ headingStyle: "atx", codeBlockStyle: "fenced" });
|
|
16222
|
+
return turndown.turndown(cleanHtmlForMarkdown(html));
|
|
16223
|
+
} catch {
|
|
16224
|
+
return cleanHtmlForMarkdown(html).replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
16225
|
+
}
|
|
16226
|
+
}
|
|
16227
|
+
function paginateContent(text, startIndex, maxLength) {
|
|
16228
|
+
const safeStart = clampNumber(startIndex, 0, 0, Math.max(0, text.length));
|
|
16229
|
+
const safeMax = clampNumber(maxLength, 8e3, 1, 1e5);
|
|
16230
|
+
const content = text.slice(safeStart, safeStart + safeMax);
|
|
16231
|
+
const nextStartIndex = safeStart + content.length < text.length ? safeStart + content.length : void 0;
|
|
16232
|
+
return {
|
|
16233
|
+
content,
|
|
16234
|
+
contentLength: text.length,
|
|
16235
|
+
truncated: nextStartIndex !== void 0,
|
|
16236
|
+
startIndex: safeStart,
|
|
16237
|
+
...nextStartIndex !== void 0 && { nextStartIndex }
|
|
16238
|
+
};
|
|
16239
|
+
}
|
|
16240
|
+
async function fetchWithValidatedRedirects(startUrl, options) {
|
|
16241
|
+
let current = startUrl;
|
|
16242
|
+
const maxRedirects = options.maxRedirects ?? 10;
|
|
16243
|
+
for (let redirects = 0; redirects <= maxRedirects; redirects++) {
|
|
16244
|
+
const response = await fetch(current.href, {
|
|
16245
|
+
signal: options.signal,
|
|
16246
|
+
headers: options.headers,
|
|
16247
|
+
redirect: "manual"
|
|
16248
|
+
});
|
|
16249
|
+
if (response.status < 300 || response.status >= 400) {
|
|
16250
|
+
return { response, finalUrl: current.href };
|
|
16251
|
+
}
|
|
16252
|
+
const location = response.headers.get("location");
|
|
16253
|
+
if (!location) {
|
|
16254
|
+
return { response, finalUrl: current.href };
|
|
16255
|
+
}
|
|
16256
|
+
const nextUrl = new URL(location, current.href);
|
|
16257
|
+
const nextSafety = validateHttpUrl(nextUrl.href, { allowPrivate: options.allowPrivate });
|
|
16258
|
+
if (!nextSafety.ok) {
|
|
16259
|
+
return { finalUrl: nextUrl.href, error: `Blocked redirect: ${nextSafety.error}` };
|
|
16260
|
+
}
|
|
16261
|
+
current = nextSafety.url;
|
|
16262
|
+
}
|
|
16263
|
+
return { finalUrl: current.href, error: `Too many redirects (>${maxRedirects})` };
|
|
16264
|
+
}
|
|
16265
|
+
async function extractUrlContent(inputUrl, options) {
|
|
16266
|
+
const initial = validateHttpUrl(inputUrl, { allowPrivate: options.allowPrivate });
|
|
16267
|
+
if (!initial.ok) {
|
|
16268
|
+
return {
|
|
16269
|
+
url: inputUrl,
|
|
16270
|
+
finalUrl: inputUrl,
|
|
16271
|
+
contentType: "",
|
|
16272
|
+
content: "",
|
|
16273
|
+
contentLength: 0,
|
|
16274
|
+
truncated: false,
|
|
16275
|
+
startIndex: options.startIndex,
|
|
16276
|
+
error: initial.error
|
|
16277
|
+
};
|
|
16278
|
+
}
|
|
16279
|
+
const controller = new AbortController();
|
|
16280
|
+
const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? 2e4);
|
|
16281
|
+
try {
|
|
16282
|
+
const fetched = await fetchWithValidatedRedirects(initial.url, {
|
|
16283
|
+
signal: controller.signal,
|
|
16284
|
+
headers: { "User-Agent": "OpenJaw-Agent/1.0" },
|
|
16285
|
+
allowPrivate: options.allowPrivate
|
|
16286
|
+
});
|
|
16287
|
+
const finalUrl = fetched.finalUrl;
|
|
16288
|
+
if (fetched.error || !fetched.response) {
|
|
16289
|
+
return {
|
|
16290
|
+
url: inputUrl,
|
|
16291
|
+
finalUrl,
|
|
16292
|
+
contentType: "",
|
|
16293
|
+
content: "",
|
|
16294
|
+
contentLength: 0,
|
|
16295
|
+
truncated: false,
|
|
16296
|
+
startIndex: options.startIndex,
|
|
16297
|
+
error: fetched.error ?? "Fetch failed before response"
|
|
16298
|
+
};
|
|
16299
|
+
}
|
|
16300
|
+
const response = fetched.response;
|
|
16301
|
+
if (!response.ok) {
|
|
16302
|
+
return {
|
|
16303
|
+
url: inputUrl,
|
|
16304
|
+
finalUrl,
|
|
16305
|
+
contentType: response.headers.get("content-type") || "",
|
|
16306
|
+
content: "",
|
|
16307
|
+
contentLength: 0,
|
|
16308
|
+
truncated: false,
|
|
16309
|
+
startIndex: options.startIndex,
|
|
16310
|
+
error: `HTTP ${response.status}: ${response.statusText}`
|
|
16311
|
+
};
|
|
16312
|
+
}
|
|
16313
|
+
const contentType = response.headers.get("content-type") || "";
|
|
16314
|
+
if (isPdfLike(finalUrl, contentType)) {
|
|
16315
|
+
return {
|
|
16316
|
+
url: inputUrl,
|
|
16317
|
+
finalUrl,
|
|
16318
|
+
contentType,
|
|
16319
|
+
content: "",
|
|
16320
|
+
contentLength: 0,
|
|
16321
|
+
truncated: false,
|
|
16322
|
+
startIndex: options.startIndex,
|
|
16323
|
+
error: "PDF extraction is not yet supported"
|
|
16324
|
+
};
|
|
16325
|
+
}
|
|
16326
|
+
const rawText = await response.text();
|
|
16327
|
+
const title = contentType.includes("html") ? extractHtmlTitle(rawText) : void 0;
|
|
16328
|
+
const text = contentType.includes("html") ? options.format === "text" ? htmlToText(rawText) : await htmlToMarkdown(rawText) : rawText;
|
|
16329
|
+
return {
|
|
16330
|
+
url: inputUrl,
|
|
16331
|
+
finalUrl,
|
|
16332
|
+
title,
|
|
16333
|
+
contentType,
|
|
16334
|
+
...paginateContent(text, options.startIndex, options.maxLength)
|
|
16335
|
+
};
|
|
16336
|
+
} catch (err) {
|
|
16337
|
+
return {
|
|
16338
|
+
url: inputUrl,
|
|
16339
|
+
finalUrl: inputUrl,
|
|
16340
|
+
contentType: "",
|
|
16341
|
+
content: "",
|
|
16342
|
+
contentLength: 0,
|
|
16343
|
+
truncated: false,
|
|
16344
|
+
startIndex: options.startIndex,
|
|
16345
|
+
error: `Fetch failed: ${err instanceof Error ? err.message : String(err)}`
|
|
16346
|
+
};
|
|
16347
|
+
} finally {
|
|
16348
|
+
clearTimeout(timeout);
|
|
16349
|
+
}
|
|
16350
|
+
}
|
|
15753
16351
|
function createShellTools(_config, hooks) {
|
|
15754
16352
|
return [
|
|
15755
16353
|
{
|
|
@@ -16016,58 +16614,86 @@ function createShellTools(_config, hooks) {
|
|
|
16016
16614
|
// ─── Web Fetch tool (headless URL content extraction) ───
|
|
16017
16615
|
{
|
|
16018
16616
|
name: "web_fetch",
|
|
16019
|
-
description: "Fetch content from a URL
|
|
16617
|
+
description: "Fetch raw content from a URL. Use for APIs, JSON, plain text, and small pages. For articles/docs/pages that need cleanup, prefer web_extract.",
|
|
16020
16618
|
parameters: {
|
|
16021
16619
|
type: "object",
|
|
16022
16620
|
properties: {
|
|
16023
16621
|
url: { type: "string", description: "The URL to fetch" },
|
|
16024
16622
|
max_length: { type: "number", description: "Maximum characters to return (default: 5000)" },
|
|
16025
|
-
start_index: { type: "number", description: "Start index for content pagination (default: 0). Use to continue reading truncated content." }
|
|
16623
|
+
start_index: { type: "number", description: "Start index for content pagination (default: 0). Use to continue reading truncated content." },
|
|
16624
|
+
allow_private: { type: "boolean", description: "Allow private, loopback, or internal network URLs. Default false." }
|
|
16026
16625
|
},
|
|
16027
16626
|
required: ["url"]
|
|
16028
16627
|
},
|
|
16029
16628
|
execute: /* @__PURE__ */ __name(async (input) => {
|
|
16030
16629
|
const url = input.url;
|
|
16031
|
-
const maxLength = input.max_length
|
|
16032
|
-
const startIndex = input.start_index
|
|
16033
|
-
|
|
16034
|
-
|
|
16035
|
-
|
|
16036
|
-
|
|
16037
|
-
|
|
16038
|
-
|
|
16039
|
-
|
|
16040
|
-
|
|
16041
|
-
|
|
16042
|
-
|
|
16043
|
-
|
|
16044
|
-
|
|
16045
|
-
|
|
16046
|
-
|
|
16047
|
-
|
|
16048
|
-
|
|
16049
|
-
|
|
16050
|
-
const turndown = new TurndownService({
|
|
16051
|
-
headingStyle: "atx",
|
|
16052
|
-
codeBlockStyle: "fenced"
|
|
16053
|
-
});
|
|
16054
|
-
const cleaned = text.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "").replace(/<nav[^>]*>[\s\S]*?<\/nav>/gi, "").replace(/<footer[^>]*>[\s\S]*?<\/footer>/gi, "");
|
|
16055
|
-
text = turndown.turndown(cleaned);
|
|
16056
|
-
} catch {
|
|
16057
|
-
text = text.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
16058
|
-
}
|
|
16059
|
-
}
|
|
16060
|
-
if (startIndex > 0) {
|
|
16061
|
-
text = text.slice(startIndex);
|
|
16062
|
-
}
|
|
16063
|
-
if (text.length > maxLength) {
|
|
16064
|
-
text = text.slice(0, maxLength);
|
|
16065
|
-
return { url, contentType, length: text.length, content: text, truncated: true, nextIndex: startIndex + maxLength, hint: `Content truncated. Use start_index: ${startIndex + maxLength} to continue reading.` };
|
|
16630
|
+
const maxLength = clampNumber(input.max_length, 5e3, 1, 1e5);
|
|
16631
|
+
const startIndex = clampNumber(input.start_index, 0, 0, Number.MAX_SAFE_INTEGER);
|
|
16632
|
+
const allowPrivate = input.allow_private === true;
|
|
16633
|
+
const extracted = await extractUrlContent(url, { maxLength, startIndex, allowPrivate, timeoutMs: 15e3, format: "markdown" });
|
|
16634
|
+
if (extracted.error)
|
|
16635
|
+
return { error: extracted.error, url: extracted.url, finalUrl: extracted.finalUrl };
|
|
16636
|
+
return {
|
|
16637
|
+
url: extracted.url,
|
|
16638
|
+
finalUrl: extracted.finalUrl,
|
|
16639
|
+
contentType: extracted.contentType,
|
|
16640
|
+
title: extracted.title,
|
|
16641
|
+
length: extracted.content.length,
|
|
16642
|
+
contentLength: extracted.contentLength,
|
|
16643
|
+
content: extracted.content,
|
|
16644
|
+
truncated: extracted.truncated,
|
|
16645
|
+
...extracted.nextStartIndex !== void 0 && {
|
|
16646
|
+
nextIndex: extracted.nextStartIndex,
|
|
16647
|
+
nextStartIndex: extracted.nextStartIndex,
|
|
16648
|
+
hint: `Content truncated. Use start_index: ${extracted.nextStartIndex} to continue reading.`
|
|
16066
16649
|
}
|
|
16067
|
-
|
|
16068
|
-
|
|
16069
|
-
|
|
16070
|
-
|
|
16650
|
+
};
|
|
16651
|
+
}, "execute")
|
|
16652
|
+
},
|
|
16653
|
+
// ─── Web Extract tool (selected-source reading) ───
|
|
16654
|
+
{
|
|
16655
|
+
name: "web_extract",
|
|
16656
|
+
description: "Extract readable content from selected web page URLs. Use after web_search when you need to read source pages, articles, docs, or papers. Returns markdown/text with metadata and pagination. For raw APIs or plain JSON, web_fetch is lighter.",
|
|
16657
|
+
parameters: {
|
|
16658
|
+
type: "object",
|
|
16659
|
+
properties: {
|
|
16660
|
+
urls: { type: "array", items: { type: "string" }, description: "List of URLs to extract content from (max 5)" },
|
|
16661
|
+
format: { type: "string", enum: ["markdown", "text"], description: "Output format. HTML is converted to markdown by default.", default: "markdown" },
|
|
16662
|
+
max_length: { type: "number", description: "Maximum characters per URL to return (default: 8000, max: 100000)" },
|
|
16663
|
+
start_index: { type: "number", description: "Start index for content pagination (default: 0). Applies to each URL." },
|
|
16664
|
+
allow_private: { type: "boolean", description: "Allow private, loopback, or internal network URLs. Default false." },
|
|
16665
|
+
use_llm_processing: { type: "boolean", description: "Reserved for future LLM summarization. Currently deterministic extraction only." }
|
|
16666
|
+
},
|
|
16667
|
+
required: ["urls"]
|
|
16668
|
+
},
|
|
16669
|
+
execute: /* @__PURE__ */ __name(async (input) => {
|
|
16670
|
+
const rawUrls = Array.isArray(input.urls) ? input.urls.filter((url) => typeof url === "string") : [];
|
|
16671
|
+
const urls = rawUrls.slice(0, 5);
|
|
16672
|
+
if (urls.length === 0)
|
|
16673
|
+
return { error: "web_extract requires at least one URL", results: [] };
|
|
16674
|
+
const maxLength = clampNumber(input.max_length, 8e3, 1, 1e5);
|
|
16675
|
+
const startIndex = clampNumber(input.start_index, 0, 0, Number.MAX_SAFE_INTEGER);
|
|
16676
|
+
const allowPrivate = input.allow_private === true;
|
|
16677
|
+
const format2 = input.format === "text" ? "text" : "markdown";
|
|
16678
|
+
const results = await Promise.all(urls.map((url) => extractUrlContent(url, { maxLength, startIndex, allowPrivate, format: format2 })));
|
|
16679
|
+
return {
|
|
16680
|
+
results: results.map((result) => ({
|
|
16681
|
+
url: result.url,
|
|
16682
|
+
finalUrl: result.finalUrl,
|
|
16683
|
+
title: result.title,
|
|
16684
|
+
contentType: result.contentType,
|
|
16685
|
+
format: format2,
|
|
16686
|
+
content: result.content,
|
|
16687
|
+
contentLength: result.contentLength,
|
|
16688
|
+
truncated: result.truncated,
|
|
16689
|
+
startIndex: result.startIndex,
|
|
16690
|
+
...result.nextStartIndex !== void 0 && { nextStartIndex: result.nextStartIndex },
|
|
16691
|
+
...result.error && { error: result.error }
|
|
16692
|
+
})),
|
|
16693
|
+
count: results.length,
|
|
16694
|
+
...rawUrls.length > urls.length && { warning: `Only the first ${urls.length} URLs were extracted (max 5 per call).` },
|
|
16695
|
+
...input.use_llm_processing === true && { llmProcessing: "unavailable: deterministic extraction only in this build" }
|
|
16696
|
+
};
|
|
16071
16697
|
}, "execute")
|
|
16072
16698
|
},
|
|
16073
16699
|
// ─── Sleep tool ───
|
|
@@ -16162,12 +16788,14 @@ function createShellTools(_config, hooks) {
|
|
|
16162
16788
|
signal: controller.signal
|
|
16163
16789
|
});
|
|
16164
16790
|
if (native && Array.isArray(native.results) && native.results.length > 0) {
|
|
16791
|
+
const results = normalizeSearchResults(native.results, maxResults);
|
|
16165
16792
|
return {
|
|
16166
16793
|
query,
|
|
16167
|
-
resultCount:
|
|
16168
|
-
results
|
|
16794
|
+
resultCount: results.length,
|
|
16795
|
+
results,
|
|
16169
16796
|
summary: native.summary,
|
|
16170
16797
|
provider: native.provider,
|
|
16798
|
+
backend: native.provider,
|
|
16171
16799
|
durationSeconds: native.durationSeconds
|
|
16172
16800
|
};
|
|
16173
16801
|
}
|
|
@@ -16218,11 +16846,13 @@ function createShellTools(_config, hooks) {
|
|
|
16218
16846
|
results = results.filter((r) => !blockedDomains.some((d) => urlMatchesDomain(r.url, d)));
|
|
16219
16847
|
}
|
|
16220
16848
|
if (results.length > 0) {
|
|
16849
|
+
const normalizedResults = normalizeSearchResults(results, maxResults);
|
|
16221
16850
|
return {
|
|
16222
16851
|
query,
|
|
16223
|
-
resultCount:
|
|
16224
|
-
results,
|
|
16852
|
+
resultCount: normalizedResults.length,
|
|
16853
|
+
results: normalizedResults,
|
|
16225
16854
|
provider: `duckduckgo-${ep.kind}`,
|
|
16855
|
+
backend: `duckduckgo-${ep.kind}`,
|
|
16226
16856
|
...nativeError ? { nativeSearchError: nativeError } : {}
|
|
16227
16857
|
};
|
|
16228
16858
|
}
|
|
@@ -16287,12 +16917,23 @@ var init_shell = __esm({
|
|
|
16287
16917
|
"../openjaw-mcp/dist/tools/shell.js"() {
|
|
16288
16918
|
"use strict";
|
|
16289
16919
|
init_web_search_types();
|
|
16920
|
+
init_url_safety();
|
|
16290
16921
|
__name(truncateOutput, "truncateOutput");
|
|
16291
16922
|
DECODE_ENTITIES = /* @__PURE__ */ __name((s) => s.replace(/&/g, "&").replace(/ /g, " ").replace(/'/g, "'").replace(/"/g, '"').replace(/</g, "<").replace(/>/g, ">"), "DECODE_ENTITIES");
|
|
16292
16923
|
__name(parseLiteResults, "parseLiteResults");
|
|
16293
16924
|
__name(parseHtmlResults, "parseHtmlResults");
|
|
16294
16925
|
__name(isAnomalyPage, "isAnomalyPage");
|
|
16295
16926
|
__name(urlMatchesDomain, "urlMatchesDomain");
|
|
16927
|
+
__name(normalizeSearchResults, "normalizeSearchResults");
|
|
16928
|
+
__name(clampNumber, "clampNumber");
|
|
16929
|
+
__name(extractHtmlTitle, "extractHtmlTitle");
|
|
16930
|
+
__name(cleanHtmlForMarkdown, "cleanHtmlForMarkdown");
|
|
16931
|
+
__name(htmlToText, "htmlToText");
|
|
16932
|
+
__name(isPdfLike, "isPdfLike");
|
|
16933
|
+
__name(htmlToMarkdown, "htmlToMarkdown");
|
|
16934
|
+
__name(paginateContent, "paginateContent");
|
|
16935
|
+
__name(fetchWithValidatedRedirects, "fetchWithValidatedRedirects");
|
|
16936
|
+
__name(extractUrlContent, "extractUrlContent");
|
|
16296
16937
|
__name(createShellTools, "createShellTools");
|
|
16297
16938
|
}
|
|
16298
16939
|
});
|
|
@@ -19667,6 +20308,7 @@ var init_categories = __esm({
|
|
|
19667
20308
|
["clipboard_", "system"],
|
|
19668
20309
|
["web_fetch", "system"],
|
|
19669
20310
|
["web_search", "system"],
|
|
20311
|
+
["web_extract", "system"],
|
|
19670
20312
|
["notify", "system"],
|
|
19671
20313
|
["sleep", "system"],
|
|
19672
20314
|
["ask_user", "system"],
|
|
@@ -20248,6 +20890,8 @@ function parseSkillFile(content, filename) {
|
|
|
20248
20890
|
whenToUse: meta.whenToUse || meta.when_to_use,
|
|
20249
20891
|
tools: Array.isArray(meta.tools) ? meta.tools : meta.tools ? parseYamlArray(meta.tools) : void 0,
|
|
20250
20892
|
model: meta.model,
|
|
20893
|
+
execution: meta.execution === "fork" ? "fork" : meta.execution === "inline" ? "inline" : void 0,
|
|
20894
|
+
timeoutMs: typeof meta.timeoutMs === "number" ? meta.timeoutMs : typeof meta.timeout_ms === "number" ? meta.timeout_ms : void 0,
|
|
20251
20895
|
version: meta.version,
|
|
20252
20896
|
author: meta.author,
|
|
20253
20897
|
platforms: Array.isArray(meta.platforms) ? meta.platforms : void 0,
|
|
@@ -20527,8 +21171,10 @@ function getSkillsSection() {
|
|
|
20527
21171
|
const content = `# Available Skills
|
|
20528
21172
|
|
|
20529
21173
|
Use the \`invoke_skill\` tool to execute any skill listed below.
|
|
20530
|
-
|
|
20531
|
-
|
|
21174
|
+
Skills are for specialized, multi-step workflows that materially benefit from the skill's process.
|
|
21175
|
+
Do NOT invoke a skill for simple one-off questions, quick factual/current-info lookups, basic summaries, or tasks a visible built-in/MCP tool can answer in 1-2 calls.
|
|
21176
|
+
For news/latest/current events, use \`web_search\` directly. For selected URLs, use \`web_extract\` directly.
|
|
21177
|
+
When a skill truly matches a complex workflow, invoke it. Most skills load inline into the current turn so you can continue with visible tools. Only skills marked for fork execution run as isolated sub-agents and may take longer than ordinary tools.
|
|
20532
21178
|
|
|
20533
21179
|
To create new skills or improve existing ones, use \`invoke_skill("skill-creator")\`.
|
|
20534
21180
|
New skills must be saved to \`~/.openjaw-agent/skills/\` (user skills directory).
|
|
@@ -26869,10 +27515,38 @@ __export(skill_tool_exports, {
|
|
|
26869
27515
|
createSkillTool: () => createSkillTool
|
|
26870
27516
|
});
|
|
26871
27517
|
import { readFileSync as readFileSync23, writeFileSync as writeFileSync15 } from "node:fs";
|
|
27518
|
+
function resolveSkillTimeoutMs() {
|
|
27519
|
+
const raw = process.env["OPENJAW_SKILL_TIMEOUT_MS"];
|
|
27520
|
+
const parsed = raw ? Number(raw) : NaN;
|
|
27521
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
27522
|
+
return Math.min(Math.max(Math.floor(parsed), 1e4), 6e5);
|
|
27523
|
+
}
|
|
27524
|
+
return DEFAULT_SKILL_TIMEOUT_MS;
|
|
27525
|
+
}
|
|
27526
|
+
function resolveForkTimeoutMs(skill) {
|
|
27527
|
+
const configured = skill.meta.timeoutMs;
|
|
27528
|
+
if (typeof configured === "number" && Number.isFinite(configured) && configured > 0) {
|
|
27529
|
+
return Math.min(Math.max(Math.floor(configured), 1e4), 6e5);
|
|
27530
|
+
}
|
|
27531
|
+
return resolveSkillTimeoutMs();
|
|
27532
|
+
}
|
|
27533
|
+
async function withTimeout(promise, ms) {
|
|
27534
|
+
let timeoutId;
|
|
27535
|
+
try {
|
|
27536
|
+
return await Promise.race([
|
|
27537
|
+
promise,
|
|
27538
|
+
new Promise((resolve5) => {
|
|
27539
|
+
timeoutId = setTimeout(() => resolve5(SKILL_TIMEOUT), ms);
|
|
27540
|
+
})
|
|
27541
|
+
]);
|
|
27542
|
+
} finally {
|
|
27543
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
27544
|
+
}
|
|
27545
|
+
}
|
|
26872
27546
|
function createSkillTool(config, toolRegistry, systemPromptFn) {
|
|
26873
27547
|
return {
|
|
26874
27548
|
name: "invoke_skill",
|
|
26875
|
-
description: "Execute a skill from the available skills list. Skills
|
|
27549
|
+
description: "Execute a skill from the available skills list. Skills are for specialized multi-step workflows. Do not use for simple one-off lookups, quick current-news/factual questions, or tasks a direct built-in tool can answer in 1-2 calls.",
|
|
26876
27550
|
parameters: {
|
|
26877
27551
|
type: "object",
|
|
26878
27552
|
properties: {
|
|
@@ -26883,6 +27557,11 @@ function createSkillTool(config, toolRegistry, systemPromptFn) {
|
|
|
26883
27557
|
args: {
|
|
26884
27558
|
type: "string",
|
|
26885
27559
|
description: "Optional arguments or context for the skill"
|
|
27560
|
+
},
|
|
27561
|
+
mode: {
|
|
27562
|
+
type: "string",
|
|
27563
|
+
enum: ["inline", "fork"],
|
|
27564
|
+
description: "Execution mode. Default inline loads instructions into this turn; fork runs an isolated sub-agent for skills that explicitly support long-running execution."
|
|
26886
27565
|
}
|
|
26887
27566
|
},
|
|
26888
27567
|
required: ["skill"]
|
|
@@ -26890,6 +27569,7 @@ function createSkillTool(config, toolRegistry, systemPromptFn) {
|
|
|
26890
27569
|
execute: /* @__PURE__ */ __name(async (input) => {
|
|
26891
27570
|
const skillName = input.skill.trim().replace(/^\//, "");
|
|
26892
27571
|
const args = input.args || "";
|
|
27572
|
+
const requestedMode = input.mode === "fork" ? "fork" : input.mode === "inline" ? "inline" : void 0;
|
|
26893
27573
|
const skill = findSkill(skillName);
|
|
26894
27574
|
if (!skill) {
|
|
26895
27575
|
clearSkillRegistry();
|
|
@@ -26897,17 +27577,39 @@ function createSkillTool(config, toolRegistry, systemPromptFn) {
|
|
|
26897
27577
|
if (!retrySkill) {
|
|
26898
27578
|
return { success: false, error: `Skill "${skillName}" not found. Check /skills for available skills.` };
|
|
26899
27579
|
}
|
|
26900
|
-
return await executeSkill(retrySkill
|
|
27580
|
+
return await executeSkill(retrySkill, args, requestedMode, config, toolRegistry, systemPromptFn);
|
|
26901
27581
|
}
|
|
26902
|
-
return await executeSkill(skill
|
|
27582
|
+
return await executeSkill(skill, args, requestedMode, config, toolRegistry, systemPromptFn);
|
|
26903
27583
|
}, "execute")
|
|
26904
27584
|
};
|
|
26905
27585
|
}
|
|
26906
|
-
async function executeSkill(
|
|
27586
|
+
async function executeSkill(skill, args, requestedMode, config, toolRegistry, systemPromptFn) {
|
|
27587
|
+
const skillName = skill.name;
|
|
26907
27588
|
const body = loadSkillPrompt(skillName);
|
|
26908
27589
|
if (!body) {
|
|
26909
27590
|
return { success: false, error: `Could not load skill content for "${skillName}"` };
|
|
26910
27591
|
}
|
|
27592
|
+
const mode = requestedMode ?? skill.meta.execution ?? "inline";
|
|
27593
|
+
if (mode === "fork") {
|
|
27594
|
+
return await executeForkedSkill(skill, body, args, config, toolRegistry, systemPromptFn);
|
|
27595
|
+
}
|
|
27596
|
+
return loadInlineSkill(skill, body, args);
|
|
27597
|
+
}
|
|
27598
|
+
function loadInlineSkill(skill, body, args) {
|
|
27599
|
+
const skillBlock = args ? `${body}
|
|
27600
|
+
|
|
27601
|
+
# User instructions for this skill
|
|
27602
|
+
${args}` : body;
|
|
27603
|
+
return {
|
|
27604
|
+
success: true,
|
|
27605
|
+
skill: skill.name,
|
|
27606
|
+
mode: "inline",
|
|
27607
|
+
message: "Skill instructions loaded inline. Continue the task in the current conversation using these instructions and visible tools.",
|
|
27608
|
+
instructions: skillBlock
|
|
27609
|
+
};
|
|
27610
|
+
}
|
|
27611
|
+
async function executeForkedSkill(skill, body, args, config, toolRegistry, systemPromptFn) {
|
|
27612
|
+
const skillName = skill.name;
|
|
26911
27613
|
const baseSections = await systemPromptFn();
|
|
26912
27614
|
const staticSections = baseSections.slice(0, 4).filter(Boolean);
|
|
26913
27615
|
const skillPrompt = [...staticSections, body].join("\n\n");
|
|
@@ -26917,7 +27619,34 @@ async function executeSkill(skillName, args, config, toolRegistry, systemPromptF
|
|
|
26917
27619
|
let lastAnswer = "";
|
|
26918
27620
|
let thinking = "";
|
|
26919
27621
|
const allChunks = [];
|
|
26920
|
-
|
|
27622
|
+
const timeoutMs = resolveForkTimeoutMs(skill);
|
|
27623
|
+
const deadline = Date.now() + timeoutMs;
|
|
27624
|
+
const iterator = forkLoop.run(userMessage, skillPrompt)[Symbol.asyncIterator]();
|
|
27625
|
+
while (true) {
|
|
27626
|
+
const remaining = deadline - Date.now();
|
|
27627
|
+
if (remaining <= 0) {
|
|
27628
|
+
forkLoop.abort();
|
|
27629
|
+
return {
|
|
27630
|
+
success: false,
|
|
27631
|
+
skill: skillName,
|
|
27632
|
+
timeoutMs,
|
|
27633
|
+
error: `Skill "${skillName}" exceeded ${Math.round(timeoutMs / 1e3)}s. Use direct tools for simple requests, or rerun with narrower instructions.`,
|
|
27634
|
+
partial: lastAnswer || thinking || void 0
|
|
27635
|
+
};
|
|
27636
|
+
}
|
|
27637
|
+
const next = await withTimeout(iterator.next(), remaining);
|
|
27638
|
+
if (next === SKILL_TIMEOUT) {
|
|
27639
|
+
forkLoop.abort();
|
|
27640
|
+
return {
|
|
27641
|
+
success: false,
|
|
27642
|
+
skill: skillName,
|
|
27643
|
+
timeoutMs,
|
|
27644
|
+
error: `Skill "${skillName}" exceeded ${Math.round(timeoutMs / 1e3)}s. Use direct tools for simple requests, or rerun with narrower instructions.`,
|
|
27645
|
+
partial: lastAnswer || thinking || void 0
|
|
27646
|
+
};
|
|
27647
|
+
}
|
|
27648
|
+
if (next.done) break;
|
|
27649
|
+
const chunk = next.value;
|
|
26921
27650
|
if (chunk.type === "answer" && chunk.content) lastAnswer = chunk.content;
|
|
26922
27651
|
if (chunk.type === "thinking" && chunk.content) thinking += chunk.content;
|
|
26923
27652
|
allChunks.push({ type: chunk.type, content: chunk.content });
|
|
@@ -26925,14 +27654,14 @@ async function executeSkill(skillName, args, config, toolRegistry, systemPromptF
|
|
|
26925
27654
|
const result = lastAnswer || thinking || "Skill completed (no output)";
|
|
26926
27655
|
const compressed = result.length > 2e3 ? result.slice(0, 2e3) + `
|
|
26927
27656
|
...(${result.length} chars total, truncated)` : result;
|
|
26928
|
-
const
|
|
26929
|
-
if (
|
|
27657
|
+
const currentSkill = findSkill(skillName);
|
|
27658
|
+
if (currentSkill?.filePath && currentSkill.source === "user") {
|
|
26930
27659
|
const lessons = extractLessons(allChunks);
|
|
26931
27660
|
if (lessons) {
|
|
26932
|
-
appendLessonsLearned(
|
|
27661
|
+
appendLessonsLearned(currentSkill.filePath, lessons);
|
|
26933
27662
|
}
|
|
26934
27663
|
}
|
|
26935
|
-
return { success: true, skill: skillName, result: compressed };
|
|
27664
|
+
return { success: true, skill: skillName, mode: "fork", result: compressed };
|
|
26936
27665
|
} catch (err) {
|
|
26937
27666
|
return { success: false, skill: skillName, error: err instanceof Error ? err.message : String(err) };
|
|
26938
27667
|
}
|
|
@@ -26973,13 +27702,21 @@ function appendLessonsLearned(skillPath, lessons) {
|
|
|
26973
27702
|
} catch {
|
|
26974
27703
|
}
|
|
26975
27704
|
}
|
|
27705
|
+
var DEFAULT_SKILL_TIMEOUT_MS, SKILL_TIMEOUT;
|
|
26976
27706
|
var init_skill_tool = __esm({
|
|
26977
27707
|
"src/tools/skill-tool.ts"() {
|
|
26978
27708
|
"use strict";
|
|
26979
27709
|
init_agent_loop();
|
|
26980
27710
|
init_registry2();
|
|
27711
|
+
DEFAULT_SKILL_TIMEOUT_MS = 12e4;
|
|
27712
|
+
SKILL_TIMEOUT = /* @__PURE__ */ Symbol("skill-timeout");
|
|
27713
|
+
__name(resolveSkillTimeoutMs, "resolveSkillTimeoutMs");
|
|
27714
|
+
__name(resolveForkTimeoutMs, "resolveForkTimeoutMs");
|
|
27715
|
+
__name(withTimeout, "withTimeout");
|
|
26981
27716
|
__name(createSkillTool, "createSkillTool");
|
|
26982
27717
|
__name(executeSkill, "executeSkill");
|
|
27718
|
+
__name(loadInlineSkill, "loadInlineSkill");
|
|
27719
|
+
__name(executeForkedSkill, "executeForkedSkill");
|
|
26983
27720
|
__name(extractLessons, "extractLessons");
|
|
26984
27721
|
__name(appendLessonsLearned, "appendLessonsLearned");
|
|
26985
27722
|
}
|
|
@@ -27832,13 +28569,13 @@ var init_feishu = __esm({
|
|
|
27832
28569
|
if (stat2.isFile() && stat2.size < 30 * 1024 * 1024) {
|
|
27833
28570
|
const fileType = this.getFeishuFileType(fileName);
|
|
27834
28571
|
this.emit({ type: "system", content: `\u{1F4E4} Uploading to Feishu: ${fileName} (${(stat2.size / 1024).toFixed(0)}KB, type=${fileType})` });
|
|
27835
|
-
const
|
|
28572
|
+
const withTimeout2 = /* @__PURE__ */ __name((promise, ms, label) => Promise.race([
|
|
27836
28573
|
promise,
|
|
27837
28574
|
new Promise((_, reject) => setTimeout(() => reject(new Error(`${label} timed out after ${ms / 1e3}s`)), ms))
|
|
27838
28575
|
]), "withTimeout");
|
|
27839
28576
|
const { createReadStream: crs } = await import("node:fs");
|
|
27840
28577
|
const t0 = Date.now();
|
|
27841
|
-
const uploadResp = await
|
|
28578
|
+
const uploadResp = await withTimeout2(
|
|
27842
28579
|
this.client.im.file.create({
|
|
27843
28580
|
data: {
|
|
27844
28581
|
file_type: fileType,
|
|
@@ -27856,7 +28593,7 @@ var init_feishu = __esm({
|
|
|
27856
28593
|
this.emit({ type: "system", content: `\u{1F4E4} Upload done in ${(uploadMs / 1e3).toFixed(1)}s: code=${respCode}, file_key=${fileKey ? "\u2713" : "\u2717"}` });
|
|
27857
28594
|
if (fileKey) {
|
|
27858
28595
|
const t1 = Date.now();
|
|
27859
|
-
const sendResp = await
|
|
28596
|
+
const sendResp = await withTimeout2(
|
|
27860
28597
|
this.client.im.message.create({
|
|
27861
28598
|
params: { receive_id_type: "chat_id" },
|
|
27862
28599
|
data: {
|
|
@@ -48666,26 +49403,14 @@ ${helpMessage}` : field.label;
|
|
|
48666
49403
|
bus.log("info", `session.steer ${String(params.text ?? "").slice(0, 200)}`);
|
|
48667
49404
|
return { status: "queued", text: String(params.text ?? "") };
|
|
48668
49405
|
});
|
|
48669
|
-
let pendingImageSeq = 0;
|
|
48670
49406
|
let pendingImage = null;
|
|
48671
|
-
const nextImageAttachmentId = /* @__PURE__ */ __name(() => `img-${Date.now().toString(36)}-${++pendingImageSeq}`, "nextImageAttachmentId");
|
|
48672
49407
|
bus.registerRpc("prompt.submit", async (params) => {
|
|
48673
49408
|
const text = String(params.text ?? "");
|
|
48674
49409
|
if (!text) return { ok: false };
|
|
48675
49410
|
const systemPromptArr = await systemPromptFn();
|
|
48676
49411
|
const systemPrompt = systemPromptArr.join("\n\n");
|
|
48677
|
-
const
|
|
48678
|
-
|
|
48679
|
-
if (imageForSubmit) {
|
|
48680
|
-
const modelInfo = agentLoop.getActiveModelMetadata() ?? await agentLoop.listModels().then(() => agentLoop.getActiveModelMetadata()).catch(() => void 0);
|
|
48681
|
-
if (modelInfo?.supportsVision === false) {
|
|
48682
|
-
throw new Error(`model ${agentLoop.model} does not support image input; switch to a vision-capable model before submitting an image`);
|
|
48683
|
-
}
|
|
48684
|
-
}
|
|
48685
|
-
const imageData = imageForSubmit ? { base64: imageForSubmit.base64, mimeType: imageForSubmit.mimeType } : void 0;
|
|
48686
|
-
if (imageForSubmit) {
|
|
48687
|
-
pendingImage = null;
|
|
48688
|
-
}
|
|
49412
|
+
const imageData = pendingImage ? { base64: pendingImage.base64, mimeType: pendingImage.mimeType } : void 0;
|
|
49413
|
+
pendingImage = null;
|
|
48689
49414
|
currentRun = { abort: /* @__PURE__ */ __name(() => agentLoop.abort(), "abort") };
|
|
48690
49415
|
void streamAgentRun({ agentLoop, bus, systemPrompt, text, imageData }).finally(() => {
|
|
48691
49416
|
currentRun = null;
|
|
@@ -48761,32 +49486,6 @@ ${helpMessage}` : field.label;
|
|
|
48761
49486
|
});
|
|
48762
49487
|
});
|
|
48763
49488
|
bus.registerRpc("clipboard.paste", async () => {
|
|
48764
|
-
try {
|
|
48765
|
-
if (clipboardHasImage()) {
|
|
48766
|
-
const img = readClipboardImage();
|
|
48767
|
-
if (img) {
|
|
48768
|
-
const attachmentId = nextImageAttachmentId();
|
|
48769
|
-
pendingImage = {
|
|
48770
|
-
attachmentId,
|
|
48771
|
-
base64: img.base64,
|
|
48772
|
-
mimeType: img.mimeType,
|
|
48773
|
-
name: "clipboard.png"
|
|
48774
|
-
};
|
|
48775
|
-
const byteLength = Math.ceil(img.base64.length * 3 / 4);
|
|
48776
|
-
const tokenEstimate = Math.max(1, Math.ceil(byteLength / 750));
|
|
48777
|
-
return {
|
|
48778
|
-
attached: true,
|
|
48779
|
-
attachment_id: attachmentId,
|
|
48780
|
-
count: 1,
|
|
48781
|
-
height: img.height,
|
|
48782
|
-
name: "clipboard.png",
|
|
48783
|
-
token_estimate: tokenEstimate,
|
|
48784
|
-
width: img.width
|
|
48785
|
-
};
|
|
48786
|
-
}
|
|
48787
|
-
}
|
|
48788
|
-
} catch {
|
|
48789
|
-
}
|
|
48790
49489
|
try {
|
|
48791
49490
|
const text = await readClipboardText();
|
|
48792
49491
|
return { attached: false, message: text ?? "" };
|
|
@@ -49411,16 +50110,13 @@ ${helpMessage}` : field.label;
|
|
|
49411
50110
|
}
|
|
49412
50111
|
const ext = extname4(path3).toLowerCase().replace(/^\./, "");
|
|
49413
50112
|
const mimeType = ext === "png" ? "image/png" : ext === "jpg" || ext === "jpeg" ? "image/jpeg" : ext === "gif" ? "image/gif" : ext === "webp" ? "image/webp" : "image/png";
|
|
49414
|
-
const attachmentId = nextImageAttachmentId();
|
|
49415
50113
|
pendingImage = {
|
|
49416
|
-
attachmentId,
|
|
49417
50114
|
base64: buffer.toString("base64"),
|
|
49418
50115
|
mimeType,
|
|
49419
50116
|
name: basename3(path3)
|
|
49420
50117
|
};
|
|
49421
50118
|
const tokenEstimate = Math.max(1, Math.ceil(buffer.byteLength / 750));
|
|
49422
50119
|
return {
|
|
49423
|
-
attachment_id: attachmentId,
|
|
49424
50120
|
height: 0,
|
|
49425
50121
|
name: basename3(path3),
|
|
49426
50122
|
remainder,
|
|
@@ -49428,13 +50124,6 @@ ${helpMessage}` : field.label;
|
|
|
49428
50124
|
width: 0
|
|
49429
50125
|
};
|
|
49430
50126
|
});
|
|
49431
|
-
bus.registerRpc("image.clear", (params) => {
|
|
49432
|
-
const attachmentId = String(params.attachment_id ?? "");
|
|
49433
|
-
if (!attachmentId || pendingImage?.attachmentId === attachmentId) {
|
|
49434
|
-
pendingImage = null;
|
|
49435
|
-
}
|
|
49436
|
-
return { ok: true };
|
|
49437
|
-
});
|
|
49438
50127
|
bus.registerRpc("process.stop", () => {
|
|
49439
50128
|
const wasRunning = agentLoop.isRunning;
|
|
49440
50129
|
agentLoop.abort();
|
|
@@ -49634,7 +50323,6 @@ var init_rpcHandlers = __esm({
|
|
|
49634
50323
|
init_uiStore();
|
|
49635
50324
|
init_catalog();
|
|
49636
50325
|
init_clipboard();
|
|
49637
|
-
init_clipboard_image();
|
|
49638
50326
|
init_models_static();
|
|
49639
50327
|
init_providers();
|
|
49640
50328
|
init_types();
|
|
@@ -52896,7 +53584,6 @@ function useComposerState({
|
|
|
52896
53584
|
}) {
|
|
52897
53585
|
const [input, setInput] = useState12("");
|
|
52898
53586
|
const [inputBuf, setInputBuf] = useState12([]);
|
|
52899
|
-
const [attachedImage, setAttachedImage] = useState12(null);
|
|
52900
53587
|
const [pasteSnips, setPasteSnips] = useState12([]);
|
|
52901
53588
|
const isBlocked = useStore($isBlocked);
|
|
52902
53589
|
const { querier } = use_stdin_default();
|
|
@@ -52914,25 +53601,14 @@ function useComposerState({
|
|
|
52914
53601
|
} = useQueue();
|
|
52915
53602
|
const { historyRef, historyIdx, setHistoryIdx, historyDraftRef, pushHistory } = useInputHistory();
|
|
52916
53603
|
const { completions, compIdx, setCompIdx, compReplace } = useCompletion(input, isBlocked, gw);
|
|
52917
|
-
const
|
|
52918
|
-
const attachmentId = attachedImage?.attachment_id;
|
|
52919
|
-
setAttachedImage(null);
|
|
52920
|
-
if (attachmentId) {
|
|
52921
|
-
void gw.request("image.clear", { attachment_id: attachmentId }).catch(() => {
|
|
52922
|
-
});
|
|
52923
|
-
}
|
|
52924
|
-
}, [attachedImage?.attachment_id, gw]);
|
|
52925
|
-
const clearIn = useCallback7((options = {}) => {
|
|
53604
|
+
const clearIn = useCallback7(() => {
|
|
52926
53605
|
setInput("");
|
|
52927
53606
|
setInputBuf([]);
|
|
52928
53607
|
setPasteSnips([]);
|
|
52929
|
-
if (!options.keepAttachedImage) {
|
|
52930
|
-
clearAttachedImage();
|
|
52931
|
-
}
|
|
52932
53608
|
setQueueEdit(null);
|
|
52933
53609
|
setHistoryIdx(null);
|
|
52934
53610
|
historyDraftRef.current = "";
|
|
52935
|
-
}, [
|
|
53611
|
+
}, [historyDraftRef, setQueueEdit, setHistoryIdx]);
|
|
52936
53612
|
const handleResolvedPaste = useCallback7(
|
|
52937
53613
|
async ({
|
|
52938
53614
|
bracketed,
|
|
@@ -52955,13 +53631,6 @@ function useComposerState({
|
|
|
52955
53631
|
session_id: sid
|
|
52956
53632
|
});
|
|
52957
53633
|
if (attached?.name) {
|
|
52958
|
-
setAttachedImage({
|
|
52959
|
-
attachment_id: attached.attachment_id,
|
|
52960
|
-
height: attached.height,
|
|
52961
|
-
name: attached.name,
|
|
52962
|
-
token_estimate: attached.token_estimate,
|
|
52963
|
-
width: attached.width
|
|
52964
|
-
});
|
|
52965
53634
|
onImageAttached?.(attached);
|
|
52966
53635
|
const remainder = attached.remainder?.trim() ?? "";
|
|
52967
53636
|
if (!remainder) {
|
|
@@ -53014,7 +53683,7 @@ function useComposerState({
|
|
|
53014
53683
|
}) => {
|
|
53015
53684
|
if (hotkey) {
|
|
53016
53685
|
const preferOsc52 = isRemoteShellSession(process.env);
|
|
53017
|
-
const readPreferredText =
|
|
53686
|
+
const readPreferredText = preferOsc52 ? readOsc52Clipboard(querier).then(async (osc52Text) => {
|
|
53018
53687
|
if (isUsableClipboardText(osc52Text)) {
|
|
53019
53688
|
return osc52Text;
|
|
53020
53689
|
}
|
|
@@ -53024,12 +53693,8 @@ function useComposerState({
|
|
|
53024
53693
|
return clipText;
|
|
53025
53694
|
}
|
|
53026
53695
|
return readOsc52Clipboard(querier);
|
|
53027
|
-
})
|
|
53028
|
-
return
|
|
53029
|
-
if (imageAttached) {
|
|
53030
|
-
return null;
|
|
53031
|
-
}
|
|
53032
|
-
const preferredText = await readPreferredText();
|
|
53696
|
+
});
|
|
53697
|
+
return readPreferredText.then(async (preferredText) => {
|
|
53033
53698
|
if (isUsableClipboardText(preferredText)) {
|
|
53034
53699
|
return handleResolvedPaste({ bracketed: false, cursor, text: preferredText, value });
|
|
53035
53700
|
}
|
|
@@ -53067,7 +53732,6 @@ function useComposerState({
|
|
|
53067
53732
|
}, [input, inputBuf, submitRef]);
|
|
53068
53733
|
const actions = useMemo6(
|
|
53069
53734
|
() => ({
|
|
53070
|
-
clearAttachedImage,
|
|
53071
53735
|
clearIn,
|
|
53072
53736
|
dequeue,
|
|
53073
53737
|
enqueue,
|
|
@@ -53078,7 +53742,6 @@ function useComposerState({
|
|
|
53078
53742
|
replaceQueue: replaceQ,
|
|
53079
53743
|
setCompIdx,
|
|
53080
53744
|
setHistoryIdx,
|
|
53081
|
-
setAttachedImage,
|
|
53082
53745
|
setInput,
|
|
53083
53746
|
setInputBuf,
|
|
53084
53747
|
setPasteSnips,
|
|
@@ -53086,7 +53749,6 @@ function useComposerState({
|
|
|
53086
53749
|
syncQueue
|
|
53087
53750
|
}),
|
|
53088
53751
|
[
|
|
53089
|
-
clearAttachedImage,
|
|
53090
53752
|
clearIn,
|
|
53091
53753
|
dequeue,
|
|
53092
53754
|
enqueue,
|
|
@@ -53113,7 +53775,6 @@ function useComposerState({
|
|
|
53113
53775
|
);
|
|
53114
53776
|
const state = useMemo6(
|
|
53115
53777
|
() => ({
|
|
53116
|
-
attachedImage,
|
|
53117
53778
|
compIdx,
|
|
53118
53779
|
compReplace,
|
|
53119
53780
|
completions,
|
|
@@ -53124,7 +53785,7 @@ function useComposerState({
|
|
|
53124
53785
|
queueEditIdx,
|
|
53125
53786
|
queuedDisplay
|
|
53126
53787
|
}),
|
|
53127
|
-
[
|
|
53788
|
+
[compIdx, compReplace, completions, historyIdx, input, inputBuf, pasteSnips, queueEditIdx, queuedDisplay]
|
|
53128
53789
|
);
|
|
53129
53790
|
return {
|
|
53130
53791
|
actions,
|
|
@@ -53713,9 +54374,6 @@ function useInputHandlers(ctx) {
|
|
|
53713
54374
|
if (key.escape && terminal.hasSelection) {
|
|
53714
54375
|
return clearSelection2();
|
|
53715
54376
|
}
|
|
53716
|
-
if (key.escape && cState.attachedImage) {
|
|
53717
|
-
return cActions.clearAttachedImage();
|
|
53718
|
-
}
|
|
53719
54377
|
if (key.escape && live.focusedPane === "transcript") {
|
|
53720
54378
|
patchUiState({ focusedPane: "composer" });
|
|
53721
54379
|
return;
|
|
@@ -53764,7 +54422,7 @@ function useInputHandlers(ctx) {
|
|
|
53764
54422
|
sys: actions.sys
|
|
53765
54423
|
});
|
|
53766
54424
|
}
|
|
53767
|
-
if (cState.input || cState.inputBuf.length
|
|
54425
|
+
if (cState.input || cState.inputBuf.length) {
|
|
53768
54426
|
return cActions.clearIn();
|
|
53769
54427
|
}
|
|
53770
54428
|
return actions.die();
|
|
@@ -53954,7 +54612,6 @@ function useSessionLifecycle(opts) {
|
|
|
53954
54612
|
setHistoryItems([]);
|
|
53955
54613
|
setLastUserMsg("");
|
|
53956
54614
|
setStickyPrompt("");
|
|
53957
|
-
composerActions.clearAttachedImage();
|
|
53958
54615
|
composerActions.setPasteSnips([]);
|
|
53959
54616
|
evictInkCaches("half");
|
|
53960
54617
|
}, [composerActions, setHistoryItems, setLastUserMsg, setStickyPrompt, setVoiceProcessing, setVoiceRecording]);
|
|
@@ -53967,7 +54624,6 @@ function useSessionLifecycle(opts) {
|
|
|
53967
54624
|
setHistoryItems(info ? [introMsg(info)] : []);
|
|
53968
54625
|
setStickyPrompt("");
|
|
53969
54626
|
setLastUserMsg("");
|
|
53970
|
-
composerActions.clearAttachedImage();
|
|
53971
54627
|
composerActions.setPasteSnips([]);
|
|
53972
54628
|
patchTurnState({ activity: [] });
|
|
53973
54629
|
patchUiState({ info, usage: usageFrom(info) });
|
|
@@ -54198,7 +54854,6 @@ function useSubmission(opts) {
|
|
|
54198
54854
|
const expand = expandSnips(composerState.pasteSnips);
|
|
54199
54855
|
const startSubmit = /* @__PURE__ */ __name((displayText, submitText, showUserMessage2 = true) => {
|
|
54200
54856
|
const sid2 = getUiState().sid;
|
|
54201
|
-
const imageAttachment = composerState.attachedImage;
|
|
54202
54857
|
if (!sid2) {
|
|
54203
54858
|
return sys("session not ready yet");
|
|
54204
54859
|
}
|
|
@@ -54211,20 +54866,7 @@ function useSubmission(opts) {
|
|
|
54211
54866
|
patchUiState({ busy: true, status: "running\u2026" });
|
|
54212
54867
|
turnController.bufRef = "";
|
|
54213
54868
|
turnController.interrupted = false;
|
|
54214
|
-
|
|
54215
|
-
if (imageAttachment?.attachment_id) {
|
|
54216
|
-
submitParams.image_attachment_id = imageAttachment.attachment_id;
|
|
54217
|
-
}
|
|
54218
|
-
if (imageAttachment) {
|
|
54219
|
-
composerActions.setAttachedImage(null);
|
|
54220
|
-
}
|
|
54221
|
-
gw.request("prompt.submit", submitParams).catch((e) => {
|
|
54222
|
-
if (imageAttachment) {
|
|
54223
|
-
composerActions.setAttachedImage(imageAttachment);
|
|
54224
|
-
sys(`error: ${e.message}`);
|
|
54225
|
-
patchUiState({ busy: false, status: "ready" });
|
|
54226
|
-
return;
|
|
54227
|
-
}
|
|
54869
|
+
gw.request("prompt.submit", { session_id: sid2, text: submitText }).catch((e) => {
|
|
54228
54870
|
if (isSessionBusyError(e)) {
|
|
54229
54871
|
composerActions.enqueue(submitText);
|
|
54230
54872
|
patchUiState({ busy: true, status: "queued for next turn" });
|
|
@@ -54250,7 +54892,7 @@ function useSubmission(opts) {
|
|
|
54250
54892
|
startSubmit(r.text || text, expand(r.text || text), showUserMessage);
|
|
54251
54893
|
}).catch(() => startSubmit(text, expand(text), showUserMessage));
|
|
54252
54894
|
},
|
|
54253
|
-
[appendMessage, composerActions, composerState.
|
|
54895
|
+
[appendMessage, composerActions, composerState.pasteSnips, gw, maybeGoodVibes, setLastUserMsg, sys]
|
|
54254
54896
|
);
|
|
54255
54897
|
const shellExec = useCallback9(
|
|
54256
54898
|
(cmd) => {
|
|
@@ -54313,16 +54955,6 @@ function useSubmission(opts) {
|
|
|
54313
54955
|
}
|
|
54314
54956
|
sys(note);
|
|
54315
54957
|
}, "fallback");
|
|
54316
|
-
if (composerState.attachedImage) {
|
|
54317
|
-
if (live.sid) {
|
|
54318
|
-
turnController.interruptTurn({ appendMessage, gw, sid: live.sid, sys });
|
|
54319
|
-
}
|
|
54320
|
-
if (hasInterpolation(full)) {
|
|
54321
|
-
patchUiState({ busy: true });
|
|
54322
|
-
return interpolate(full, send);
|
|
54323
|
-
}
|
|
54324
|
-
return send(full);
|
|
54325
|
-
}
|
|
54326
54958
|
if (mode === "queue") {
|
|
54327
54959
|
return composerActions.enqueue(full);
|
|
54328
54960
|
}
|
|
@@ -54344,7 +54976,7 @@ function useSubmission(opts) {
|
|
|
54344
54976
|
}
|
|
54345
54977
|
send(full);
|
|
54346
54978
|
},
|
|
54347
|
-
[appendMessage, composerActions, composerRefs,
|
|
54979
|
+
[appendMessage, composerActions, composerRefs, gw, interpolate, send, sys]
|
|
54348
54980
|
);
|
|
54349
54981
|
const dispatchSubmission = useCallback9(
|
|
54350
54982
|
(full) => {
|
|
@@ -54369,8 +55001,8 @@ function useSubmission(opts) {
|
|
|
54369
55001
|
return;
|
|
54370
55002
|
}
|
|
54371
55003
|
const editIdx = composerRefs.queueEditRef.current;
|
|
55004
|
+
composerActions.clearIn();
|
|
54372
55005
|
if (editIdx !== null) {
|
|
54373
|
-
composerActions.clearIn();
|
|
54374
55006
|
composerActions.replaceQueue(editIdx, full);
|
|
54375
55007
|
const picked = composerRefs.queueRef.current.splice(editIdx, 1)[0];
|
|
54376
55008
|
composerActions.syncQueue();
|
|
@@ -54387,7 +55019,6 @@ function useSubmission(opts) {
|
|
|
54387
55019
|
}
|
|
54388
55020
|
return sendQueued(picked);
|
|
54389
55021
|
}
|
|
54390
|
-
composerActions.clearIn({ keepAttachedImage: !!composerState.attachedImage });
|
|
54391
55022
|
composerActions.pushHistory(full);
|
|
54392
55023
|
if (getUiState().busy) {
|
|
54393
55024
|
return handleBusyInput(full);
|
|
@@ -54398,7 +55029,7 @@ function useSubmission(opts) {
|
|
|
54398
55029
|
}
|
|
54399
55030
|
send(full);
|
|
54400
55031
|
},
|
|
54401
|
-
[appendMessage, composerActions, composerRefs,
|
|
55032
|
+
[appendMessage, composerActions, composerRefs, handleBusyInput, interpolate, send, sendQueued, shellExec, slashRef]
|
|
54402
55033
|
);
|
|
54403
55034
|
const submit = useCallback9(
|
|
54404
55035
|
(value) => {
|
|
@@ -54516,7 +55147,8 @@ function useMainApp(gw) {
|
|
|
54516
55147
|
const scrollRef = useRef13(null);
|
|
54517
55148
|
const onEventRef = useRef13(() => {
|
|
54518
55149
|
});
|
|
54519
|
-
const clipboardPasteRef = useRef13(() =>
|
|
55150
|
+
const clipboardPasteRef = useRef13(() => {
|
|
55151
|
+
});
|
|
54520
55152
|
const submitRef = useRef13(() => {
|
|
54521
55153
|
});
|
|
54522
55154
|
const terminalHintsShownRef = useRef13(/* @__PURE__ */ new Set());
|
|
@@ -54569,7 +55201,9 @@ function useMainApp(gw) {
|
|
|
54569
55201
|
const composer = useComposerState({
|
|
54570
55202
|
gw,
|
|
54571
55203
|
onClipboardPaste: /* @__PURE__ */ __name((quiet) => clipboardPasteRef.current(quiet), "onClipboardPaste"),
|
|
54572
|
-
onImageAttached: /* @__PURE__ */ __name(() =>
|
|
55204
|
+
onImageAttached: /* @__PURE__ */ __name((info) => {
|
|
55205
|
+
sys(attachedImageNotice(info));
|
|
55206
|
+
}, "onImageAttached"),
|
|
54573
55207
|
submitRef
|
|
54574
55208
|
});
|
|
54575
55209
|
const { actions: composerActions, refs: composerRefs, state: composerState } = composer;
|
|
@@ -54777,25 +55411,17 @@ function useMainApp(gw) {
|
|
|
54777
55411
|
const paste2 = useCallback10(
|
|
54778
55412
|
(quiet = false) => rpc("clipboard.paste", { session_id: getUiState().sid }).then((r) => {
|
|
54779
55413
|
if (!r) {
|
|
54780
|
-
return
|
|
55414
|
+
return;
|
|
54781
55415
|
}
|
|
54782
55416
|
if (r.attached) {
|
|
54783
|
-
|
|
54784
|
-
|
|
54785
|
-
height: r.height,
|
|
54786
|
-
name: r.name ?? `Image #${r.count ?? 1}`,
|
|
54787
|
-
token_estimate: r.token_estimate,
|
|
54788
|
-
width: r.width
|
|
54789
|
-
});
|
|
54790
|
-
patchUiState({ status: "image attached" });
|
|
54791
|
-
return true;
|
|
55417
|
+
const meta = imageTokenMeta(r);
|
|
55418
|
+
return sys(`\u{1F4CE} Image #${r.count} attached from clipboard${meta ? ` \xB7 ${meta}` : ""}`);
|
|
54792
55419
|
}
|
|
54793
55420
|
if (!quiet) {
|
|
54794
55421
|
sys(r.message || "No image found in clipboard");
|
|
54795
55422
|
}
|
|
54796
|
-
return false;
|
|
54797
55423
|
}),
|
|
54798
|
-
[
|
|
55424
|
+
[rpc, sys]
|
|
54799
55425
|
);
|
|
54800
55426
|
clipboardPasteRef.current = paste2;
|
|
54801
55427
|
const { dispatchSubmission, send, sendQueued, submit } = useSubmission({
|
|
@@ -55035,7 +55661,6 @@ function useMainApp(gw) {
|
|
|
55035
55661
|
cols,
|
|
55036
55662
|
compIdx: composerState.compIdx,
|
|
55037
55663
|
completions: composerState.completions,
|
|
55038
|
-
attachedImage: composerState.attachedImage,
|
|
55039
55664
|
empty,
|
|
55040
55665
|
handleTextPaste: composerActions.handleTextPaste,
|
|
55041
55666
|
input: composerState.input,
|
|
@@ -55092,6 +55717,7 @@ var init_useMainApp = __esm({
|
|
|
55092
55717
|
init_env();
|
|
55093
55718
|
init_limits();
|
|
55094
55719
|
init_details();
|
|
55720
|
+
init_messages();
|
|
55095
55721
|
init_paths();
|
|
55096
55722
|
init_useGitBranch();
|
|
55097
55723
|
init_useVirtualHistory();
|
|
@@ -59544,7 +60170,7 @@ var init_emoji = __esm({
|
|
|
59544
60170
|
});
|
|
59545
60171
|
|
|
59546
60172
|
// src/lib/externalLink.ts
|
|
59547
|
-
import { isIP } from "node:net";
|
|
60173
|
+
import { isIP as isIP2 } from "node:net";
|
|
59548
60174
|
import { useEffect as useEffect21, useMemo as useMemo13, useState as useState25 } from "react";
|
|
59549
60175
|
function normalizeExternalUrl(value) {
|
|
59550
60176
|
const trimmed = value.trim();
|
|
@@ -59650,13 +60276,13 @@ function isPrivateIpv6(value) {
|
|
|
59650
60276
|
}
|
|
59651
60277
|
return false;
|
|
59652
60278
|
}
|
|
59653
|
-
function
|
|
60279
|
+
function normalizeHostname2(value) {
|
|
59654
60280
|
const withoutBrackets = value.replace(/^\[/, "").replace(/\]$/, "");
|
|
59655
60281
|
const withoutZoneId = withoutBrackets.split("%", 1)[0];
|
|
59656
60282
|
return withoutZoneId.replace(/\.$/, "").toLowerCase();
|
|
59657
60283
|
}
|
|
59658
60284
|
function isPrivateOrLocalHost(hostname) {
|
|
59659
|
-
const normalized =
|
|
60285
|
+
const normalized = normalizeHostname2(hostname);
|
|
59660
60286
|
if (!normalized) {
|
|
59661
60287
|
return true;
|
|
59662
60288
|
}
|
|
@@ -59666,7 +60292,7 @@ function isPrivateOrLocalHost(hostname) {
|
|
|
59666
60292
|
if (LOCAL_HOST_SUFFIXES.some((suffix) => normalized.endsWith(suffix))) {
|
|
59667
60293
|
return true;
|
|
59668
60294
|
}
|
|
59669
|
-
const ipVersion =
|
|
60295
|
+
const ipVersion = isIP2(normalized);
|
|
59670
60296
|
if (ipVersion === 4) {
|
|
59671
60297
|
return isPrivateIpv4(normalized);
|
|
59672
60298
|
}
|
|
@@ -59850,7 +60476,7 @@ var init_externalLink = __esm({
|
|
|
59850
60476
|
__name(parseIpv4Octets, "parseIpv4Octets");
|
|
59851
60477
|
__name(isPrivateIpv4, "isPrivateIpv4");
|
|
59852
60478
|
__name(isPrivateIpv6, "isPrivateIpv6");
|
|
59853
|
-
__name(
|
|
60479
|
+
__name(normalizeHostname2, "normalizeHostname");
|
|
59854
60480
|
__name(isPrivateOrLocalHost, "isPrivateOrLocalHost");
|
|
59855
60481
|
__name(isTitleFetchable, "isTitleFetchable");
|
|
59856
60482
|
__name(decodeHtmlEntities, "decodeHtmlEntities");
|
|
@@ -62396,7 +63022,6 @@ var init_appLayout = __esm({
|
|
|
62396
63022
|
init_env();
|
|
62397
63023
|
init_limits();
|
|
62398
63024
|
init_placeholders();
|
|
62399
|
-
init_messages();
|
|
62400
63025
|
init_inputMetrics();
|
|
62401
63026
|
init_perfPane();
|
|
62402
63027
|
init_agentsOverlay();
|
|
@@ -62516,7 +63141,6 @@ var init_appLayout = __esm({
|
|
|
62516
63141
|
const inputColumns = stableComposerColumns(composer.cols, promptWidth);
|
|
62517
63142
|
const inputHeight = inputVisualHeight(composer.input, inputColumns);
|
|
62518
63143
|
const inputMouseRef = useRef19(null);
|
|
62519
|
-
const attachedImageMeta = composer.attachedImage ? imageTokenMeta(composer.attachedImage) : "";
|
|
62520
63144
|
const captureInputDrag = /* @__PURE__ */ __name((e) => {
|
|
62521
63145
|
if (e.button !== 0) {
|
|
62522
63146
|
return;
|
|
@@ -62587,12 +63211,6 @@ var init_appLayout = __esm({
|
|
|
62587
63211
|
),
|
|
62588
63212
|
composer.input === "?" && !composer.inputBuf.length && /* @__PURE__ */ jsx41(HelpHint, { t: ui.theme }),
|
|
62589
63213
|
!isBlocked && /* @__PURE__ */ jsxs28(Fragment11, { children: [
|
|
62590
|
-
composer.attachedImage && /* @__PURE__ */ jsx41(Box_default, { width: Math.max(1, composer.cols - 2), children: /* @__PURE__ */ jsxs28(Text9, { color: ui.theme.color.systemNote, wrap: "truncate-end", children: [
|
|
62591
|
-
"\u{1F4CE} ",
|
|
62592
|
-
composer.attachedImage.name,
|
|
62593
|
-
attachedImageMeta ? ` \xB7 ${attachedImageMeta}` : "",
|
|
62594
|
-
/* @__PURE__ */ jsx41(Text9, { color: ui.theme.color.muted, children: " \xB7 Esc to remove" })
|
|
62595
|
-
] }) }),
|
|
62596
63214
|
composer.inputBuf.map((line, i) => /* @__PURE__ */ jsxs28(Box_default, { children: [
|
|
62597
63215
|
/* @__PURE__ */ jsx41(Box_default, { width: promptWidth, children: i === 0 ? /* @__PURE__ */ jsx41(PromptPrefix, { color: ui.theme.color.muted, promptText, width: promptWidth }) : /* @__PURE__ */ jsx41(Text9, { color: ui.theme.color.muted, children: promptBlank }) }),
|
|
62598
63216
|
/* @__PURE__ */ jsx41(Text9, { color: ui.theme.color.composeText, children: line || " " })
|