@jackwener/opencli 0.1.2 → 0.2.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/browser.d.ts CHANGED
@@ -13,6 +13,7 @@ export declare class Page implements IPage {
13
13
  call(method: string, params?: Record<string, any>): Promise<any>;
14
14
  goto(url: string): Promise<void>;
15
15
  evaluate(js: string): Promise<any>;
16
+ private normalizeEval;
16
17
  snapshot(opts?: {
17
18
  interactive?: boolean;
18
19
  compact?: boolean;
package/dist/browser.js CHANGED
@@ -72,7 +72,26 @@ export class Page {
72
72
  await this.call('tools/call', { name: 'browser_navigate', arguments: { url } });
73
73
  }
74
74
  async evaluate(js) {
75
- return this.call('tools/call', { name: 'browser_evaluate', arguments: { function: js } });
75
+ // Normalize IIFE format to function format expected by MCP browser_evaluate
76
+ const normalized = this.normalizeEval(js);
77
+ return this.call('tools/call', { name: 'browser_evaluate', arguments: { function: normalized } });
78
+ }
79
+ normalizeEval(source) {
80
+ const s = source.trim();
81
+ if (!s)
82
+ return '() => undefined';
83
+ // IIFE: (async () => {...})() → wrap as () => (...)
84
+ if (s.startsWith('(') && s.endsWith(')()'))
85
+ return `() => (${s})`;
86
+ // Already a function/arrow
87
+ if (/^(async\s+)?\([^)]*\)\s*=>/.test(s))
88
+ return s;
89
+ if (/^(async\s+)?[A-Za-z_][A-Za-z0-9_]*\s*=>/.test(s))
90
+ return s;
91
+ if (s.startsWith('function ') || s.startsWith('async function '))
92
+ return s;
93
+ // Raw expression → wrap
94
+ return `() => (${s})`;
76
95
  }
77
96
  async snapshot(opts = {}) {
78
97
  const raw = await this.call('tools/call', { name: 'browser_snapshot', arguments: {} });
package/dist/output.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Output formatting: table, JSON, Markdown, CSV.
2
+ * Output formatting: table, JSON, Markdown, CSV, YAML.
3
3
  */
4
4
  export interface RenderOptions {
5
5
  fmt?: string;
package/dist/output.js CHANGED
@@ -1,8 +1,9 @@
1
1
  /**
2
- * Output formatting: table, JSON, Markdown, CSV.
2
+ * Output formatting: table, JSON, Markdown, CSV, YAML.
3
3
  */
4
4
  import chalk from 'chalk';
5
5
  import Table from 'cli-table3';
6
+ import yaml from 'js-yaml';
6
7
  export function render(data, opts = {}) {
7
8
  const fmt = opts.fmt ?? 'table';
8
9
  if (data === null || data === undefined) {
@@ -20,6 +21,10 @@ export function render(data, opts = {}) {
20
21
  case 'csv':
21
22
  renderCsv(data, opts);
22
23
  break;
24
+ case 'yaml':
25
+ case 'yml':
26
+ renderYaml(data);
27
+ break;
23
28
  default:
24
29
  renderTable(data, opts);
25
30
  break;
@@ -32,19 +37,15 @@ function renderTable(data, opts) {
32
37
  return;
33
38
  }
34
39
  const columns = opts.columns ?? Object.keys(rows[0]);
35
- const header = columns.map((c, i) => i === 0 ? '#' : capitalize(c));
40
+ const header = columns.map(c => capitalize(c));
36
41
  const table = new Table({
37
42
  head: header.map(h => chalk.bold(h)),
38
43
  style: { head: [], border: [] },
39
44
  wordWrap: true,
40
45
  wrapOnWordBoundary: true,
41
- colWidths: columns.map((c, i) => {
46
+ colWidths: columns.map((_c, i) => {
42
47
  if (i === 0)
43
- return 4;
44
- if (c === 'url' || c === 'description')
45
- return null;
46
- if (c === 'title' || c === 'name' || c === 'repo')
47
- return null;
48
+ return 6;
48
49
  return null;
49
50
  }).filter(() => true),
50
51
  });
@@ -93,6 +94,9 @@ function renderCsv(data, opts) {
93
94
  }).join(','));
94
95
  }
95
96
  }
97
+ function renderYaml(data) {
98
+ console.log(yaml.dump(data, { sortKeys: false, lineWidth: 120, noRefs: true }));
99
+ }
96
100
  function capitalize(s) {
97
101
  return s.charAt(0).toUpperCase() + s.slice(1);
98
102
  }
@@ -4,9 +4,19 @@
4
4
  export function render(template, ctx) {
5
5
  if (typeof template !== 'string')
6
6
  return template;
7
- const fullMatch = template.match(/^\$\{\{\s*(.*?)\s*\}\}$/);
8
- if (fullMatch)
7
+ // Full expression: entire string is a single ${{ ... }}
8
+ // Use [^}] to prevent matching across }} boundaries (e.g. "${{ a }}-${{ b }}")
9
+ const fullMatch = template.match(/^\$\{\{\s*([^}]*(?:\}[^}][^}]*)*)\s*\}\}$/);
10
+ if (fullMatch && !template.includes('}}-') && !template.includes('}}${{'))
9
11
  return evalExpr(fullMatch[1].trim(), ctx);
12
+ // Check if the entire string is a single expression (no other text around it)
13
+ const singleExpr = template.match(/^\$\{\{\s*([\s\S]*?)\s*\}\}$/);
14
+ if (singleExpr) {
15
+ // Verify it's truly a single expression (no other ${{ inside)
16
+ const inner = singleExpr[1];
17
+ if (!inner.includes('${{'))
18
+ return evalExpr(inner.trim(), ctx);
19
+ }
10
20
  return template.replace(/\$\{\{\s*(.*?)\s*\}\}/g, (_m, expr) => String(evalExpr(expr.trim(), ctx)));
11
21
  }
12
22
  export function evalExpr(expr, ctx) {
@@ -14,19 +24,14 @@ export function evalExpr(expr, ctx) {
14
24
  const item = ctx.item ?? {};
15
25
  const data = ctx.data;
16
26
  const index = ctx.index ?? 0;
17
- // Default filter: args.limit | default(20)
18
- if (expr.includes('|') && expr.includes('default(')) {
19
- const [mainExpr, rest] = expr.split('|', 2);
20
- const defaultMatch = rest.match(/default\((.+?)\)/);
21
- const defaultVal = defaultMatch ? defaultMatch[1] : null;
22
- const result = resolvePath(mainExpr.trim(), { args, item, data, index });
23
- if (result === null || result === undefined) {
24
- if (defaultVal !== null) {
25
- const intVal = parseInt(defaultVal, 10);
26
- if (!isNaN(intVal) && String(intVal) === defaultVal.trim())
27
- return intVal;
28
- return defaultVal.replace(/^['"]|['"]$/g, '');
29
- }
27
+ // ── Pipe filters: expr | filter1(arg) | filter2 ──
28
+ // Supports: default(val), join(sep), upper, lower, truncate(n), trim, replace(old,new)
29
+ if (expr.includes('|') && !expr.includes('||')) {
30
+ const segments = expr.split('|').map(s => s.trim());
31
+ const mainExpr = segments[0];
32
+ let result = resolvePath(mainExpr, { args, item, data, index });
33
+ for (let i = 1; i < segments.length; i++) {
34
+ result = applyFilter(segments[i], result);
30
35
  }
31
36
  return result;
32
37
  }
@@ -59,6 +64,58 @@ export function evalExpr(expr, ctx) {
59
64
  }
60
65
  return resolvePath(expr, { args, item, data, index });
61
66
  }
67
+ /**
68
+ * Apply a named filter to a value.
69
+ * Supported filters:
70
+ * default(val), join(sep), upper, lower, truncate(n), trim,
71
+ * replace(old,new), keys, length, first, last
72
+ */
73
+ function applyFilter(filterExpr, value) {
74
+ const match = filterExpr.match(/^(\w+)(?:\((.+)\))?$/);
75
+ if (!match)
76
+ return value;
77
+ const [, name, rawArgs] = match;
78
+ const filterArg = rawArgs?.replace(/^['"]|['"]$/g, '') ?? '';
79
+ switch (name) {
80
+ case 'default': {
81
+ if (value === null || value === undefined || value === '') {
82
+ const intVal = parseInt(filterArg, 10);
83
+ if (!isNaN(intVal) && String(intVal) === filterArg.trim())
84
+ return intVal;
85
+ return filterArg;
86
+ }
87
+ return value;
88
+ }
89
+ case 'join':
90
+ return Array.isArray(value) ? value.join(filterArg || ', ') : value;
91
+ case 'upper':
92
+ return typeof value === 'string' ? value.toUpperCase() : value;
93
+ case 'lower':
94
+ return typeof value === 'string' ? value.toLowerCase() : value;
95
+ case 'trim':
96
+ return typeof value === 'string' ? value.trim() : value;
97
+ case 'truncate': {
98
+ const n = parseInt(filterArg, 10) || 50;
99
+ return typeof value === 'string' && value.length > n ? value.slice(0, n) + '...' : value;
100
+ }
101
+ case 'replace': {
102
+ if (typeof value !== 'string')
103
+ return value;
104
+ const parts = rawArgs?.split(',').map(s => s.trim().replace(/^['"]|['"]$/g, '')) ?? [];
105
+ return parts.length >= 2 ? value.replaceAll(parts[0], parts[1]) : value;
106
+ }
107
+ case 'keys':
108
+ return value && typeof value === 'object' ? Object.keys(value) : value;
109
+ case 'length':
110
+ return Array.isArray(value) ? value.length : typeof value === 'string' ? value.length : value;
111
+ case 'first':
112
+ return Array.isArray(value) ? value[0] : value;
113
+ case 'last':
114
+ return Array.isArray(value) ? value[value.length - 1] : value;
115
+ default:
116
+ return value;
117
+ }
118
+ }
62
119
  export function resolvePath(pathStr, ctx) {
63
120
  const args = ctx.args ?? {};
64
121
  const item = ctx.item ?? {};
@@ -54,6 +54,24 @@ describe('evalExpr', () => {
54
54
  it('resolves simple path', () => {
55
55
  expect(evalExpr('item.title', { item: { title: 'Test' } })).toBe('Test');
56
56
  });
57
+ it('applies join filter', () => {
58
+ expect(evalExpr('item.tags | join(,)', { item: { tags: ['a', 'b', 'c'] } })).toBe('a,b,c');
59
+ });
60
+ it('applies upper filter', () => {
61
+ expect(evalExpr('item.name | upper', { item: { name: 'hello' } })).toBe('HELLO');
62
+ });
63
+ it('applies lower filter', () => {
64
+ expect(evalExpr('item.name | lower', { item: { name: 'HELLO' } })).toBe('hello');
65
+ });
66
+ it('applies truncate filter', () => {
67
+ expect(evalExpr('item.text | truncate(5)', { item: { text: 'Hello World!' } })).toBe('Hello...');
68
+ });
69
+ it('chains filters', () => {
70
+ expect(evalExpr('item.name | upper | truncate(3)', { item: { name: 'hello' } })).toBe('HEL...');
71
+ });
72
+ it('applies length filter', () => {
73
+ expect(evalExpr('item.items | length', { item: { items: [1, 2, 3] } })).toBe(3);
74
+ });
57
75
  });
58
76
  describe('render', () => {
59
77
  it('renders full expression', () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jackwener/opencli",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
package/src/browser.ts CHANGED
@@ -67,7 +67,22 @@ export class Page implements IPage {
67
67
  }
68
68
 
69
69
  async evaluate(js: string): Promise<any> {
70
- return this.call('tools/call', { name: 'browser_evaluate', arguments: { function: js } });
70
+ // Normalize IIFE format to function format expected by MCP browser_evaluate
71
+ const normalized = this.normalizeEval(js);
72
+ return this.call('tools/call', { name: 'browser_evaluate', arguments: { function: normalized } });
73
+ }
74
+
75
+ private normalizeEval(source: string): string {
76
+ const s = source.trim();
77
+ if (!s) return '() => undefined';
78
+ // IIFE: (async () => {...})() → wrap as () => (...)
79
+ if (s.startsWith('(') && s.endsWith(')()')) return `() => (${s})`;
80
+ // Already a function/arrow
81
+ if (/^(async\s+)?\([^)]*\)\s*=>/.test(s)) return s;
82
+ if (/^(async\s+)?[A-Za-z_][A-Za-z0-9_]*\s*=>/.test(s)) return s;
83
+ if (s.startsWith('function ') || s.startsWith('async function ')) return s;
84
+ // Raw expression → wrap
85
+ return `() => (${s})`;
71
86
  }
72
87
 
73
88
  async snapshot(opts: { interactive?: boolean; compact?: boolean; maxDepth?: number; raw?: boolean } = {}): Promise<any> {
package/src/output.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  /**
2
- * Output formatting: table, JSON, Markdown, CSV.
2
+ * Output formatting: table, JSON, Markdown, CSV, YAML.
3
3
  */
4
4
 
5
5
  import chalk from 'chalk';
6
6
  import Table from 'cli-table3';
7
+ import yaml from 'js-yaml';
7
8
 
8
9
  export interface RenderOptions {
9
10
  fmt?: string;
@@ -23,6 +24,7 @@ export function render(data: any, opts: RenderOptions = {}): void {
23
24
  case 'json': renderJson(data); break;
24
25
  case 'md': case 'markdown': renderMarkdown(data, opts); break;
25
26
  case 'csv': renderCsv(data, opts); break;
27
+ case 'yaml': case 'yml': renderYaml(data); break;
26
28
  default: renderTable(data, opts); break;
27
29
  }
28
30
  }
@@ -32,16 +34,14 @@ function renderTable(data: any, opts: RenderOptions): void {
32
34
  if (!rows.length) { console.log(chalk.dim('(no data)')); return; }
33
35
  const columns = opts.columns ?? Object.keys(rows[0]);
34
36
 
35
- const header = columns.map((c, i) => i === 0 ? '#' : capitalize(c));
37
+ const header = columns.map(c => capitalize(c));
36
38
  const table = new Table({
37
39
  head: header.map(h => chalk.bold(h)),
38
40
  style: { head: [], border: [] },
39
41
  wordWrap: true,
40
42
  wrapOnWordBoundary: true,
41
- colWidths: columns.map((c, i) => {
42
- if (i === 0) return 4;
43
- if (c === 'url' || c === 'description') return null as any;
44
- if (c === 'title' || c === 'name' || c === 'repo') return null as any;
43
+ colWidths: columns.map((_c, i) => {
44
+ if (i === 0) return 6;
45
45
  return null as any;
46
46
  }).filter(() => true),
47
47
  });
@@ -91,6 +91,10 @@ function renderCsv(data: any, opts: RenderOptions): void {
91
91
  }
92
92
  }
93
93
 
94
+ function renderYaml(data: any): void {
95
+ console.log(yaml.dump(data, { sortKeys: false, lineWidth: 120, noRefs: true }));
96
+ }
97
+
94
98
  function capitalize(s: string): string {
95
99
  return s.charAt(0).toUpperCase() + s.slice(1);
96
100
  }
@@ -57,6 +57,24 @@ describe('evalExpr', () => {
57
57
  it('resolves simple path', () => {
58
58
  expect(evalExpr('item.title', { item: { title: 'Test' } })).toBe('Test');
59
59
  });
60
+ it('applies join filter', () => {
61
+ expect(evalExpr('item.tags | join(,)', { item: { tags: ['a', 'b', 'c'] } })).toBe('a,b,c');
62
+ });
63
+ it('applies upper filter', () => {
64
+ expect(evalExpr('item.name | upper', { item: { name: 'hello' } })).toBe('HELLO');
65
+ });
66
+ it('applies lower filter', () => {
67
+ expect(evalExpr('item.name | lower', { item: { name: 'HELLO' } })).toBe('hello');
68
+ });
69
+ it('applies truncate filter', () => {
70
+ expect(evalExpr('item.text | truncate(5)', { item: { text: 'Hello World!' } })).toBe('Hello...');
71
+ });
72
+ it('chains filters', () => {
73
+ expect(evalExpr('item.name | upper | truncate(3)', { item: { name: 'hello' } })).toBe('HEL...');
74
+ });
75
+ it('applies length filter', () => {
76
+ expect(evalExpr('item.items | length', { item: { items: [1, 2, 3] } })).toBe(3);
77
+ });
60
78
  });
61
79
 
62
80
  describe('render', () => {
@@ -11,8 +11,17 @@ export interface RenderContext {
11
11
 
12
12
  export function render(template: any, ctx: RenderContext): any {
13
13
  if (typeof template !== 'string') return template;
14
- const fullMatch = template.match(/^\$\{\{\s*(.*?)\s*\}\}$/);
15
- if (fullMatch) return evalExpr(fullMatch[1].trim(), ctx);
14
+ // Full expression: entire string is a single ${{ ... }}
15
+ // Use [^}] to prevent matching across }} boundaries (e.g. "${{ a }}-${{ b }}")
16
+ const fullMatch = template.match(/^\$\{\{\s*([^}]*(?:\}[^}][^}]*)*)\s*\}\}$/);
17
+ if (fullMatch && !template.includes('}}-') && !template.includes('}}${{')) return evalExpr(fullMatch[1].trim(), ctx);
18
+ // Check if the entire string is a single expression (no other text around it)
19
+ const singleExpr = template.match(/^\$\{\{\s*([\s\S]*?)\s*\}\}$/);
20
+ if (singleExpr) {
21
+ // Verify it's truly a single expression (no other ${{ inside)
22
+ const inner = singleExpr[1];
23
+ if (!inner.includes('${{')) return evalExpr(inner.trim(), ctx);
24
+ }
16
25
  return template.replace(/\$\{\{\s*(.*?)\s*\}\}/g, (_m, expr) => String(evalExpr(expr.trim(), ctx)));
17
26
  }
18
27
 
@@ -22,18 +31,14 @@ export function evalExpr(expr: string, ctx: RenderContext): any {
22
31
  const data = ctx.data;
23
32
  const index = ctx.index ?? 0;
24
33
 
25
- // Default filter: args.limit | default(20)
26
- if (expr.includes('|') && expr.includes('default(')) {
27
- const [mainExpr, rest] = expr.split('|', 2);
28
- const defaultMatch = rest.match(/default\((.+?)\)/);
29
- const defaultVal = defaultMatch ? defaultMatch[1] : null;
30
- const result = resolvePath(mainExpr.trim(), { args, item, data, index });
31
- if (result === null || result === undefined) {
32
- if (defaultVal !== null) {
33
- const intVal = parseInt(defaultVal!, 10);
34
- if (!isNaN(intVal) && String(intVal) === defaultVal!.trim()) return intVal;
35
- return defaultVal!.replace(/^['"]|['"]$/g, '');
36
- }
34
+ // ── Pipe filters: expr | filter1(arg) | filter2 ──
35
+ // Supports: default(val), join(sep), upper, lower, truncate(n), trim, replace(old,new)
36
+ if (expr.includes('|') && !expr.includes('||')) {
37
+ const segments = expr.split('|').map(s => s.trim());
38
+ const mainExpr = segments[0];
39
+ let result = resolvePath(mainExpr, { args, item, data, index });
40
+ for (let i = 1; i < segments.length; i++) {
41
+ result = applyFilter(segments[i], result);
37
42
  }
38
43
  return result;
39
44
  }
@@ -66,6 +71,57 @@ export function evalExpr(expr: string, ctx: RenderContext): any {
66
71
  return resolvePath(expr, { args, item, data, index });
67
72
  }
68
73
 
74
+ /**
75
+ * Apply a named filter to a value.
76
+ * Supported filters:
77
+ * default(val), join(sep), upper, lower, truncate(n), trim,
78
+ * replace(old,new), keys, length, first, last
79
+ */
80
+ function applyFilter(filterExpr: string, value: any): any {
81
+ const match = filterExpr.match(/^(\w+)(?:\((.+)\))?$/);
82
+ if (!match) return value;
83
+ const [, name, rawArgs] = match;
84
+ const filterArg = rawArgs?.replace(/^['"]|['"]$/g, '') ?? '';
85
+
86
+ switch (name) {
87
+ case 'default': {
88
+ if (value === null || value === undefined || value === '') {
89
+ const intVal = parseInt(filterArg, 10);
90
+ if (!isNaN(intVal) && String(intVal) === filterArg.trim()) return intVal;
91
+ return filterArg;
92
+ }
93
+ return value;
94
+ }
95
+ case 'join':
96
+ return Array.isArray(value) ? value.join(filterArg || ', ') : value;
97
+ case 'upper':
98
+ return typeof value === 'string' ? value.toUpperCase() : value;
99
+ case 'lower':
100
+ return typeof value === 'string' ? value.toLowerCase() : value;
101
+ case 'trim':
102
+ return typeof value === 'string' ? value.trim() : value;
103
+ case 'truncate': {
104
+ const n = parseInt(filterArg, 10) || 50;
105
+ return typeof value === 'string' && value.length > n ? value.slice(0, n) + '...' : value;
106
+ }
107
+ case 'replace': {
108
+ if (typeof value !== 'string') return value;
109
+ const parts = rawArgs?.split(',').map(s => s.trim().replace(/^['"]|['"]$/g, '')) ?? [];
110
+ return parts.length >= 2 ? value.replaceAll(parts[0], parts[1]) : value;
111
+ }
112
+ case 'keys':
113
+ return value && typeof value === 'object' ? Object.keys(value) : value;
114
+ case 'length':
115
+ return Array.isArray(value) ? value.length : typeof value === 'string' ? value.length : value;
116
+ case 'first':
117
+ return Array.isArray(value) ? value[0] : value;
118
+ case 'last':
119
+ return Array.isArray(value) ? value[value.length - 1] : value;
120
+ default:
121
+ return value;
122
+ }
123
+ }
124
+
69
125
  export function resolvePath(pathStr: string, ctx: RenderContext): any {
70
126
  const args = ctx.args ?? {};
71
127
  const item = ctx.item ?? {};