feat: release v0.2.0 with all new adapters and fixes
This commit is contained in:
1 parent
f19842c6b7
commit
1361fe6c19
4 files changed
+114
-21
No files matched your search
+16
-1
@@ -67,7 +67,22 @@ export class Page implements IPage {
|
||||
}
|
||||
|
||||
async evaluate(js: string): Promise<any> {
|
||||
return this.call('tools/call', { name: 'browser_evaluate', arguments: { function: js } });
|
||||
// Normalize IIFE format to function format expected by MCP browser_evaluate
|
||||
const normalized = this.normalizeEval(js);
|
||||
return this.call('tools/call', { name: 'browser_evaluate', arguments: { function: normalized } });
|
||||
}
|
||||
|
||||
private normalizeEval(source: string): string {
|
||||
const s = source.trim();
|
||||
if (!s) return '() => undefined';
|
||||
// IIFE: (async () => {...})() → wrap as () => (...)
|
||||
if (s.startsWith('(') && s.endsWith(')()')) return `() => (${s})`;
|
||||
// Already a function/arrow
|
||||
if (/^(async\s+)?\([^)]*\)\s*=>/.test(s)) return s;
|
||||
if (/^(async\s+)?[A-Za-z_][A-Za-z0-9_]*\s*=>/.test(s)) return s;
|
||||
if (s.startsWith('function ') || s.startsWith('async function ')) return s;
|
||||
// Raw expression → wrap
|
||||
return `() => (${s})`;
|
||||
}
|
||||
|
||||
async snapshot(opts: { interactive?: boolean; compact?: boolean; maxDepth?: number; raw?: boolean } = {}): Promise<any> {
|
||||
|
||||
+10
-6
@@ -1,9 +1,10 @@
|
||||
/**
|
||||
* Output formatting: table, JSON, Markdown, CSV.
|
||||
* Output formatting: table, JSON, Markdown, CSV, YAML.
|
||||
*/
|
||||
|
||||
import chalk from 'chalk';
|
||||
import Table from 'cli-table3';
|
||||
import yaml from 'js-yaml';
|
||||
|
||||
export interface RenderOptions {
|
||||
fmt?: string;
|
||||
@@ -23,6 +24,7 @@ export function render(data: any, opts: RenderOptions = {}): void {
|
||||
case 'json': renderJson(data); break;
|
||||
case 'md': case 'markdown': renderMarkdown(data, opts); break;
|
||||
case 'csv': renderCsv(data, opts); break;
|
||||
case 'yaml': case 'yml': renderYaml(data); break;
|
||||
default: renderTable(data, opts); break;
|
||||
}
|
||||
}
|
||||
@@ -32,16 +34,14 @@ function renderTable(data: any, opts: RenderOptions): void {
|
||||
if (!rows.length) { console.log(chalk.dim('(no data)')); return; }
|
||||
const columns = opts.columns ?? Object.keys(rows[0]);
|
||||
|
||||
const header = columns.map((c, i) => i === 0 ? '#' : capitalize(c));
|
||||
const header = columns.map(c => capitalize(c));
|
||||
const table = new Table({
|
||||
head: header.map(h => chalk.bold(h)),
|
||||
style: { head: [], border: [] },
|
||||
wordWrap: true,
|
||||
wrapOnWordBoundary: true,
|
||||
colWidths: columns.map((c, i) => {
|
||||
if (i === 0) return 4;
|
||||
if (c === 'url' || c === 'description') return null as any;
|
||||
if (c === 'title' || c === 'name' || c === 'repo') return null as any;
|
||||
colWidths: columns.map((_c, i) => {
|
||||
if (i === 0) return 6;
|
||||
return null as any;
|
||||
}).filter(() => true),
|
||||
});
|
||||
@@ -91,6 +91,10 @@ function renderCsv(data: any, opts: RenderOptions): void {
|
||||
}
|
||||
}
|
||||
|
||||
function renderYaml(data: any): void {
|
||||
console.log(yaml.dump(data, { sortKeys: false, lineWidth: 120, noRefs: true }));
|
||||
}
|
||||
|
||||
function capitalize(s: string): string {
|
||||
return s.charAt(0).toUpperCase() + s.slice(1);
|
||||
}
|
||||
@@ -57,6 +57,24 @@ describe('evalExpr', () => {
|
||||
it('resolves simple path', () => {
|
||||
expect(evalExpr('item.title', { item: { title: 'Test' } })).toBe('Test');
|
||||
});
|
||||
it('applies join filter', () => {
|
||||
expect(evalExpr('item.tags | join(,)', { item: { tags: ['a', 'b', 'c'] } })).toBe('a,b,c');
|
||||
});
|
||||
it('applies upper filter', () => {
|
||||
expect(evalExpr('item.name | upper', { item: { name: 'hello' } })).toBe('HELLO');
|
||||
});
|
||||
it('applies lower filter', () => {
|
||||
expect(evalExpr('item.name | lower', { item: { name: 'HELLO' } })).toBe('hello');
|
||||
});
|
||||
it('applies truncate filter', () => {
|
||||
expect(evalExpr('item.text | truncate(5)', { item: { text: 'Hello World!' } })).toBe('Hello...');
|
||||
});
|
||||
it('chains filters', () => {
|
||||
expect(evalExpr('item.name | upper | truncate(3)', { item: { name: 'hello' } })).toBe('HEL...');
|
||||
});
|
||||
it('applies length filter', () => {
|
||||
expect(evalExpr('item.items | length', { item: { items: [1, 2, 3] } })).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('render', () => {
|
||||
|
||||
+70
-14
@@ -11,8 +11,17 @@ export interface RenderContext {
|
||||
|
||||
export function render(template: any, ctx: RenderContext): any {
|
||||
if (typeof template !== 'string') return template;
|
||||
const fullMatch = template.match(/^\$\{\{\s*(.*?)\s*\}\}$/);
|
||||
if (fullMatch) return evalExpr(fullMatch[1].trim(), ctx);
|
||||
// Full expression: entire string is a single ${{ ... }}
|
||||
// Use [^}] to prevent matching across }} boundaries (e.g. "${{ a }}-${{ b }}")
|
||||
const fullMatch = template.match(/^\$\{\{\s*([^}]*(?:\}[^}][^}]*)*)\s*\}\}$/);
|
||||
if (fullMatch && !template.includes('}}-') && !template.includes('}}${{')) return evalExpr(fullMatch[1].trim(), ctx);
|
||||
// Check if the entire string is a single expression (no other text around it)
|
||||
const singleExpr = template.match(/^\$\{\{\s*([\s\S]*?)\s*\}\}$/);
|
||||
if (singleExpr) {
|
||||
// Verify it's truly a single expression (no other ${{ inside)
|
||||
const inner = singleExpr[1];
|
||||
if (!inner.includes('${{')) return evalExpr(inner.trim(), ctx);
|
||||
}
|
||||
return template.replace(/\$\{\{\s*(.*?)\s*\}\}/g, (_m, expr) => String(evalExpr(expr.trim(), ctx)));
|
||||
}
|
||||
|
||||
@@ -22,18 +31,14 @@ export function evalExpr(expr: string, ctx: RenderContext): any {
|
||||
const data = ctx.data;
|
||||
const index = ctx.index ?? 0;
|
||||
|
||||
// Default filter: args.limit | default(20)
|
||||
if (expr.includes('|') && expr.includes('default(')) {
|
||||
const [mainExpr, rest] = expr.split('|', 2);
|
||||
const defaultMatch = rest.match(/default\((.+?)\)/);
|
||||
const defaultVal = defaultMatch ? defaultMatch[1] : null;
|
||||
const result = resolvePath(mainExpr.trim(), { args, item, data, index });
|
||||
if (result === null || result === undefined) {
|
||||
if (defaultVal !== null) {
|
||||
const intVal = parseInt(defaultVal!, 10);
|
||||
if (!isNaN(intVal) && String(intVal) === defaultVal!.trim()) return intVal;
|
||||
return defaultVal!.replace(/^['"]|['"]$/g, '');
|
||||
}
|
||||
// ── Pipe filters: expr | filter1(arg) | filter2 ──
|
||||
// Supports: default(val), join(sep), upper, lower, truncate(n), trim, replace(old,new)
|
||||
if (expr.includes('|') && !expr.includes('||')) {
|
||||
const segments = expr.split('|').map(s => s.trim());
|
||||
const mainExpr = segments[0];
|
||||
let result = resolvePath(mainExpr, { args, item, data, index });
|
||||
for (let i = 1; i < segments.length; i++) {
|
||||
result = applyFilter(segments[i], result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -66,6 +71,57 @@ export function evalExpr(expr: string, ctx: RenderContext): any {
|
||||
return resolvePath(expr, { args, item, data, index });
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a named filter to a value.
|
||||
* Supported filters:
|
||||
* default(val), join(sep), upper, lower, truncate(n), trim,
|
||||
* replace(old,new), keys, length, first, last
|
||||
*/
|
||||
function applyFilter(filterExpr: string, value: any): any {
|
||||
const match = filterExpr.match(/^(\w+)(?:\((.+)\))?$/);
|
||||
if (!match) return value;
|
||||
const [, name, rawArgs] = match;
|
||||
const filterArg = rawArgs?.replace(/^['"]|['"]$/g, '') ?? '';
|
||||
|
||||
switch (name) {
|
||||
case 'default': {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
const intVal = parseInt(filterArg, 10);
|
||||
if (!isNaN(intVal) && String(intVal) === filterArg.trim()) return intVal;
|
||||
return filterArg;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
case 'join':
|
||||
return Array.isArray(value) ? value.join(filterArg || ', ') : value;
|
||||
case 'upper':
|
||||
return typeof value === 'string' ? value.toUpperCase() : value;
|
||||
case 'lower':
|
||||
return typeof value === 'string' ? value.toLowerCase() : value;
|
||||
case 'trim':
|
||||
return typeof value === 'string' ? value.trim() : value;
|
||||
case 'truncate': {
|
||||
const n = parseInt(filterArg, 10) || 50;
|
||||
return typeof value === 'string' && value.length > n ? value.slice(0, n) + '...' : value;
|
||||
}
|
||||
case 'replace': {
|
||||
if (typeof value !== 'string') return value;
|
||||
const parts = rawArgs?.split(',').map(s => s.trim().replace(/^['"]|['"]$/g, '')) ?? [];
|
||||
return parts.length >= 2 ? value.replaceAll(parts[0], parts[1]) : value;
|
||||
}
|
||||
case 'keys':
|
||||
return value && typeof value === 'object' ? Object.keys(value) : value;
|
||||
case 'length':
|
||||
return Array.isArray(value) ? value.length : typeof value === 'string' ? value.length : value;
|
||||
case 'first':
|
||||
return Array.isArray(value) ? value[0] : value;
|
||||
case 'last':
|
||||
return Array.isArray(value) ? value[value.length - 1] : value;
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
export function resolvePath(pathStr: string, ctx: RenderContext): any {
|
||||
const args = ctx.args ?? {};
|
||||
const item = ctx.item ?? {};
|
||||
|
||||
Reference in new issue
Block a user