@aexol/spectral 0.8.7 → 0.8.10
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/auth-helper.d.ts +49 -0
- package/dist/auth-helper.d.ts.map +1 -0
- package/dist/auth-helper.js +191 -0
- package/dist/commands/login-oauth.d.ts +2 -9
- package/dist/commands/login-oauth.d.ts.map +1 -1
- package/dist/commands/login-oauth.js +10 -105
- package/dist/commands/serve.d.ts.map +1 -1
- package/dist/commands/serve.js +68 -1
- package/dist/designer/systems.d.ts.map +1 -1
- package/dist/designer/systems.js +5 -0
- package/dist/extensions/aexol-mcp.d.ts.map +1 -1
- package/dist/extensions/aexol-mcp.js +33 -2
- package/dist/extensions/browser/browser-service.d.ts +104 -0
- package/dist/extensions/browser/browser-service.d.ts.map +1 -0
- package/dist/extensions/browser/browser-service.js +333 -0
- package/dist/extensions/browser/config.d.ts +24 -0
- package/dist/extensions/browser/config.d.ts.map +1 -0
- package/dist/extensions/browser/config.js +17 -0
- package/dist/extensions/browser/index.d.ts +35 -0
- package/dist/extensions/browser/index.d.ts.map +1 -0
- package/dist/extensions/browser/index.js +126 -0
- package/dist/extensions/browser/stream/signaling-client.d.ts +28 -0
- package/dist/extensions/browser/stream/signaling-client.d.ts.map +1 -0
- package/dist/extensions/browser/stream/signaling-client.js +69 -0
- package/dist/extensions/browser/stream/stream-injector.d.ts +30 -0
- package/dist/extensions/browser/stream/stream-injector.d.ts.map +1 -0
- package/dist/extensions/browser/stream/stream-injector.js +161 -0
- package/dist/extensions/browser/stream/stream-tool.d.ts +10 -0
- package/dist/extensions/browser/stream/stream-tool.d.ts.map +1 -0
- package/dist/extensions/browser/stream/stream-tool.js +182 -0
- package/dist/extensions/browser/tools/console.d.ts +7 -0
- package/dist/extensions/browser/tools/console.d.ts.map +1 -0
- package/dist/extensions/browser/tools/console.js +54 -0
- package/dist/extensions/browser/tools/core.d.ts +7 -0
- package/dist/extensions/browser/tools/core.d.ts.map +1 -0
- package/dist/extensions/browser/tools/core.js +115 -0
- package/dist/extensions/browser/tools/dialog.d.ts +11 -0
- package/dist/extensions/browser/tools/dialog.d.ts.map +1 -0
- package/dist/extensions/browser/tools/dialog.js +54 -0
- package/dist/extensions/browser/tools/drop.d.ts +7 -0
- package/dist/extensions/browser/tools/drop.d.ts.map +1 -0
- package/dist/extensions/browser/tools/drop.js +93 -0
- package/dist/extensions/browser/tools/evaluate.d.ts +7 -0
- package/dist/extensions/browser/tools/evaluate.d.ts.map +1 -0
- package/dist/extensions/browser/tools/evaluate.js +105 -0
- package/dist/extensions/browser/tools/input.d.ts +7 -0
- package/dist/extensions/browser/tools/input.d.ts.map +1 -0
- package/dist/extensions/browser/tools/input.js +290 -0
- package/dist/extensions/browser/tools/network.d.ts +7 -0
- package/dist/extensions/browser/tools/network.d.ts.map +1 -0
- package/dist/extensions/browser/tools/network.js +116 -0
- package/dist/extensions/browser/tools/screenshot.d.ts +7 -0
- package/dist/extensions/browser/tools/screenshot.d.ts.map +1 -0
- package/dist/extensions/browser/tools/screenshot.js +153 -0
- package/dist/extensions/browser/tools/tabs.d.ts +7 -0
- package/dist/extensions/browser/tools/tabs.d.ts.map +1 -0
- package/dist/extensions/browser/tools/tabs.js +72 -0
- package/dist/extensions/browser/tools/wait.d.ts +7 -0
- package/dist/extensions/browser/tools/wait.d.ts.map +1 -0
- package/dist/extensions/browser/tools/wait.js +64 -0
- package/dist/extensions/kanban-bridge.d.ts.map +1 -1
- package/dist/extensions/kanban-bridge.js +70 -3
- package/dist/mcp/ui-server.d.ts.map +1 -1
- package/dist/mcp/ui-server.js +23 -10
- package/dist/sdk/coding-agent/core/extensions/native-extensions.d.ts +60 -0
- package/dist/sdk/coding-agent/core/extensions/native-extensions.d.ts.map +1 -0
- package/dist/sdk/coding-agent/core/extensions/native-extensions.js +108 -0
- package/dist/sdk/coding-agent/core/resource-loader.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/resource-loader.js +23 -1
- package/dist/sdk/coding-agent/core/settings-manager.d.ts +5 -0
- package/dist/sdk/coding-agent/core/settings-manager.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/settings-manager.js +12 -0
- package/dist/server/sqlite-adapter.d.ts +41 -0
- package/dist/server/sqlite-adapter.d.ts.map +1 -0
- package/dist/server/sqlite-adapter.js +128 -0
- package/dist/server/storage.d.ts.map +1 -1
- package/dist/server/storage.js +2 -2
- package/package.json +5 -2
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Network tools: browser_network_requests, browser_network_request.
|
|
3
|
+
*/
|
|
4
|
+
export function registerNetworkTools(ext, bs) {
|
|
5
|
+
const networkRequestsTool = {
|
|
6
|
+
name: "browser_network_requests",
|
|
7
|
+
label: "Browser Network Requests",
|
|
8
|
+
description: "Returns a numbered list of network requests since loading the page. " +
|
|
9
|
+
"Use browser_network_request with the index number to get full details of a specific request.",
|
|
10
|
+
promptSnippet: "`browser_network_requests` — list all network requests",
|
|
11
|
+
parameters: {
|
|
12
|
+
type: "object",
|
|
13
|
+
properties: {
|
|
14
|
+
includeStatic: {
|
|
15
|
+
type: "boolean",
|
|
16
|
+
description: "Include static assets (images, fonts, CSS). Default: false (shows only XHR/fetch).",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
async execute(_toolCallId, params) {
|
|
21
|
+
const p = params;
|
|
22
|
+
try {
|
|
23
|
+
const allRequests = bs.getNetworkRequests();
|
|
24
|
+
const filtered = p.includeStatic
|
|
25
|
+
? allRequests
|
|
26
|
+
: allRequests.filter((r) => {
|
|
27
|
+
const url = r.url.toLowerCase();
|
|
28
|
+
// Filter out common static assets
|
|
29
|
+
if (url.match(/\.(png|jpg|jpeg|gif|svg|ico|webp|woff|woff2|ttf|eot|css|js)(\?|$)/))
|
|
30
|
+
return false;
|
|
31
|
+
return true;
|
|
32
|
+
});
|
|
33
|
+
if (filtered.length === 0) {
|
|
34
|
+
return { content: [{ type: "text", text: "No network requests recorded yet. Navigate to a page first." }], details: {} };
|
|
35
|
+
}
|
|
36
|
+
const lines = filtered.map((r) => `[${r.index}] ${r.method} ${r.status} ${r.url.slice(0, 120)}`);
|
|
37
|
+
return { content: [{ type: "text", text: lines.join("\n") }], details: {} };
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
41
|
+
return { content: [{ type: "text", text: `Network requests failed: ${msg}` }], details: { isError: true } };
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
const networkRequestTool = {
|
|
46
|
+
name: "browser_network_request",
|
|
47
|
+
label: "Browser Network Request Detail",
|
|
48
|
+
description: "Returns full details (headers and body) of a single network request. " +
|
|
49
|
+
"Use the index number from browser_network_requests.",
|
|
50
|
+
promptSnippet: "`browser_network_request { index }` — get full request details",
|
|
51
|
+
parameters: {
|
|
52
|
+
type: "object",
|
|
53
|
+
properties: {
|
|
54
|
+
index: {
|
|
55
|
+
type: "number",
|
|
56
|
+
description: "The request index from browser_network_requests output.",
|
|
57
|
+
},
|
|
58
|
+
part: {
|
|
59
|
+
type: "string",
|
|
60
|
+
enum: ["request", "response", "both"],
|
|
61
|
+
description: "Which part to return. Default: both.",
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
required: ["index"],
|
|
65
|
+
},
|
|
66
|
+
async execute(_toolCallId, params) {
|
|
67
|
+
const p = params;
|
|
68
|
+
try {
|
|
69
|
+
const req = bs.getNetworkRequest(p.index);
|
|
70
|
+
if (!req) {
|
|
71
|
+
return { content: [{ type: "text", text: `No request found at index ${p.index}.` }], details: {} };
|
|
72
|
+
}
|
|
73
|
+
const lines = [`[${req.index}] ${req.method} ${req.url}`, `Status: ${req.status} ${req.statusText}`];
|
|
74
|
+
if (!p.part || p.part === "request" || p.part === "both") {
|
|
75
|
+
lines.push("\n--- Request Headers ---");
|
|
76
|
+
for (const [k, v] of Object.entries(req.headers)) {
|
|
77
|
+
lines.push(`${k}: ${v}`);
|
|
78
|
+
}
|
|
79
|
+
if (req.postData) {
|
|
80
|
+
lines.push(`\n--- Request Body ---\n${req.postData.slice(0, 10_000)}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (!p.part || p.part === "response" || p.part === "both") {
|
|
84
|
+
lines.push("\n--- Response Headers ---");
|
|
85
|
+
for (const [k, v] of Object.entries(req.responseHeaders)) {
|
|
86
|
+
lines.push(`${k}: ${v}`);
|
|
87
|
+
}
|
|
88
|
+
if (req.body) {
|
|
89
|
+
lines.push(`\n--- Response Body ---\n${req.body.slice(0, 20_000)}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const text = lines.join("\n");
|
|
93
|
+
return {
|
|
94
|
+
content: [{
|
|
95
|
+
type: "text",
|
|
96
|
+
text: text.length > 50_000 ? text.slice(0, 50_000) + "\n\n[truncated]" : text,
|
|
97
|
+
}],
|
|
98
|
+
details: {},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
103
|
+
return { content: [{ type: "text", text: `Network request detail failed: ${msg}` }], details: { isError: true } };
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
for (const tool of [networkRequestsTool, networkRequestTool]) {
|
|
108
|
+
try {
|
|
109
|
+
ext.registerTool(tool);
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
113
|
+
process.stderr.write(`[browser] Failed to register ${tool.name}: ${msg}\n`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Screenshot & snapshot tools: browser_take_screenshot, browser_snapshot.
|
|
3
|
+
*/
|
|
4
|
+
import type { ExtensionAPI } from "../../../sdk/coding-agent/index.js";
|
|
5
|
+
import { BrowserService } from "../browser-service.js";
|
|
6
|
+
export declare function registerScreenshotTools(ext: ExtensionAPI, bs: BrowserService): void;
|
|
7
|
+
//# sourceMappingURL=screenshot.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"screenshot.d.ts","sourceRoot":"","sources":["../../../../src/extensions/browser/tools/screenshot.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,KAAK,EAAE,YAAY,EAAkB,MAAM,oCAAoC,CAAC;AACvF,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAEvD,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,YAAY,EAAE,EAAE,EAAE,cAAc,GAAG,IAAI,CAgKnF"}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Screenshot & snapshot tools: browser_take_screenshot, browser_snapshot.
|
|
3
|
+
*/
|
|
4
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
export function registerScreenshotTools(ext, bs) {
|
|
7
|
+
const screenshotTool = {
|
|
8
|
+
name: "browser_take_screenshot",
|
|
9
|
+
label: "Browser Take Screenshot",
|
|
10
|
+
description: "Take a screenshot of the current page or viewport. " +
|
|
11
|
+
"Saves to the output directory. Use browser_snapshot for text-based page analysis.",
|
|
12
|
+
promptSnippet: "`browser_take_screenshot` — capture a screenshot of the page",
|
|
13
|
+
parameters: {
|
|
14
|
+
type: "object",
|
|
15
|
+
properties: {
|
|
16
|
+
filename: {
|
|
17
|
+
type: "string",
|
|
18
|
+
description: "Filename for the screenshot (default: screenshot-<timestamp>.png).",
|
|
19
|
+
},
|
|
20
|
+
fullPage: {
|
|
21
|
+
type: "boolean",
|
|
22
|
+
description: "Capture the full scrollable page, not just the viewport. Default: false.",
|
|
23
|
+
},
|
|
24
|
+
type: {
|
|
25
|
+
type: "string",
|
|
26
|
+
enum: ["png", "jpeg"],
|
|
27
|
+
description: "Image format. Default: png.",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
async execute(_toolCallId, params) {
|
|
32
|
+
const p = params;
|
|
33
|
+
try {
|
|
34
|
+
const page = bs.getActivePage();
|
|
35
|
+
const config = bs.getConfig();
|
|
36
|
+
mkdirSync(config.outputDir, { recursive: true });
|
|
37
|
+
const filename = p.filename ?? `screenshot-${Date.now()}.${p.type ?? "png"}`;
|
|
38
|
+
const filepath = join(config.outputDir, filename);
|
|
39
|
+
await page.screenshot({
|
|
40
|
+
path: filepath,
|
|
41
|
+
fullPage: p.fullPage ?? false,
|
|
42
|
+
type: p.type ?? "png",
|
|
43
|
+
});
|
|
44
|
+
return {
|
|
45
|
+
content: [{
|
|
46
|
+
type: "text",
|
|
47
|
+
text: `✓ Screenshot saved: ${filepath}\nURL: ${page.url()}`,
|
|
48
|
+
}],
|
|
49
|
+
details: {},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
54
|
+
return { content: [{ type: "text", text: `Screenshot failed: ${msg}` }], details: { isError: true } };
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
const snapshotTool = {
|
|
59
|
+
name: "browser_snapshot",
|
|
60
|
+
label: "Browser Snapshot",
|
|
61
|
+
description: "Capture the accessibility snapshot of the current page. " +
|
|
62
|
+
"This provides a structured text representation of the page content, " +
|
|
63
|
+
"which is much more useful for understanding page structure and interacting " +
|
|
64
|
+
"with elements than a screenshot. Each interactive element gets a unique ref.",
|
|
65
|
+
promptSnippet: "`browser_snapshot` — get text-based page structure",
|
|
66
|
+
parameters: {
|
|
67
|
+
type: "object",
|
|
68
|
+
properties: {
|
|
69
|
+
filename: {
|
|
70
|
+
type: "string",
|
|
71
|
+
description: "Filename to save the snapshot to (default: snapshot-<timestamp>.txt).",
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
async execute(_toolCallId, params) {
|
|
76
|
+
const p = params;
|
|
77
|
+
try {
|
|
78
|
+
const page = bs.getActivePage();
|
|
79
|
+
const config = bs.getConfig();
|
|
80
|
+
// Inject ARIA snapshot helper
|
|
81
|
+
const snapshot = await page.evaluate(() => {
|
|
82
|
+
function buildSnapshot(element, depth) {
|
|
83
|
+
if (depth > 20)
|
|
84
|
+
return ""; // Prevent infinite recursion
|
|
85
|
+
let result = "";
|
|
86
|
+
const indent = " ".repeat(depth);
|
|
87
|
+
const tag = element.tagName.toLowerCase();
|
|
88
|
+
const role = element.getAttribute("role");
|
|
89
|
+
const ariaLabel = element.getAttribute("aria-label");
|
|
90
|
+
const id = element.id ? `#${element.id}` : "";
|
|
91
|
+
const className = element.className && typeof element.className === "string"
|
|
92
|
+
? `.${element.className.split(" ").filter(Boolean).join(".")}`
|
|
93
|
+
: "";
|
|
94
|
+
const text = element.childNodes.length === 1 && element.childNodes[0].nodeType === Node.TEXT_NODE
|
|
95
|
+
? ` "${element.textContent?.trim()}"`
|
|
96
|
+
: "";
|
|
97
|
+
// Build element description
|
|
98
|
+
let desc = `${indent}${tag}${id}${className}`;
|
|
99
|
+
if (role)
|
|
100
|
+
desc += ` [role=${role}]`;
|
|
101
|
+
if (ariaLabel)
|
|
102
|
+
desc += ` [aria-label="${ariaLabel}"]`;
|
|
103
|
+
if (text && text.length < 100)
|
|
104
|
+
desc += text;
|
|
105
|
+
// Interactive hints
|
|
106
|
+
const href = element.href;
|
|
107
|
+
const placeholder = element.placeholder;
|
|
108
|
+
const value = element.value;
|
|
109
|
+
if (href)
|
|
110
|
+
desc += ` → ${href}`;
|
|
111
|
+
if (placeholder)
|
|
112
|
+
desc += ` placeholder="${placeholder}"`;
|
|
113
|
+
if (value)
|
|
114
|
+
desc += ` value="${value.slice(0, 50)}"`;
|
|
115
|
+
result += desc + "\n";
|
|
116
|
+
for (const child of element.children) {
|
|
117
|
+
result += buildSnapshot(child, depth + 1);
|
|
118
|
+
}
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
return buildSnapshot(document.body ?? document.documentElement, 0);
|
|
122
|
+
});
|
|
123
|
+
mkdirSync(config.outputDir, { recursive: true });
|
|
124
|
+
const filename = p.filename ?? `snapshot-${Date.now()}.txt`;
|
|
125
|
+
const filepath = join(config.outputDir, filename);
|
|
126
|
+
writeFileSync(filepath, snapshot, "utf-8");
|
|
127
|
+
const truncated = snapshot.length > 50000
|
|
128
|
+
? snapshot.slice(0, 50000) + `\n\n[truncated — full snapshot saved to ${filepath}]`
|
|
129
|
+
: snapshot;
|
|
130
|
+
return {
|
|
131
|
+
content: [{
|
|
132
|
+
type: "text",
|
|
133
|
+
text: `Saved to: ${filepath}\nURL: ${page.url()}\n\n${truncated}`,
|
|
134
|
+
}],
|
|
135
|
+
details: {},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
140
|
+
return { content: [{ type: "text", text: `Snapshot failed: ${msg}` }], details: { isError: true } };
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
for (const tool of [screenshotTool, snapshotTool]) {
|
|
145
|
+
try {
|
|
146
|
+
ext.registerTool(tool);
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
150
|
+
process.stderr.write(`[browser] Failed to register ${tool.name}: ${msg}\n`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tab management tools: list, create, close, select tabs.
|
|
3
|
+
*/
|
|
4
|
+
import type { ExtensionAPI } from "../../../sdk/coding-agent/index.js";
|
|
5
|
+
import { BrowserService } from "../browser-service.js";
|
|
6
|
+
export declare function registerTabsTools(ext: ExtensionAPI, bs: BrowserService): void;
|
|
7
|
+
//# sourceMappingURL=tabs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tabs.d.ts","sourceRoot":"","sources":["../../../../src/extensions/browser/tools/tabs.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAkB,MAAM,oCAAoC,CAAC;AACvF,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAEvD,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,YAAY,EAAE,EAAE,EAAE,cAAc,GAAG,IAAI,CAqE7E"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tab management tools: list, create, close, select tabs.
|
|
3
|
+
*/
|
|
4
|
+
export function registerTabsTools(ext, bs) {
|
|
5
|
+
const tabsTool = {
|
|
6
|
+
name: "browser_tabs",
|
|
7
|
+
label: "Browser Tabs",
|
|
8
|
+
description: "List, create, close, or select browser tabs.",
|
|
9
|
+
promptSnippet: "`browser_tabs { action }` — manage browser tabs (list/create/close/select)",
|
|
10
|
+
parameters: {
|
|
11
|
+
type: "object",
|
|
12
|
+
properties: {
|
|
13
|
+
action: {
|
|
14
|
+
type: "string",
|
|
15
|
+
enum: ["list", "create", "close", "select"],
|
|
16
|
+
description: "Tab action: list all tabs, create a new one, close one, or select (switch to) one.",
|
|
17
|
+
},
|
|
18
|
+
index: {
|
|
19
|
+
type: "number",
|
|
20
|
+
description: "Tab index for close/select actions (0-based).",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
required: ["action"],
|
|
24
|
+
},
|
|
25
|
+
async execute(_toolCallId, params) {
|
|
26
|
+
const p = params;
|
|
27
|
+
try {
|
|
28
|
+
switch (p.action) {
|
|
29
|
+
case "list": {
|
|
30
|
+
const tabs = bs.listTabs();
|
|
31
|
+
if (tabs.length === 0) {
|
|
32
|
+
return { content: [{ type: "text", text: "No tabs open." }], details: {} };
|
|
33
|
+
}
|
|
34
|
+
const lines = tabs.map((t) => `${t.index === bs["activePageIndex"] ? "▶" : " "} [${t.index}] ${t.url}`);
|
|
35
|
+
return { content: [{ type: "text", text: lines.join("\n") }], details: {} };
|
|
36
|
+
}
|
|
37
|
+
case "create": {
|
|
38
|
+
const { index } = await bs.createTab();
|
|
39
|
+
return { content: [{ type: "text", text: `✓ Created new tab [${index}]` }], details: {} };
|
|
40
|
+
}
|
|
41
|
+
case "close": {
|
|
42
|
+
if (p.index === undefined) {
|
|
43
|
+
return { content: [{ type: "text", text: "Error: 'index' is required for close action." }], details: { isError: true } };
|
|
44
|
+
}
|
|
45
|
+
await bs.closeTab(p.index);
|
|
46
|
+
return { content: [{ type: "text", text: `✓ Closed tab [${p.index}]` }], details: {} };
|
|
47
|
+
}
|
|
48
|
+
case "select": {
|
|
49
|
+
if (p.index === undefined) {
|
|
50
|
+
return { content: [{ type: "text", text: "Error: 'index' is required for select action." }], details: { isError: true } };
|
|
51
|
+
}
|
|
52
|
+
const result = await bs.selectTab(p.index);
|
|
53
|
+
return { content: [{ type: "text", text: `✓ Selected tab [${result.index}]` }], details: {} };
|
|
54
|
+
}
|
|
55
|
+
default:
|
|
56
|
+
return { content: [{ type: "text", text: `Unknown action: ${p.action}` }], details: { isError: true } };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
61
|
+
return { content: [{ type: "text", text: `Tabs action failed: ${msg}` }], details: { isError: true } };
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
try {
|
|
66
|
+
ext.registerTool(tabsTool);
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
70
|
+
process.stderr.write(`[browser] Failed to register browser_tabs: ${msg}\n`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wait tool: browser_wait_for.
|
|
3
|
+
*/
|
|
4
|
+
import type { ExtensionAPI } from "../../../sdk/coding-agent/index.js";
|
|
5
|
+
import { BrowserService } from "../browser-service.js";
|
|
6
|
+
export declare function registerWaitTools(ext: ExtensionAPI, bs: BrowserService): void;
|
|
7
|
+
//# sourceMappingURL=wait.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wait.d.ts","sourceRoot":"","sources":["../../../../src/extensions/browser/tools/wait.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAkB,MAAM,oCAAoC,CAAC;AACvF,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAEvD,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,YAAY,EAAE,EAAE,EAAE,cAAc,GAAG,IAAI,CAgE7E"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wait tool: browser_wait_for.
|
|
3
|
+
*/
|
|
4
|
+
export function registerWaitTools(ext, bs) {
|
|
5
|
+
const waitTool = {
|
|
6
|
+
name: "browser_wait_for",
|
|
7
|
+
label: "Browser Wait For",
|
|
8
|
+
description: "Wait for text to appear, text to disappear, or a specified amount of time to pass.",
|
|
9
|
+
promptSnippet: "`browser_wait_for { text, textGone, time }` — wait for a condition",
|
|
10
|
+
parameters: {
|
|
11
|
+
type: "object",
|
|
12
|
+
properties: {
|
|
13
|
+
text: {
|
|
14
|
+
type: "string",
|
|
15
|
+
description: "Wait until this text appears on the page.",
|
|
16
|
+
},
|
|
17
|
+
textGone: {
|
|
18
|
+
type: "string",
|
|
19
|
+
description: "Wait until this text disappears from the page.",
|
|
20
|
+
},
|
|
21
|
+
time: {
|
|
22
|
+
type: "number",
|
|
23
|
+
description: "Wait for this many milliseconds.",
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
async execute(_toolCallId, params) {
|
|
28
|
+
const p = params;
|
|
29
|
+
try {
|
|
30
|
+
const page = bs.getActivePage();
|
|
31
|
+
if (p.text) {
|
|
32
|
+
await page.waitForSelector(`text=${p.text}`, { timeout: 30_000 });
|
|
33
|
+
return { content: [{ type: "text", text: `✓ Text appeared: "${p.text}"` }], details: {} };
|
|
34
|
+
}
|
|
35
|
+
if (p.textGone) {
|
|
36
|
+
await page.waitForSelector(`text=${p.textGone}`, {
|
|
37
|
+
state: "detached",
|
|
38
|
+
timeout: 30_000,
|
|
39
|
+
});
|
|
40
|
+
return { content: [{ type: "text", text: `✓ Text disappeared: "${p.textGone}"` }], details: {} };
|
|
41
|
+
}
|
|
42
|
+
if (p.time) {
|
|
43
|
+
await page.waitForTimeout(p.time);
|
|
44
|
+
return { content: [{ type: "text", text: `✓ Waited ${p.time}ms` }], details: {} };
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
content: [{ type: "text", text: "No wait condition specified. Provide text, textGone, or time." }],
|
|
48
|
+
details: { isError: true },
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
53
|
+
return { content: [{ type: "text", text: `Wait failed: ${msg}` }], details: { isError: true } };
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
try {
|
|
58
|
+
ext.registerTool(waitTool);
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
62
|
+
process.stderr.write(`[browser] Failed to register browser_wait_for: ${msg}\n`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"kanban-bridge.d.ts","sourceRoot":"","sources":["../../src/extensions/kanban-bridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAkB,MAAM,8BAA8B,CAAC;
|
|
1
|
+
{"version":3,"file":"kanban-bridge.d.ts","sourceRoot":"","sources":["../../src/extensions/kanban-bridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAkB,MAAM,8BAA8B,CAAC;AA21BjF,wBAA8B,qBAAqB,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAkCpF"}
|
|
@@ -41,7 +41,7 @@ async function graphqlRequest(opts, query, variables) {
|
|
|
41
41
|
detail = (await res.text()).slice(0, 300);
|
|
42
42
|
}
|
|
43
43
|
catch { /* ignore */ }
|
|
44
|
-
throw new GraphQLError(`HTTP ${res.status} from GraphQL${detail ? `: ${detail}` : ""}
|
|
44
|
+
throw new GraphQLError(`HTTP ${res.status} from GraphQL${detail ? `: ${detail}` : ""}`, res.status);
|
|
45
45
|
}
|
|
46
46
|
const json = (await res.json());
|
|
47
47
|
if (json.errors && json.errors.length > 0) {
|
|
@@ -53,9 +53,11 @@ async function graphqlRequest(opts, query, variables) {
|
|
|
53
53
|
return json.data;
|
|
54
54
|
}
|
|
55
55
|
class GraphQLError extends Error {
|
|
56
|
-
|
|
56
|
+
status;
|
|
57
|
+
constructor(message, status) {
|
|
57
58
|
super(message);
|
|
58
59
|
this.name = "GraphQLError";
|
|
60
|
+
this.status = status;
|
|
59
61
|
}
|
|
60
62
|
}
|
|
61
63
|
// ---------------------------------------------------------------------------
|
|
@@ -165,14 +167,33 @@ async function deleteKanbanTask(opts, taskId) {
|
|
|
165
167
|
import { readConfig } from "../config.js";
|
|
166
168
|
import { readStudioBinding } from "../studio-binding.js";
|
|
167
169
|
import { getConfigFile } from "../config.js";
|
|
170
|
+
import { ensureAuthenticated } from "../auth-helper.js";
|
|
171
|
+
// Track whether we've already attempted reauth in this session to avoid loops
|
|
172
|
+
let _reauthAttempted = false;
|
|
168
173
|
async function resolveKanbanConfig(explicitProjectId) {
|
|
169
174
|
const cfg = await readConfig();
|
|
170
175
|
if (!cfg) {
|
|
176
|
+
if (!_reauthAttempted) {
|
|
177
|
+
_reauthAttempted = true;
|
|
178
|
+
process.stderr.write(`[kanban-bridge] No config found at ${getConfigFile()}. Attempting OAuth login...\n`);
|
|
179
|
+
const result = await ensureAuthenticated();
|
|
180
|
+
if (result.ok && result.config) {
|
|
181
|
+
return resolveKanbanConfig(explicitProjectId); // Retry with fresh config
|
|
182
|
+
}
|
|
183
|
+
}
|
|
171
184
|
process.stderr.write(`[kanban-bridge] No config found at ${getConfigFile()}. Run \`spectral login\`.\n`);
|
|
172
185
|
return null;
|
|
173
186
|
}
|
|
174
187
|
const token = cfg.teamApiKey || cfg.userJwt || "";
|
|
175
188
|
if (!token) {
|
|
189
|
+
if (!_reauthAttempted) {
|
|
190
|
+
_reauthAttempted = true;
|
|
191
|
+
process.stderr.write(`[kanban-bridge] No auth token in config. Attempting OAuth login...\n`);
|
|
192
|
+
const result = await ensureAuthenticated();
|
|
193
|
+
if (result.ok && result.config) {
|
|
194
|
+
return resolveKanbanConfig(explicitProjectId);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
176
197
|
process.stderr.write(`[kanban-bridge] No auth token in config. Run \`spectral login\`.\n`);
|
|
177
198
|
return null;
|
|
178
199
|
}
|
|
@@ -634,6 +655,51 @@ const kanbanDeleteTool = {
|
|
|
634
655
|
},
|
|
635
656
|
};
|
|
636
657
|
// ---------------------------------------------------------------------------
|
|
658
|
+
// Auth retry wrapper — handles 401/403 during tool execution
|
|
659
|
+
// ---------------------------------------------------------------------------
|
|
660
|
+
/**
|
|
661
|
+
* Wrap a tool's execute function so that GraphQL 401/403 errors trigger
|
|
662
|
+
* OAuth re-authentication and a single retry with the fresh token.
|
|
663
|
+
*/
|
|
664
|
+
function withReauthRetry(toolName, execute) {
|
|
665
|
+
return async (toolCallId, params, signal, onUpdate, ctx) => {
|
|
666
|
+
try {
|
|
667
|
+
return await execute(toolCallId, params, signal, onUpdate, ctx);
|
|
668
|
+
}
|
|
669
|
+
catch (err) {
|
|
670
|
+
if (err instanceof GraphQLError && (err.status === 401 || err.status === 403)) {
|
|
671
|
+
if (_reauthAttempted) {
|
|
672
|
+
return {
|
|
673
|
+
content: [{ type: "text", text: `Authentication failed (HTTP ${err.status}). Please run \`spectral login\` to re-authenticate.` }],
|
|
674
|
+
details: { isError: true },
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
_reauthAttempted = true;
|
|
678
|
+
process.stderr.write(`[kanban-bridge] Auth error (${err.status}) on ${toolName}. Attempting OAuth re-login...\n`);
|
|
679
|
+
const result = await ensureAuthenticated();
|
|
680
|
+
if (result.ok && result.config) {
|
|
681
|
+
process.stderr.write(`[kanban-bridge] Re-authenticated — retrying ${toolName}...\n`);
|
|
682
|
+
try {
|
|
683
|
+
return await execute(toolCallId, params, signal, onUpdate, ctx);
|
|
684
|
+
}
|
|
685
|
+
catch (retryErr) {
|
|
686
|
+
const retryMsg = retryErr instanceof Error ? retryErr.message : String(retryErr);
|
|
687
|
+
return {
|
|
688
|
+
content: [{ type: "text", text: `Retry after re-auth also failed: ${retryMsg}` }],
|
|
689
|
+
details: { isError: true },
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
return {
|
|
694
|
+
content: [{ type: "text", text: `Re-authentication failed: ${result.reason ?? "unknown"}. Please run \`spectral login\` manually.` }],
|
|
695
|
+
details: { isError: true },
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
throw err;
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
// ---------------------------------------------------------------------------
|
|
637
703
|
// Extension entry point
|
|
638
704
|
// ---------------------------------------------------------------------------
|
|
639
705
|
export default async function kanbanBridgeExtension(ext) {
|
|
@@ -649,7 +715,8 @@ export default async function kanbanBridgeExtension(ext) {
|
|
|
649
715
|
let registered = 0;
|
|
650
716
|
for (const tool of tools) {
|
|
651
717
|
try {
|
|
652
|
-
|
|
718
|
+
const wrappedTool = { ...tool, execute: withReauthRetry(tool.name, tool.execute) };
|
|
719
|
+
ext.registerTool(wrappedTool);
|
|
653
720
|
registered++;
|
|
654
721
|
}
|
|
655
722
|
catch (err) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ui-server.d.ts","sourceRoot":"","sources":["../../src/mcp/ui-server.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAEV,cAAc,EACf,MAAM,oCAAoC,CAAC;AAC5C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAI3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAML,KAAK,aAAa,EAClB,KAAK,eAAe,EACpB,KAAK,oBAAoB,EAIzB,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACrB,MAAM,YAAY,CAAC;AAOpB,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,QAAQ,EAAE,iBAAiB,CAAC;IAC5B,OAAO,EAAE,gBAAgB,CAAC;IAC1B,cAAc,EAAE,cAAc,CAAC;IAC/B,WAAW,CAAC,EAAE,aAAa,CAAC;IAC5B,oBAAoB,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,CAAC;IAC/C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC9D,eAAe,CAAC,EAAE,CAAC,MAAM,EAAE,oBAAoB,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACzE,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CACvC;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,CAAC,MAAM,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,aAAa,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IACvD,cAAc,EAAE,CAAC,MAAM,EAAE,cAAc,KAAK,IAAI,CAAC;IACjD,eAAe,EAAE,CAAC,MAAM,EAAE,cAAc,KAAK,IAAI,CAAC;IAClD,iBAAiB,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,eAAe,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAC;IAClD,iDAAiD;IACjD,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;IAC5C,gBAAgB,EAAE,MAAM,eAAe,GAAG,SAAS,CAAC;CACrD;AAED,wBAAsB,aAAa,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC,
|
|
1
|
+
{"version":3,"file":"ui-server.d.ts","sourceRoot":"","sources":["../../src/mcp/ui-server.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAEV,cAAc,EACf,MAAM,oCAAoC,CAAC;AAC5C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAI3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAML,KAAK,aAAa,EAClB,KAAK,eAAe,EACpB,KAAK,oBAAoB,EAIzB,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACrB,MAAM,YAAY,CAAC;AAOpB,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,QAAQ,EAAE,iBAAiB,CAAC;IAC5B,OAAO,EAAE,gBAAgB,CAAC;IAC1B,cAAc,EAAE,cAAc,CAAC;IAC/B,WAAW,CAAC,EAAE,aAAa,CAAC;IAC5B,oBAAoB,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,CAAC;IAC/C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC9D,eAAe,CAAC,EAAE,CAAC,MAAM,EAAE,oBAAoB,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACzE,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CACvC;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,CAAC,MAAM,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,aAAa,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IACvD,cAAc,EAAE,CAAC,MAAM,EAAE,cAAc,KAAK,IAAI,CAAC;IACjD,eAAe,EAAE,CAAC,MAAM,EAAE,cAAc,KAAK,IAAI,CAAC;IAClD,iBAAiB,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,eAAe,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAC;IAClD,iDAAiD;IACjD,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;IAC5C,gBAAgB,EAAE,MAAM,eAAe,GAAG,SAAS,CAAC;CACrD;AAED,wBAAsB,aAAa,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC,CAyerF"}
|
package/dist/mcp/ui-server.js
CHANGED
|
@@ -227,17 +227,30 @@ export async function startUiServer(options) {
|
|
|
227
227
|
return;
|
|
228
228
|
}
|
|
229
229
|
if (method === "GET" && url.pathname === "/app-bridge.bundle.js") {
|
|
230
|
-
// Serve the pre-bundled AppBridge module
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
230
|
+
// Serve the pre-bundled AppBridge module.
|
|
231
|
+
// Try import.meta.dirname first (npm install layout), then relative
|
|
232
|
+
// to the executable (Bun standalone binary layout).
|
|
233
|
+
const candidates = [
|
|
234
|
+
path.join(import.meta.dirname, "app-bridge.bundle.js"),
|
|
235
|
+
path.join(path.dirname(process.execPath), "app-bridge.bundle.js"),
|
|
236
|
+
];
|
|
237
|
+
let served = false;
|
|
238
|
+
for (const bundlePath of candidates) {
|
|
239
|
+
try {
|
|
240
|
+
const content = await fs.readFile(bundlePath, "utf-8");
|
|
241
|
+
res.writeHead(200, {
|
|
242
|
+
"Content-Type": "application/javascript",
|
|
243
|
+
"Cache-Control": "public, max-age=31536000",
|
|
244
|
+
});
|
|
245
|
+
res.end(content);
|
|
246
|
+
served = true;
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
// Try next candidate
|
|
251
|
+
}
|
|
239
252
|
}
|
|
240
|
-
|
|
253
|
+
if (!served) {
|
|
241
254
|
sendJson(res, 500, { ok: false, error: "Bundle not found" });
|
|
242
255
|
}
|
|
243
256
|
return;
|