feat(n8n): expand browser-cli node operations
- Add UI resources and mappings for groups, windows, sessions, storage, performance, extension, and more tab/DOM/page actions. - Remove the synthetic Gateway Health operation so exposed operations match real browser-cli commands. - Document the expanded command/policy matrix and cover the new request mappings with tests. - Cap the node SVG icon at 60x60, bump the n8n package to 0.3.0, and advertise client protocol version 0.16.0.
This commit is contained in:
@@ -7,6 +7,10 @@
|
||||
* (see `serveClient.ts`). Every operation maps to one raw extension command;
|
||||
* what the server returns is the *raw* command result (no SDK-side rendering),
|
||||
* still subject to the server's --allow-* policy noted per operation.
|
||||
*
|
||||
* Command names and argument shapes mirror the Python SDK (browser_cli/sdk/*)
|
||||
* and the server-side policy in browser_cli/command_security.py. Gating per
|
||||
* operation is documented in BrowserCli.node.ts next to each operation.
|
||||
*/
|
||||
|
||||
export type CommandParams = Record<string, unknown>;
|
||||
@@ -51,6 +55,16 @@ export function buildCommand(
|
||||
// --- Tabs -------------------------------------------------------------
|
||||
case 'tab:list':
|
||||
return { command: 'tabs.list', args: {} };
|
||||
case 'tab:query':
|
||||
return { command: 'tabs.query', args: { search: str(params, 'search') } };
|
||||
case 'tab:get':
|
||||
return { command: 'tabs.status', args: compact({ tabId: tabIdArg(params.tabId) }) };
|
||||
case 'tab:count':
|
||||
return { command: 'tabs.count', args: compact({ pattern: str(params, 'pattern') }) };
|
||||
case 'tab:filter':
|
||||
return { command: 'tabs.filter', args: { pattern: str(params, 'pattern') } };
|
||||
case 'tab:activeInWindow':
|
||||
return { command: 'tabs.active_in_window', args: { windowId: numArg(params.windowId) } };
|
||||
case 'tab:open': {
|
||||
const focus = Boolean(params.focus);
|
||||
return { command: 'navigate.open', args: compact({ url: str(params, 'url'), focus, background: !focus }) };
|
||||
@@ -63,6 +77,50 @@ export function buildCommand(
|
||||
}
|
||||
case 'tab:getHtml':
|
||||
return { command: 'tabs.html', args: compact({ tabId: tabIdArg(params.tabId) }) };
|
||||
case 'tab:activate':
|
||||
return { command: 'tabs.active', args: { tabId: numArg(params.tabId) } };
|
||||
case 'tab:move':
|
||||
return {
|
||||
command: 'tabs.move',
|
||||
args: compact({
|
||||
tabId: numArg(params.tabId),
|
||||
windowId: tabIdArg(params.windowId),
|
||||
index: indexArg(params.index),
|
||||
}),
|
||||
};
|
||||
case 'tab:reload':
|
||||
return { command: 'navigate.reload', args: compact({ tabId: tabIdArg(params.tabId) }) };
|
||||
case 'tab:hardReload':
|
||||
return { command: 'navigate.hard_reload', args: compact({ tabId: tabIdArg(params.tabId) }) };
|
||||
case 'tab:back':
|
||||
return { command: 'navigate.back', args: compact({ tabId: tabIdArg(params.tabId) }) };
|
||||
case 'tab:forward':
|
||||
return { command: 'navigate.forward', args: compact({ tabId: tabIdArg(params.tabId) }) };
|
||||
case 'tab:navigateTo':
|
||||
return { command: 'navigate.to', args: { tabId: numArg(params.tabId), url: str(params, 'url') } };
|
||||
case 'tab:mute':
|
||||
return { command: 'tabs.mute', args: compact({ tabId: tabIdArg(params.tabId) }) };
|
||||
case 'tab:unmute':
|
||||
return { command: 'tabs.unmute', args: compact({ tabId: tabIdArg(params.tabId) }) };
|
||||
case 'tab:pin':
|
||||
return { command: 'tabs.pin', args: compact({ tabId: tabIdArg(params.tabId) }) };
|
||||
case 'tab:unpin':
|
||||
return { command: 'tabs.unpin', args: compact({ tabId: tabIdArg(params.tabId) }) };
|
||||
case 'tab:dedupe':
|
||||
return { command: 'tabs.dedupe', args: { gentleMode: str(params, 'gentleMode') || 'auto' } };
|
||||
case 'tab:sort':
|
||||
return { command: 'tabs.sort', args: { by: str(params, 'by') || 'domain', gentleMode: str(params, 'gentleMode') || 'auto' } };
|
||||
case 'tab:mergeWindows':
|
||||
return { command: 'tabs.merge_windows', args: { gentleMode: str(params, 'gentleMode') || 'auto' } };
|
||||
case 'tab:screenshot':
|
||||
return {
|
||||
command: 'tabs.screenshot',
|
||||
args: compact({
|
||||
tabId: tabIdArg(params.tabId),
|
||||
format: str(params, 'format') || 'png',
|
||||
quality: indexArg(params.quality),
|
||||
}),
|
||||
};
|
||||
|
||||
// --- Page / extraction ------------------------------------------------
|
||||
case 'page:info':
|
||||
@@ -77,6 +135,8 @@ export function buildCommand(
|
||||
return { command: 'extract.html', args: compact({ selector: str(params, 'selector') }) };
|
||||
case 'page:extractMarkdown':
|
||||
return { command: 'extract.markdown', args: compact({ selector: str(params, 'selector') }) };
|
||||
case 'page:extractJson':
|
||||
return { command: 'extract.json', args: { selector: str(params, 'selector') } };
|
||||
|
||||
// --- DOM --------------------------------------------------------------
|
||||
case 'dom:query':
|
||||
@@ -85,17 +145,118 @@ export function buildCommand(
|
||||
return { command: 'dom.click', args: { selector: str(params, 'selector') } };
|
||||
case 'dom:type':
|
||||
return { command: 'dom.type', args: { selector: str(params, 'selector'), text: str(params, 'text') } };
|
||||
case 'dom:attr':
|
||||
return { command: 'dom.attr', args: { selector: str(params, 'selector'), attr: str(params, 'attr') } };
|
||||
case 'dom:text':
|
||||
return { command: 'dom.text', args: { selector: str(params, 'selector') } };
|
||||
case 'dom:exists':
|
||||
return { command: 'dom.exists', args: { selector: str(params, 'selector') } };
|
||||
case 'dom:scroll':
|
||||
return {
|
||||
command: 'dom.scroll',
|
||||
args: compact({ selector: str(params, 'selector'), x: indexArg(params.x), y: indexArg(params.y) }),
|
||||
};
|
||||
case 'dom:select':
|
||||
return { command: 'dom.select', args: { selector: str(params, 'selector'), value: str(params, 'value') } };
|
||||
case 'dom:hover':
|
||||
return { command: 'dom.hover', args: { selector: str(params, 'selector') } };
|
||||
case 'dom:check':
|
||||
return { command: 'dom.check', args: { selector: str(params, 'selector') } };
|
||||
case 'dom:uncheck':
|
||||
return { command: 'dom.uncheck', args: { selector: str(params, 'selector') } };
|
||||
case 'dom:clear':
|
||||
return { command: 'dom.clear', args: { selector: str(params, 'selector') } };
|
||||
case 'dom:focus':
|
||||
return { command: 'dom.focus', args: { selector: str(params, 'selector') } };
|
||||
case 'dom:submit':
|
||||
return { command: 'dom.submit', args: { selector: str(params, 'selector') } };
|
||||
case 'dom:key':
|
||||
return { command: 'dom.key', args: compact({ key: str(params, 'key'), selector: str(params, 'selector') }) };
|
||||
case 'dom:eval':
|
||||
return { command: 'dom.eval', args: compact({ code: str(params, 'code'), tabId: tabIdArg(params.tabId) }) };
|
||||
|
||||
// --- Groups -----------------------------------------------------------
|
||||
case 'group:list':
|
||||
return { command: 'group.list', args: {} };
|
||||
case 'group:query':
|
||||
return { command: 'group.query', args: { search: str(params, 'search') } };
|
||||
case 'group:tabs':
|
||||
return { command: 'group.tabs', args: { groupId: numArg(params.groupId) } };
|
||||
case 'group:count':
|
||||
return { command: 'group.count', args: {} };
|
||||
case 'group:create':
|
||||
return { command: 'group.open', args: { name: str(params, 'name') } };
|
||||
case 'group:addTab':
|
||||
return { command: 'group.add_tab', args: compact({ group: str(params, 'group'), url: str(params, 'url') }) };
|
||||
case 'group:move': {
|
||||
const direction = str(params, 'direction');
|
||||
return {
|
||||
command: 'group.move',
|
||||
args: { group: str(params, 'group'), forward: direction === 'forward', backward: direction === 'backward' },
|
||||
};
|
||||
}
|
||||
case 'group:close':
|
||||
return { command: 'group.close', args: { groupId: numArg(params.groupId), gentleMode: str(params, 'gentleMode') || 'auto' } };
|
||||
|
||||
// --- Windows ----------------------------------------------------------
|
||||
case 'window:list':
|
||||
return { command: 'windows.list', args: {} };
|
||||
case 'window:open':
|
||||
return { command: 'windows.open', args: compact({ url: str(params, 'url') }) };
|
||||
case 'window:close':
|
||||
return { command: 'windows.close', args: { windowId: numArg(params.windowId) } };
|
||||
case 'window:rename':
|
||||
return { command: 'windows.rename', args: { windowId: numArg(params.windowId), name: str(params, 'name') } };
|
||||
|
||||
// --- Sessions ---------------------------------------------------------
|
||||
case 'session:list':
|
||||
return { command: 'session.list', args: {} };
|
||||
case 'session:save':
|
||||
return { command: 'session.save', args: { name: str(params, 'name') } };
|
||||
case 'session:load':
|
||||
return { command: 'session.load', args: { name: str(params, 'name') } };
|
||||
case 'session:remove':
|
||||
return { command: 'session.remove', args: { name: str(params, 'name') } };
|
||||
case 'session:export':
|
||||
return { command: 'session.export', args: compact({ name: str(params, 'name') }) };
|
||||
case 'session:diff':
|
||||
return { command: 'session.diff', args: { nameA: str(params, 'nameA'), nameB: str(params, 'nameB') } };
|
||||
case 'session:autoSave':
|
||||
return { command: 'session.auto_save', args: { enabled: Boolean(params.enabled) } };
|
||||
|
||||
// --- Storage ----------------------------------------------------------
|
||||
case 'storage:get':
|
||||
return {
|
||||
command: 'storage.get',
|
||||
args: compact({ key: str(params, 'key'), type: str(params, 'storeType') || 'local', tabId: tabIdArg(params.tabId) }),
|
||||
};
|
||||
case 'storage:set':
|
||||
return {
|
||||
command: 'storage.set',
|
||||
args: compact({
|
||||
key: str(params, 'key'),
|
||||
value: str(params, 'value'),
|
||||
type: str(params, 'storeType') || 'local',
|
||||
tabId: tabIdArg(params.tabId),
|
||||
}),
|
||||
};
|
||||
|
||||
// --- Performance ------------------------------------------------------
|
||||
case 'perf:status':
|
||||
return { command: 'perf.status', args: {} };
|
||||
|
||||
// --- Extension --------------------------------------------------------
|
||||
case 'extension:info':
|
||||
return { command: 'extension.info', args: {} };
|
||||
case 'extension:capabilities':
|
||||
return { command: 'extension.capabilities', args: {} };
|
||||
case 'extension:reload':
|
||||
return { command: 'extension.reload', args: {} };
|
||||
|
||||
// --- Clients ----------------------------------------------------------
|
||||
case 'client:list':
|
||||
return { command: 'clients.list', args: {} };
|
||||
|
||||
// --- Gateway: serve has no health route, so ping with a safe command --
|
||||
case 'gateway:health':
|
||||
return { command: 'tabs.list', args: {} };
|
||||
|
||||
default:
|
||||
throw new Error(`Unsupported operation "${operation}" for resource "${resource}"`);
|
||||
}
|
||||
@@ -108,6 +269,19 @@ function tabIdArg(value: unknown): number | undefined {
|
||||
return Number.isFinite(n) && n > 0 ? n : undefined;
|
||||
}
|
||||
|
||||
/** A required numeric arg; non-finite values fall through as 0. */
|
||||
function numArg(value: unknown): number {
|
||||
const n = Number(value);
|
||||
return Number.isFinite(n) ? n : 0;
|
||||
}
|
||||
|
||||
/** An optional numeric arg that keeps 0 (a meaningful index/coordinate). */
|
||||
function indexArg(value: unknown): number | undefined {
|
||||
if (value === undefined || value === null || value === '') return undefined;
|
||||
const n = Number(value);
|
||||
return Number.isFinite(n) ? n : undefined;
|
||||
}
|
||||
|
||||
/** Accept an array, a JSON array string, or a comma/space separated list. */
|
||||
export function parseTabIds(value: unknown): number[] {
|
||||
if (Array.isArray(value)) return value.map(Number).filter(Number.isFinite);
|
||||
|
||||
Reference in New Issue
Block a user