@docyrus/docyrus 0.0.60 → 0.0.63
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/main.js +7 -4
- package/main.js.map +2 -2
- package/package.json +1 -1
- package/resources/pi-agent/extensions/browser-tools.ts +229 -0
- package/server-loader.js +588 -8
- package/server-loader.js.map +4 -4
package/server-loader.js
CHANGED
|
@@ -43986,6 +43986,199 @@ async function listAllTools(params) {
|
|
|
43986
43986
|
return tools;
|
|
43987
43987
|
}
|
|
43988
43988
|
|
|
43989
|
+
// src/server/browserToolSchemas.ts
|
|
43990
|
+
var BROWSER_TOOL_PREFIX = "docyrus_browser_";
|
|
43991
|
+
var BROWSER_TOOL_SCHEMAS = [
|
|
43992
|
+
{
|
|
43993
|
+
name: "docyrus_browser_navigate",
|
|
43994
|
+
description: "Navigate the preview browser to a URL. Use --reload to force reload after navigation.",
|
|
43995
|
+
source: "built-in",
|
|
43996
|
+
inputSchema: {
|
|
43997
|
+
type: "object",
|
|
43998
|
+
properties: {
|
|
43999
|
+
url: { type: "string", description: "URL to navigate to" },
|
|
44000
|
+
reload: { type: "boolean", description: "Force reload after navigation" }
|
|
44001
|
+
},
|
|
44002
|
+
required: ["url"]
|
|
44003
|
+
}
|
|
44004
|
+
},
|
|
44005
|
+
{
|
|
44006
|
+
name: "docyrus_browser_wait",
|
|
44007
|
+
description: "Wait for a condition before proceeding: network idle, CSS selector appearance, URL pattern match, or fixed delay.",
|
|
44008
|
+
source: "built-in",
|
|
44009
|
+
inputSchema: {
|
|
44010
|
+
type: "object",
|
|
44011
|
+
properties: {
|
|
44012
|
+
idle: { type: "boolean", description: "Wait for network idle" },
|
|
44013
|
+
selector: { type: "string", description: "Wait for CSS selector to appear" },
|
|
44014
|
+
url: { type: "string", description: "Wait for URL to match glob pattern" },
|
|
44015
|
+
ms: { type: "number", description: "Wait for fixed milliseconds" },
|
|
44016
|
+
timeout: { type: "number", description: "Maximum wait time in ms (default: 15000)" }
|
|
44017
|
+
}
|
|
44018
|
+
}
|
|
44019
|
+
},
|
|
44020
|
+
{
|
|
44021
|
+
name: "docyrus_browser_snapshot",
|
|
44022
|
+
description: "Get a compact snapshot of interactive page elements with refs (@e1, @e2, ...) for use in click/fill/select. Returns tag, text, type, name, value, placeholder, href for each element.",
|
|
44023
|
+
source: "built-in",
|
|
44024
|
+
inputSchema: {
|
|
44025
|
+
type: "object",
|
|
44026
|
+
properties: {
|
|
44027
|
+
all: { type: "boolean", description: "Include all elements, not just interactive ones" },
|
|
44028
|
+
selector: { type: "string", description: "Scope snapshot to a CSS selector subtree" }
|
|
44029
|
+
}
|
|
44030
|
+
}
|
|
44031
|
+
},
|
|
44032
|
+
{
|
|
44033
|
+
name: "docyrus_browser_click",
|
|
44034
|
+
description: "Click an element by snapshot ref (@e1), CSS selector, or x,y coordinates. Coordinate clicks pass through iframes and shadow DOM at the compositor level.",
|
|
44035
|
+
source: "built-in",
|
|
44036
|
+
inputSchema: {
|
|
44037
|
+
type: "object",
|
|
44038
|
+
properties: {
|
|
44039
|
+
target: { type: "string", description: "Snapshot ref (@e1), CSS selector, or x coordinate" },
|
|
44040
|
+
x: { type: "number", description: "X coordinate (when using coordinate mode)" },
|
|
44041
|
+
y: { type: "number", description: "Y coordinate (when using coordinate mode)" },
|
|
44042
|
+
timeout: { type: "number", description: "Timeout in ms (default: 5000)" }
|
|
44043
|
+
},
|
|
44044
|
+
required: ["target"]
|
|
44045
|
+
}
|
|
44046
|
+
},
|
|
44047
|
+
{
|
|
44048
|
+
name: "docyrus_browser_fill",
|
|
44049
|
+
description: "Clear and type a value into an input or textarea by snapshot ref or CSS selector.",
|
|
44050
|
+
source: "built-in",
|
|
44051
|
+
inputSchema: {
|
|
44052
|
+
type: "object",
|
|
44053
|
+
properties: {
|
|
44054
|
+
target: { type: "string", description: "Snapshot ref (@e1) or CSS selector" },
|
|
44055
|
+
value: { type: "string", description: "Value to type into the element" },
|
|
44056
|
+
timeout: { type: "number", description: "Timeout in ms (default: 5000)" }
|
|
44057
|
+
},
|
|
44058
|
+
required: ["target", "value"]
|
|
44059
|
+
}
|
|
44060
|
+
},
|
|
44061
|
+
{
|
|
44062
|
+
name: "docyrus_browser_select",
|
|
44063
|
+
description: "Select a dropdown option by snapshot ref or CSS selector. Matches by option text or value.",
|
|
44064
|
+
source: "built-in",
|
|
44065
|
+
inputSchema: {
|
|
44066
|
+
type: "object",
|
|
44067
|
+
properties: {
|
|
44068
|
+
target: { type: "string", description: "Snapshot ref (@e1) or CSS selector" },
|
|
44069
|
+
value: { type: "string", description: "Option text or value to select" }
|
|
44070
|
+
},
|
|
44071
|
+
required: ["target", "value"]
|
|
44072
|
+
}
|
|
44073
|
+
},
|
|
44074
|
+
{
|
|
44075
|
+
name: "docyrus_browser_eval",
|
|
44076
|
+
description: "Evaluate JavaScript in the preview page and return the result. Supports expressions and multi-statement code with async/await.",
|
|
44077
|
+
source: "built-in",
|
|
44078
|
+
inputSchema: {
|
|
44079
|
+
type: "object",
|
|
44080
|
+
properties: {
|
|
44081
|
+
code: { type: "string", description: "JavaScript code to evaluate" }
|
|
44082
|
+
},
|
|
44083
|
+
required: ["code"]
|
|
44084
|
+
}
|
|
44085
|
+
},
|
|
44086
|
+
{
|
|
44087
|
+
name: "docyrus_browser_screenshot",
|
|
44088
|
+
description: "Capture a screenshot of the preview browser. Returns base64-encoded PNG image.",
|
|
44089
|
+
source: "built-in",
|
|
44090
|
+
inputSchema: {
|
|
44091
|
+
type: "object",
|
|
44092
|
+
properties: {
|
|
44093
|
+
full: { type: "boolean", description: "Capture full page instead of just the viewport" }
|
|
44094
|
+
}
|
|
44095
|
+
}
|
|
44096
|
+
},
|
|
44097
|
+
{
|
|
44098
|
+
name: "docyrus_browser_console",
|
|
44099
|
+
description: "Read captured console messages (log, warn, error) from the preview page. Run early in the session to start capturing.",
|
|
44100
|
+
source: "built-in",
|
|
44101
|
+
inputSchema: {
|
|
44102
|
+
type: "object",
|
|
44103
|
+
properties: {
|
|
44104
|
+
level: { type: "string", description: "Filter by level: log, warn, error, info, debug" }
|
|
44105
|
+
}
|
|
44106
|
+
}
|
|
44107
|
+
},
|
|
44108
|
+
{
|
|
44109
|
+
name: "docyrus_browser_network",
|
|
44110
|
+
description: "Inspect captured network requests from the preview page. Filter by HTTP method, status code, or URL.",
|
|
44111
|
+
source: "built-in",
|
|
44112
|
+
inputSchema: {
|
|
44113
|
+
type: "object",
|
|
44114
|
+
properties: {
|
|
44115
|
+
method: { type: "string", description: "Filter by HTTP method (GET, POST, etc.)" },
|
|
44116
|
+
status: { type: "string", description: "Filter by status code or pattern (200, 4xx, 5xx)" },
|
|
44117
|
+
url: { type: "string", description: "Filter by URL substring" }
|
|
44118
|
+
}
|
|
44119
|
+
}
|
|
44120
|
+
},
|
|
44121
|
+
{
|
|
44122
|
+
name: "docyrus_browser_cookies",
|
|
44123
|
+
description: "List cookies for the preview page, optionally filtered by name or domain.",
|
|
44124
|
+
source: "built-in",
|
|
44125
|
+
inputSchema: {
|
|
44126
|
+
type: "object",
|
|
44127
|
+
properties: {
|
|
44128
|
+
name: { type: "string", description: "Filter cookies by exact name" },
|
|
44129
|
+
domain: { type: "string", description: "Filter cookies by domain (substring match)" }
|
|
44130
|
+
}
|
|
44131
|
+
}
|
|
44132
|
+
},
|
|
44133
|
+
{
|
|
44134
|
+
name: "docyrus_browser_content",
|
|
44135
|
+
description: "Extract readable markdown content from the current preview page using Readability.",
|
|
44136
|
+
source: "built-in",
|
|
44137
|
+
inputSchema: {
|
|
44138
|
+
type: "object",
|
|
44139
|
+
properties: {}
|
|
44140
|
+
}
|
|
44141
|
+
},
|
|
44142
|
+
{
|
|
44143
|
+
name: "docyrus_browser_info",
|
|
44144
|
+
description: "Get current preview page info: URL, title, viewport dimensions, scroll position, page size, and ready state.",
|
|
44145
|
+
source: "built-in",
|
|
44146
|
+
inputSchema: {
|
|
44147
|
+
type: "object",
|
|
44148
|
+
properties: {}
|
|
44149
|
+
}
|
|
44150
|
+
},
|
|
44151
|
+
{
|
|
44152
|
+
name: "docyrus_browser_devtools",
|
|
44153
|
+
description: "Read @docyrus/devtools runtime diagnostics from the preview page: API errors, usage issues, and console entries.",
|
|
44154
|
+
source: "built-in",
|
|
44155
|
+
inputSchema: {
|
|
44156
|
+
type: "object",
|
|
44157
|
+
properties: {
|
|
44158
|
+
subcommand: {
|
|
44159
|
+
type: "string",
|
|
44160
|
+
enum: ["state", "errors", "issues", "console"],
|
|
44161
|
+
description: "What to read: state (full), errors (API errors), issues (usage issues), console (entries)"
|
|
44162
|
+
},
|
|
44163
|
+
level: { type: "string", description: "Filter console entries by level (for console subcommand)" }
|
|
44164
|
+
},
|
|
44165
|
+
required: ["subcommand"]
|
|
44166
|
+
}
|
|
44167
|
+
},
|
|
44168
|
+
{
|
|
44169
|
+
name: "docyrus_browser_open",
|
|
44170
|
+
description: "Open an external website in a new browser window for automation. Returns a webviewId for targeting subsequent commands.",
|
|
44171
|
+
source: "built-in",
|
|
44172
|
+
inputSchema: {
|
|
44173
|
+
type: "object",
|
|
44174
|
+
properties: {
|
|
44175
|
+
url: { type: "string", description: "URL to open in the new browser window" }
|
|
44176
|
+
},
|
|
44177
|
+
required: ["url"]
|
|
44178
|
+
}
|
|
44179
|
+
}
|
|
44180
|
+
];
|
|
44181
|
+
|
|
43989
44182
|
// src/server/sessionConfig.ts
|
|
43990
44183
|
var import_node_fs12 = require("node:fs");
|
|
43991
44184
|
var path5 = __toESM(require("node:path"));
|
|
@@ -44345,6 +44538,107 @@ function resolveSafePath(cwd, requestPath) {
|
|
|
44345
44538
|
}
|
|
44346
44539
|
return resolved;
|
|
44347
44540
|
}
|
|
44541
|
+
var ALLOWED_UPLOAD_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
44542
|
+
// Text / code
|
|
44543
|
+
".txt",
|
|
44544
|
+
".md",
|
|
44545
|
+
".markdown",
|
|
44546
|
+
".json",
|
|
44547
|
+
".jsonl",
|
|
44548
|
+
".csv",
|
|
44549
|
+
".tsv",
|
|
44550
|
+
".xml",
|
|
44551
|
+
".yaml",
|
|
44552
|
+
".yml",
|
|
44553
|
+
".toml",
|
|
44554
|
+
".ini",
|
|
44555
|
+
".cfg",
|
|
44556
|
+
".conf",
|
|
44557
|
+
".log",
|
|
44558
|
+
".rtf",
|
|
44559
|
+
// Documents
|
|
44560
|
+
".pdf",
|
|
44561
|
+
".doc",
|
|
44562
|
+
".docx",
|
|
44563
|
+
".xls",
|
|
44564
|
+
".xlsx",
|
|
44565
|
+
".ppt",
|
|
44566
|
+
".pptx",
|
|
44567
|
+
".odt",
|
|
44568
|
+
".ods",
|
|
44569
|
+
".odp",
|
|
44570
|
+
".pages",
|
|
44571
|
+
".numbers",
|
|
44572
|
+
".key",
|
|
44573
|
+
// Images
|
|
44574
|
+
".png",
|
|
44575
|
+
".jpg",
|
|
44576
|
+
".jpeg",
|
|
44577
|
+
".gif",
|
|
44578
|
+
".bmp",
|
|
44579
|
+
".webp",
|
|
44580
|
+
".svg",
|
|
44581
|
+
".ico",
|
|
44582
|
+
".tiff",
|
|
44583
|
+
".tif",
|
|
44584
|
+
".heic",
|
|
44585
|
+
".heif",
|
|
44586
|
+
".avif",
|
|
44587
|
+
// Audio
|
|
44588
|
+
".mp3",
|
|
44589
|
+
".wav",
|
|
44590
|
+
".ogg",
|
|
44591
|
+
".flac",
|
|
44592
|
+
".aac",
|
|
44593
|
+
".m4a",
|
|
44594
|
+
".wma",
|
|
44595
|
+
".opus",
|
|
44596
|
+
// Video
|
|
44597
|
+
".mp4",
|
|
44598
|
+
".webm",
|
|
44599
|
+
".mov",
|
|
44600
|
+
".avi",
|
|
44601
|
+
".mkv",
|
|
44602
|
+
".m4v",
|
|
44603
|
+
".wmv",
|
|
44604
|
+
// Archives
|
|
44605
|
+
".zip",
|
|
44606
|
+
".tar",
|
|
44607
|
+
".gz",
|
|
44608
|
+
".7z",
|
|
44609
|
+
".rar",
|
|
44610
|
+
// Fonts
|
|
44611
|
+
".woff",
|
|
44612
|
+
".woff2",
|
|
44613
|
+
".ttf",
|
|
44614
|
+
".otf",
|
|
44615
|
+
".eot"
|
|
44616
|
+
]);
|
|
44617
|
+
function sanitizeUploadPath(filePath) {
|
|
44618
|
+
const cleaned = filePath.replace(/[\x00-\x1f\x7f]/g, "");
|
|
44619
|
+
const normalized = cleaned.replace(/\\/g, "/").replace(/\/{2,}/g, "/");
|
|
44620
|
+
if (!normalized || normalized === "/") {
|
|
44621
|
+
throw new Error("Invalid upload path");
|
|
44622
|
+
}
|
|
44623
|
+
const ext = normalized.includes(".") ? `.${normalized.split(".").pop().toLowerCase()}` : "";
|
|
44624
|
+
if (!ext || !ALLOWED_UPLOAD_EXTENSIONS.has(ext)) {
|
|
44625
|
+
throw new Error(`File type '${ext || "(none)"}' is not allowed. Allowed: text, images, audio, video, and document files.`);
|
|
44626
|
+
}
|
|
44627
|
+
return normalized;
|
|
44628
|
+
}
|
|
44629
|
+
async function assertNotSymlink(filePath) {
|
|
44630
|
+
try {
|
|
44631
|
+
const stats = await (0, import_promises18.lstat)(filePath);
|
|
44632
|
+
if (stats.isSymbolicLink()) {
|
|
44633
|
+
throw new Error("Cannot write to symbolic link");
|
|
44634
|
+
}
|
|
44635
|
+
} catch (error48) {
|
|
44636
|
+
if (error48 instanceof Error && "code" in error48 && error48.code === "ENOENT") {
|
|
44637
|
+
return;
|
|
44638
|
+
}
|
|
44639
|
+
throw error48;
|
|
44640
|
+
}
|
|
44641
|
+
}
|
|
44348
44642
|
function globToRegex(pattern) {
|
|
44349
44643
|
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\0").replace(/\*/g, "[^/]*").replace(/\0/g, ".*").replace(/\?/g, "[^/]");
|
|
44350
44644
|
return new RegExp(`^${escaped}$`);
|
|
@@ -44425,6 +44719,7 @@ async function walkFiles(params) {
|
|
|
44425
44719
|
}
|
|
44426
44720
|
async function createAgentServer(params) {
|
|
44427
44721
|
const { port, sessionManager, modelRegistry, authRuntime, context, onCreateSession, onResumeSession, authToken } = params;
|
|
44722
|
+
const extensionUIBridge = params.extensionUIBridge;
|
|
44428
44723
|
let activeSession = params.session;
|
|
44429
44724
|
let sessionMode = "normal";
|
|
44430
44725
|
const pendingAskUserRequests = /* @__PURE__ */ new Map();
|
|
@@ -44534,13 +44829,41 @@ async function createAgentServer(params) {
|
|
|
44534
44829
|
const encoder = new TextEncoder();
|
|
44535
44830
|
const stream = new ReadableStream({
|
|
44536
44831
|
start(controller) {
|
|
44832
|
+
let closed = false;
|
|
44537
44833
|
function writeChunk(chunk) {
|
|
44538
|
-
|
|
44834
|
+
if (closed) {
|
|
44835
|
+
return;
|
|
44836
|
+
}
|
|
44837
|
+
try {
|
|
44838
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}
|
|
44539
44839
|
|
|
44540
44840
|
`));
|
|
44841
|
+
} catch {
|
|
44842
|
+
closed = true;
|
|
44843
|
+
}
|
|
44844
|
+
}
|
|
44845
|
+
function closeController() {
|
|
44846
|
+
if (closed) {
|
|
44847
|
+
return;
|
|
44848
|
+
}
|
|
44849
|
+
closed = true;
|
|
44850
|
+
try {
|
|
44851
|
+
controller.close();
|
|
44852
|
+
} catch {
|
|
44853
|
+
}
|
|
44541
44854
|
}
|
|
44542
44855
|
writeChunk({ type: "start" });
|
|
44543
44856
|
writeChunk({ type: "start-step" });
|
|
44857
|
+
let extensionUICleanup;
|
|
44858
|
+
if (extensionUIBridge) {
|
|
44859
|
+
extensionUIBridge.setRequestHandler((request) => {
|
|
44860
|
+
writeChunk(request);
|
|
44861
|
+
});
|
|
44862
|
+
extensionUICleanup = () => {
|
|
44863
|
+
extensionUIBridge.setRequestHandler(() => {
|
|
44864
|
+
});
|
|
44865
|
+
};
|
|
44866
|
+
}
|
|
44544
44867
|
const bridge = createEventBridge({
|
|
44545
44868
|
messageId,
|
|
44546
44869
|
onChunk: writeChunk,
|
|
@@ -44548,15 +44871,17 @@ async function createAgentServer(params) {
|
|
|
44548
44871
|
pendingAskUserRequests.set(sessionId, { toolCallId, request });
|
|
44549
44872
|
},
|
|
44550
44873
|
onDone: () => {
|
|
44874
|
+
extensionUICleanup?.();
|
|
44551
44875
|
writeChunk({ type: "finish-step" });
|
|
44552
44876
|
writeChunk({ type: "finish" });
|
|
44553
|
-
|
|
44877
|
+
closeController();
|
|
44554
44878
|
},
|
|
44555
44879
|
onError: (errorText) => {
|
|
44880
|
+
extensionUICleanup?.();
|
|
44556
44881
|
writeChunk({ type: "error", errorText });
|
|
44557
44882
|
writeChunk({ type: "finish-step" });
|
|
44558
44883
|
writeChunk({ type: "finish" });
|
|
44559
|
-
|
|
44884
|
+
closeController();
|
|
44560
44885
|
}
|
|
44561
44886
|
});
|
|
44562
44887
|
const unsubscribe = activeSession.subscribe((event) => {
|
|
@@ -44568,17 +44893,19 @@ async function createAgentServer(params) {
|
|
|
44568
44893
|
activeSession.prompt(promptText).then(() => {
|
|
44569
44894
|
if (!activeSession.isStreaming) {
|
|
44570
44895
|
unsubscribe();
|
|
44896
|
+
extensionUICleanup?.();
|
|
44571
44897
|
writeChunk({ type: "finish-step" });
|
|
44572
44898
|
writeChunk({ type: "finish" });
|
|
44573
|
-
|
|
44899
|
+
closeController();
|
|
44574
44900
|
}
|
|
44575
44901
|
}).catch((error48) => {
|
|
44576
44902
|
const errorMessage = error48 instanceof Error ? error48.message : String(error48);
|
|
44903
|
+
extensionUICleanup?.();
|
|
44577
44904
|
writeChunk({ type: "error", errorText: errorMessage });
|
|
44578
44905
|
writeChunk({ type: "finish-step" });
|
|
44579
44906
|
writeChunk({ type: "finish" });
|
|
44580
44907
|
unsubscribe();
|
|
44581
|
-
|
|
44908
|
+
closeController();
|
|
44582
44909
|
});
|
|
44583
44910
|
}
|
|
44584
44911
|
});
|
|
@@ -44723,7 +45050,8 @@ async function createAgentServer(params) {
|
|
|
44723
45050
|
agentDir: context.agentDir,
|
|
44724
45051
|
version: context.version,
|
|
44725
45052
|
sessionDir: context.sessionDir,
|
|
44726
|
-
thinkingLevel: context.thinkingLevel
|
|
45053
|
+
thinkingLevel: context.thinkingLevel,
|
|
45054
|
+
desktop: context.desktop
|
|
44727
45055
|
});
|
|
44728
45056
|
});
|
|
44729
45057
|
app.get("/api/models", (c) => {
|
|
@@ -45412,6 +45740,59 @@ async function createAgentServer(params) {
|
|
|
45412
45740
|
return c.json({ error: message }, 400);
|
|
45413
45741
|
}
|
|
45414
45742
|
});
|
|
45743
|
+
app.post("/api/fs/upload", async (c) => {
|
|
45744
|
+
const MAX_UPLOAD_SIZE = 50 * 1024 * 1024;
|
|
45745
|
+
const contentLength = parseInt(c.req.header("content-length") || "0", 10);
|
|
45746
|
+
if (contentLength > MAX_UPLOAD_SIZE) {
|
|
45747
|
+
return c.json({ error: `File too large. Maximum upload size is ${MAX_UPLOAD_SIZE / 1024 / 1024} MB` }, 413);
|
|
45748
|
+
}
|
|
45749
|
+
const contentType = c.req.header("content-type") || "";
|
|
45750
|
+
if (contentType.includes("multipart/form-data")) {
|
|
45751
|
+
const formData = await c.req.formData();
|
|
45752
|
+
const file2 = formData.get("file");
|
|
45753
|
+
const targetPath2 = formData.get("path");
|
|
45754
|
+
if (!file2 || !(file2 instanceof File)) {
|
|
45755
|
+
return c.json({ error: "Missing required form field: file" }, 400);
|
|
45756
|
+
}
|
|
45757
|
+
if (file2.size > MAX_UPLOAD_SIZE) {
|
|
45758
|
+
return c.json({ error: `File too large (${(file2.size / 1024 / 1024).toFixed(1)} MB). Maximum is ${MAX_UPLOAD_SIZE / 1024 / 1024} MB` }, 413);
|
|
45759
|
+
}
|
|
45760
|
+
const destPath = typeof targetPath2 === "string" && targetPath2 ? targetPath2 : file2.name;
|
|
45761
|
+
try {
|
|
45762
|
+
const sanitized = sanitizeUploadPath(destPath);
|
|
45763
|
+
const resolved = resolveSafePath(context.cwd, sanitized);
|
|
45764
|
+
await assertNotSymlink(resolved);
|
|
45765
|
+
await (0, import_promises18.mkdir)((0, import_node_path22.join)(resolved, ".."), { recursive: true });
|
|
45766
|
+
const buffer = Buffer.from(await file2.arrayBuffer());
|
|
45767
|
+
await (0, import_promises18.writeFile)(resolved, buffer);
|
|
45768
|
+
const fileStat = await (0, import_promises18.stat)(resolved);
|
|
45769
|
+
return c.json({ ok: true, path: sanitized, name: file2.name, size: fileStat.size, type: file2.type });
|
|
45770
|
+
} catch (error48) {
|
|
45771
|
+
const message = error48 instanceof Error ? error48.message : String(error48);
|
|
45772
|
+
return c.json({ error: message }, 400);
|
|
45773
|
+
}
|
|
45774
|
+
}
|
|
45775
|
+
const targetPath = c.req.query("path") || c.req.header("x-file-path");
|
|
45776
|
+
if (!targetPath) {
|
|
45777
|
+
return c.json({ error: "Missing target path. Use multipart form field 'path', query param 'path', or header 'X-File-Path'" }, 400);
|
|
45778
|
+
}
|
|
45779
|
+
try {
|
|
45780
|
+
const sanitized = sanitizeUploadPath(targetPath);
|
|
45781
|
+
const resolved = resolveSafePath(context.cwd, sanitized);
|
|
45782
|
+
await assertNotSymlink(resolved);
|
|
45783
|
+
await (0, import_promises18.mkdir)((0, import_node_path22.join)(resolved, ".."), { recursive: true });
|
|
45784
|
+
const buffer = Buffer.from(await c.req.arrayBuffer());
|
|
45785
|
+
if (buffer.length > MAX_UPLOAD_SIZE) {
|
|
45786
|
+
return c.json({ error: `File too large (${(buffer.length / 1024 / 1024).toFixed(1)} MB). Maximum is ${MAX_UPLOAD_SIZE / 1024 / 1024} MB` }, 413);
|
|
45787
|
+
}
|
|
45788
|
+
await (0, import_promises18.writeFile)(resolved, buffer);
|
|
45789
|
+
const fileStat = await (0, import_promises18.stat)(resolved);
|
|
45790
|
+
return c.json({ ok: true, path: sanitized, size: fileStat.size });
|
|
45791
|
+
} catch (error48) {
|
|
45792
|
+
const message = error48 instanceof Error ? error48.message : String(error48);
|
|
45793
|
+
return c.json({ error: message }, 400);
|
|
45794
|
+
}
|
|
45795
|
+
});
|
|
45415
45796
|
app.post("/api/fs/mkdir", async (c) => {
|
|
45416
45797
|
const body2 = await c.req.json();
|
|
45417
45798
|
if (!body2.path) {
|
|
@@ -46064,6 +46445,20 @@ async function createAgentServer(params) {
|
|
|
46064
46445
|
return c.json({ error: message }, 500);
|
|
46065
46446
|
}
|
|
46066
46447
|
});
|
|
46448
|
+
app.post("/api/extension-ui-response", async (c) => {
|
|
46449
|
+
if (!extensionUIBridge) {
|
|
46450
|
+
return c.json({ error: "Extension UI bridge not available" }, 404);
|
|
46451
|
+
}
|
|
46452
|
+
const body2 = await c.req.json();
|
|
46453
|
+
if (!body2.id) {
|
|
46454
|
+
return c.json({ error: "Missing required field: id" }, 400);
|
|
46455
|
+
}
|
|
46456
|
+
const handled = extensionUIBridge.handleResponse(body2);
|
|
46457
|
+
if (!handled) {
|
|
46458
|
+
return c.json({ error: `No pending extension UI request for id: ${body2.id}` }, 404);
|
|
46459
|
+
}
|
|
46460
|
+
return c.json({ ok: true });
|
|
46461
|
+
});
|
|
46067
46462
|
app.get("/api/tools", async (c) => {
|
|
46068
46463
|
try {
|
|
46069
46464
|
const tools = await listAllTools({
|
|
@@ -46071,11 +46466,15 @@ async function createAgentServer(params) {
|
|
|
46071
46466
|
agentDir: context.agentDir,
|
|
46072
46467
|
cwd: context.cwd
|
|
46073
46468
|
});
|
|
46469
|
+
if (context.desktop) {
|
|
46470
|
+
tools.push(...BROWSER_TOOL_SCHEMAS);
|
|
46471
|
+
}
|
|
46074
46472
|
const builtInCount = tools.filter((t) => t.source === "built-in").length;
|
|
46075
46473
|
const mcpCount = tools.filter((t) => t.source !== "built-in").length;
|
|
46076
46474
|
return c.json({
|
|
46077
46475
|
tools,
|
|
46078
|
-
summary: { builtIn: builtInCount, mcp: mcpCount, total: tools.length }
|
|
46476
|
+
summary: { builtIn: builtInCount, mcp: mcpCount, total: tools.length },
|
|
46477
|
+
clientSideToolPrefixes: context.desktop ? [BROWSER_TOOL_PREFIX] : []
|
|
46079
46478
|
});
|
|
46080
46479
|
} catch (error48) {
|
|
46081
46480
|
const message = error48 instanceof Error ? error48.message : String(error48);
|
|
@@ -46267,6 +46666,8 @@ async function createAgentServer(params) {
|
|
|
46267
46666
|
process.stderr.write(` GET /api/fs/read \u2014 read file contents
|
|
46268
46667
|
`);
|
|
46269
46668
|
process.stderr.write(` POST /api/fs/write \u2014 write file
|
|
46669
|
+
`);
|
|
46670
|
+
process.stderr.write(` POST /api/fs/upload \u2014 upload file (multipart or raw)
|
|
46270
46671
|
`);
|
|
46271
46672
|
process.stderr.write(` POST /api/fs/mkdir \u2014 create directory
|
|
46272
46673
|
`);
|
|
@@ -46298,6 +46699,12 @@ async function createAgentServer(params) {
|
|
|
46298
46699
|
`);
|
|
46299
46700
|
process.stderr.write(` GET /api/tools \u2014 list all tools
|
|
46300
46701
|
`);
|
|
46702
|
+
if (context.desktop) {
|
|
46703
|
+
process.stderr.write(` POST /api/extension-ui-response \u2014 submit extension UI response
|
|
46704
|
+
`);
|
|
46705
|
+
process.stderr.write(` Browser tools: docyrus_browser_* (client-side via extension_ui)
|
|
46706
|
+
`);
|
|
46707
|
+
}
|
|
46301
46708
|
process.stderr.write(` * /api/cli/** \u2014 proxy any docyrus CLI command
|
|
46302
46709
|
`);
|
|
46303
46710
|
process.stderr.write(` WS /api/terminal \u2014 PTY terminal (WebSocket)
|
|
@@ -46493,6 +46900,163 @@ function createServerSessionAdapter(params) {
|
|
|
46493
46900
|
};
|
|
46494
46901
|
}
|
|
46495
46902
|
|
|
46903
|
+
// src/server/extensionUIBridge.ts
|
|
46904
|
+
var import_node_crypto7 = require("node:crypto");
|
|
46905
|
+
var ExtensionUIBridge = class {
|
|
46906
|
+
pending = /* @__PURE__ */ new Map();
|
|
46907
|
+
requestHandler;
|
|
46908
|
+
defaultTimeout;
|
|
46909
|
+
constructor(options) {
|
|
46910
|
+
this.requestHandler = options.onRequest;
|
|
46911
|
+
this.defaultTimeout = options.defaultTimeout ?? 12e4;
|
|
46912
|
+
}
|
|
46913
|
+
/**
|
|
46914
|
+
* Update the request handler. Used to wire into the active chat stream.
|
|
46915
|
+
*/
|
|
46916
|
+
setRequestHandler(handler2) {
|
|
46917
|
+
this.requestHandler = handler2;
|
|
46918
|
+
}
|
|
46919
|
+
/**
|
|
46920
|
+
* Handle an incoming extension_ui_response from the client.
|
|
46921
|
+
* Returns true if the response matched a pending request.
|
|
46922
|
+
*/
|
|
46923
|
+
handleResponse(response) {
|
|
46924
|
+
const entry = this.pending.get(response.id);
|
|
46925
|
+
if (!entry) {
|
|
46926
|
+
return false;
|
|
46927
|
+
}
|
|
46928
|
+
if (entry.timer) {
|
|
46929
|
+
clearTimeout(entry.timer);
|
|
46930
|
+
}
|
|
46931
|
+
this.pending.delete(response.id);
|
|
46932
|
+
entry.resolve(response);
|
|
46933
|
+
return true;
|
|
46934
|
+
}
|
|
46935
|
+
/**
|
|
46936
|
+
* Check if there are any pending requests.
|
|
46937
|
+
*/
|
|
46938
|
+
hasPending() {
|
|
46939
|
+
return this.pending.size > 0;
|
|
46940
|
+
}
|
|
46941
|
+
/**
|
|
46942
|
+
* Create a dialog promise that sends a request and waits for a response.
|
|
46943
|
+
*/
|
|
46944
|
+
createDialogPromise(request, defaultValue, parseResponse, opts) {
|
|
46945
|
+
if (opts?.signal?.aborted) {
|
|
46946
|
+
return Promise.resolve(defaultValue);
|
|
46947
|
+
}
|
|
46948
|
+
const id = (0, import_node_crypto7.randomUUID)();
|
|
46949
|
+
const timeout = opts?.timeout ?? this.defaultTimeout;
|
|
46950
|
+
return new Promise((resolve4, reject) => {
|
|
46951
|
+
const cleanup = () => {
|
|
46952
|
+
if (timer) {
|
|
46953
|
+
clearTimeout(timer);
|
|
46954
|
+
}
|
|
46955
|
+
opts?.signal?.removeEventListener("abort", onAbort);
|
|
46956
|
+
this.pending.delete(id);
|
|
46957
|
+
};
|
|
46958
|
+
const onAbort = () => {
|
|
46959
|
+
cleanup();
|
|
46960
|
+
resolve4(defaultValue);
|
|
46961
|
+
};
|
|
46962
|
+
opts?.signal?.addEventListener("abort", onAbort, { once: true });
|
|
46963
|
+
const timer = timeout > 0 ? setTimeout(() => {
|
|
46964
|
+
cleanup();
|
|
46965
|
+
resolve4(defaultValue);
|
|
46966
|
+
}, timeout) : void 0;
|
|
46967
|
+
this.pending.set(id, {
|
|
46968
|
+
resolve: (response) => {
|
|
46969
|
+
cleanup();
|
|
46970
|
+
resolve4(parseResponse(response));
|
|
46971
|
+
},
|
|
46972
|
+
reject: (error48) => {
|
|
46973
|
+
cleanup();
|
|
46974
|
+
reject(error48);
|
|
46975
|
+
},
|
|
46976
|
+
timer
|
|
46977
|
+
});
|
|
46978
|
+
this.requestHandler({ type: "extension_ui_request", id, ...request });
|
|
46979
|
+
});
|
|
46980
|
+
}
|
|
46981
|
+
/**
|
|
46982
|
+
* Build an ExtensionUIContext compatible with pi agent's extension system.
|
|
46983
|
+
*/
|
|
46984
|
+
createUIContext() {
|
|
46985
|
+
return {
|
|
46986
|
+
select: (title, options, opts) => this.createDialogPromise(
|
|
46987
|
+
{ method: "select", title, options, timeout: opts?.timeout },
|
|
46988
|
+
void 0,
|
|
46989
|
+
(r) => "cancelled" in r && r.cancelled ? void 0 : r.value,
|
|
46990
|
+
opts
|
|
46991
|
+
),
|
|
46992
|
+
confirm: (title, message, opts) => this.createDialogPromise(
|
|
46993
|
+
{ method: "confirm", title, message, timeout: opts?.timeout },
|
|
46994
|
+
false,
|
|
46995
|
+
(r) => "cancelled" in r && r.cancelled ? false : r.confirmed ?? false,
|
|
46996
|
+
opts
|
|
46997
|
+
),
|
|
46998
|
+
input: (title, placeholder, opts) => this.createDialogPromise(
|
|
46999
|
+
{ method: "input", title, placeholder, timeout: opts?.timeout },
|
|
47000
|
+
void 0,
|
|
47001
|
+
(r) => "cancelled" in r && r.cancelled ? void 0 : r.value,
|
|
47002
|
+
opts
|
|
47003
|
+
),
|
|
47004
|
+
editor: (title, prefill, opts) => this.createDialogPromise(
|
|
47005
|
+
{ method: "editor", title, placeholder: prefill },
|
|
47006
|
+
void 0,
|
|
47007
|
+
(r) => "cancelled" in r && r.cancelled ? void 0 : r.value,
|
|
47008
|
+
opts
|
|
47009
|
+
),
|
|
47010
|
+
notify: (message, notifyType) => {
|
|
47011
|
+
const id = (0, import_node_crypto7.randomUUID)();
|
|
47012
|
+
this.requestHandler({ type: "extension_ui_request", id, method: "notify", message, placeholder: notifyType });
|
|
47013
|
+
},
|
|
47014
|
+
setStatus: (statusKey, statusText) => {
|
|
47015
|
+
const id = (0, import_node_crypto7.randomUUID)();
|
|
47016
|
+
this.requestHandler({ type: "extension_ui_request", id, method: "setStatus", title: statusKey, message: statusText });
|
|
47017
|
+
},
|
|
47018
|
+
setWidget: (widgetKey, widgetLines) => {
|
|
47019
|
+
const id = (0, import_node_crypto7.randomUUID)();
|
|
47020
|
+
this.requestHandler({ type: "extension_ui_request", id, method: "setWidget", title: widgetKey, options: widgetLines });
|
|
47021
|
+
},
|
|
47022
|
+
setTitle: (title) => {
|
|
47023
|
+
const id = (0, import_node_crypto7.randomUUID)();
|
|
47024
|
+
this.requestHandler({ type: "extension_ui_request", id, method: "setTitle", title });
|
|
47025
|
+
},
|
|
47026
|
+
setEditorText: (text3) => {
|
|
47027
|
+
const id = (0, import_node_crypto7.randomUUID)();
|
|
47028
|
+
this.requestHandler({ type: "extension_ui_request", id, method: "set_editor_text", message: text3 });
|
|
47029
|
+
},
|
|
47030
|
+
// No-op methods for server mode (not applicable without TUI)
|
|
47031
|
+
setWorkingMessage: () => {
|
|
47032
|
+
},
|
|
47033
|
+
setHiddenThinkingLabel: () => {
|
|
47034
|
+
},
|
|
47035
|
+
setFooter: () => {
|
|
47036
|
+
},
|
|
47037
|
+
setHeader: () => {
|
|
47038
|
+
},
|
|
47039
|
+
onTerminalInput: () => () => {
|
|
47040
|
+
},
|
|
47041
|
+
custom: async () => void 0,
|
|
47042
|
+
pasteToEditor: () => {
|
|
47043
|
+
},
|
|
47044
|
+
getEditorText: () => "",
|
|
47045
|
+
setEditorComponent: () => {
|
|
47046
|
+
},
|
|
47047
|
+
get theme() {
|
|
47048
|
+
return { name: "default" };
|
|
47049
|
+
},
|
|
47050
|
+
getAllThemes: () => [],
|
|
47051
|
+
getTheme: () => void 0,
|
|
47052
|
+
setTheme: () => ({ success: false, error: "UI not available in server mode" }),
|
|
47053
|
+
getToolsExpanded: () => false,
|
|
47054
|
+
setToolsExpanded: () => {
|
|
47055
|
+
}
|
|
47056
|
+
};
|
|
47057
|
+
}
|
|
47058
|
+
};
|
|
47059
|
+
|
|
46496
47060
|
// src/services/spinner.ts
|
|
46497
47061
|
var import_picocolors = __toESM(require_picocolors());
|
|
46498
47062
|
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
@@ -46707,8 +47271,17 @@ async function main() {
|
|
|
46707
47271
|
`
|
|
46708
47272
|
);
|
|
46709
47273
|
}
|
|
47274
|
+
const extensionUIBridge = new ExtensionUIBridge({
|
|
47275
|
+
onRequest: () => {
|
|
47276
|
+
}
|
|
47277
|
+
// Will be wired up by agentServer after the chat stream is created
|
|
47278
|
+
});
|
|
47279
|
+
await session.bindExtensions({
|
|
47280
|
+
uiContext: extensionUIBridge.createUIContext()
|
|
47281
|
+
});
|
|
46710
47282
|
await createAgentServer({
|
|
46711
47283
|
session: createServerSessionAdapter({ session, extensionsResult }),
|
|
47284
|
+
extensionUIBridge,
|
|
46712
47285
|
authToken: request.auth,
|
|
46713
47286
|
port: request.port,
|
|
46714
47287
|
sessionManager: {
|
|
@@ -46728,7 +47301,8 @@ async function main() {
|
|
|
46728
47301
|
agentDir,
|
|
46729
47302
|
version: version2,
|
|
46730
47303
|
sessionDir: request.sessionDir ?? null,
|
|
46731
|
-
thinkingLevel: request.thinking ?? null
|
|
47304
|
+
thinkingLevel: request.thinking ?? null,
|
|
47305
|
+
desktop: request.desktop === true
|
|
46732
47306
|
},
|
|
46733
47307
|
onCreateSession: async () => {
|
|
46734
47308
|
const { session: freshSession, extensionsResult: freshExtensionsResult } = await pi.createAgentSession({
|
|
@@ -46747,6 +47321,9 @@ async function main() {
|
|
|
46747
47321
|
session: freshSession,
|
|
46748
47322
|
modelRegistry
|
|
46749
47323
|
});
|
|
47324
|
+
await freshSession.bindExtensions({
|
|
47325
|
+
uiContext: extensionUIBridge.createUIContext()
|
|
47326
|
+
});
|
|
46750
47327
|
return createServerSessionAdapter({
|
|
46751
47328
|
session: freshSession,
|
|
46752
47329
|
extensionsResult: freshExtensionsResult
|
|
@@ -46779,6 +47356,9 @@ async function main() {
|
|
|
46779
47356
|
session: resumedSession,
|
|
46780
47357
|
modelRegistry
|
|
46781
47358
|
});
|
|
47359
|
+
await resumedSession.bindExtensions({
|
|
47360
|
+
uiContext: extensionUIBridge.createUIContext()
|
|
47361
|
+
});
|
|
46782
47362
|
return createServerSessionAdapter({
|
|
46783
47363
|
session: resumedSession,
|
|
46784
47364
|
extensionsResult: resumedExtensionsResult
|