/ autoresearch / save-tasks.json
save-tasks.json
1 [ 2 { 3 "name": "httpbin-get", 4 "site": "test-httpbin", 5 "command": "get", 6 "adapter": "import { cli, Strategy } from '@jackwener/opencli/registry';\n\ncli({\n site: 'test-httpbin',\n name: 'get',\n description: 'httpbin echo test',\n domain: 'httpbin.org',\n strategy: Strategy.PUBLIC,\n browser: false,\n args: [],\n columns: ['origin', 'url'],\n func: async () => {\n const res = await fetch('https://httpbin.org/get');\n const d = await res.json();\n return [{ origin: d.origin, url: d.url }];\n },\n});\n", 7 "judge": { 8 "type": "arrayMinLength", 9 "minLength": 1 10 }, 11 "note": "Simplest possible: httpbin echo, single row" 12 }, 13 { 14 "name": "jsonplaceholder-posts", 15 "site": "test-jsonplaceholder", 16 "command": "posts", 17 "adapter": "import { cli, Strategy } from '@jackwener/opencli/registry';\n\ncli({\n site: 'test-jsonplaceholder',\n name: 'posts',\n description: 'JSONPlaceholder posts',\n domain: 'jsonplaceholder.typicode.com',\n strategy: Strategy.PUBLIC,\n browser: false,\n args: [\n { name: 'limit', type: 'int', default: 5, help: 'Number of posts' },\n ],\n columns: ['id', 'title'],\n func: async (_page, kwargs) => {\n const limit = kwargs.limit ?? 5;\n const res = await fetch('https://jsonplaceholder.typicode.com/posts');\n const posts = await res.json();\n return posts.slice(0, limit).map((p: any) => ({ id: p.id, title: p.title }));\n },\n});\n", 18 "judge": { 19 "type": "arrayMinLength", 20 "minLength": 3 21 } 22 }, 23 { 24 "name": "jsonplaceholder-users", 25 "site": "test-jsonplaceholder", 26 "command": "users", 27 "adapter": "import { cli, Strategy } from '@jackwener/opencli/registry';\n\ncli({\n site: 'test-jsonplaceholder',\n name: 'users',\n description: 'JSONPlaceholder users',\n domain: 'jsonplaceholder.typicode.com',\n strategy: Strategy.PUBLIC,\n browser: false,\n args: [\n { name: 'limit', type: 'int', default: 5, help: 'Number of users' },\n ],\n columns: ['id', 'name', 'email'],\n func: async (_page, kwargs) => {\n const limit = kwargs.limit ?? 5;\n const res = await fetch('https://jsonplaceholder.typicode.com/users');\n const users = await res.json();\n return users.slice(0, limit).map((u: any) => ({ id: u.id, name: u.name, email: u.email }));\n },\n});\n", 28 "judge": { 29 "type": "arrayMinLength", 30 "minLength": 3 31 } 32 }, 33 { 34 "name": "hn-top", 35 "site": "test-hn", 36 "command": "top", 37 "adapter": "import { cli, Strategy } from '@jackwener/opencli/registry';\n\ncli({\n site: 'test-hn',\n name: 'top',\n description: 'HackerNews top stories',\n domain: 'news.ycombinator.com',\n strategy: Strategy.PUBLIC,\n browser: false,\n args: [\n { name: 'limit', type: 'int', default: 5, help: 'Number of stories' },\n ],\n columns: ['rank', 'title', 'score'],\n func: async (_page, kwargs) => {\n const limit = Math.min(kwargs.limit ?? 5, 10);\n const res = await fetch('https://hacker-news.firebaseio.com/v0/topstories.json');\n const ids = await res.json();\n const items = await Promise.all(ids.slice(0, limit).map(async (id: number) => {\n const r = await fetch(`https://hacker-news.firebaseio.com/v0/item/${id}.json`);\n return r.json();\n }));\n return items.map((item: any, i: number) => ({\n rank: i + 1, title: item.title, score: item.score,\n }));\n },\n});\n", 38 "judge": { 39 "type": "arrayMinLength", 40 "minLength": 3 41 } 42 }, 43 { 44 "name": "hn-ask", 45 "site": "test-hn", 46 "command": "ask", 47 "adapter": "import { cli, Strategy } from '@jackwener/opencli/registry';\n\ncli({\n site: 'test-hn',\n name: 'ask',\n description: 'HackerNews Ask HN stories',\n domain: 'news.ycombinator.com',\n strategy: Strategy.PUBLIC,\n browser: false,\n args: [\n { name: 'limit', type: 'int', default: 5, help: 'Number of stories' },\n ],\n columns: ['rank', 'title', 'score'],\n func: async (_page, kwargs) => {\n const limit = Math.min(kwargs.limit ?? 5, 10);\n const res = await fetch('https://hacker-news.firebaseio.com/v0/askstories.json');\n const ids = await res.json();\n const items = await Promise.all(ids.slice(0, limit).map(async (id: number) => {\n const r = await fetch(`https://hacker-news.firebaseio.com/v0/item/${id}.json`);\n return r.json();\n }));\n return items.map((item: any, i: number) => ({\n rank: i + 1, title: item.title, score: item.score,\n }));\n },\n});\n", 48 "judge": { 49 "type": "arrayMinLength", 50 "minLength": 3 51 } 52 }, 53 { 54 "name": "wiki-summary", 55 "site": "test-wiki", 56 "command": "summary", 57 "adapter": "import { cli, Strategy } from '@jackwener/opencli/registry';\n\ncli({\n site: 'test-wiki',\n name: 'summary',\n description: 'Wikipedia article summary',\n domain: 'en.wikipedia.org',\n strategy: Strategy.PUBLIC,\n browser: false,\n args: [\n { name: 'title', type: 'string', default: 'JavaScript', positional: true, help: 'Article title' },\n ],\n columns: ['title', 'extract'],\n func: async (_page, kwargs) => {\n const title = encodeURIComponent(kwargs.title);\n const res = await fetch(`https://en.wikipedia.org/api/rest_v1/page/summary/${title}`);\n const d = await res.json();\n return [{ title: d.title, extract: d.extract?.slice(0, 200) }];\n },\n});\n", 58 "judge": { 59 "type": "contains", 60 "value": "programming language" 61 } 62 }, 63 { 64 "name": "lobsters-hot", 65 "site": "test-lobsters", 66 "command": "hot", 67 "adapter": "import { cli, Strategy } from '@jackwener/opencli/registry';\n\ncli({\n site: 'test-lobsters',\n name: 'hot',\n description: 'Lobsters hottest stories',\n domain: 'lobste.rs',\n strategy: Strategy.PUBLIC,\n browser: false,\n args: [\n { name: 'limit', type: 'int', default: 5, help: 'Number of stories' },\n ],\n columns: ['title', 'score', 'url'],\n func: async (_page, kwargs) => {\n const limit = kwargs.limit ?? 5;\n const res = await fetch('https://lobste.rs/hottest.json');\n const stories = await res.json();\n return stories.slice(0, limit).map((s: any) => ({\n title: s.title, score: s.score, url: s.short_id_url,\n }));\n },\n});\n", 68 "judge": { 69 "type": "arrayMinLength", 70 "minLength": 3 71 } 72 }, 73 { 74 "name": "devto-top", 75 "site": "test-devto", 76 "command": "top", 77 "adapter": "import { cli, Strategy } from '@jackwener/opencli/registry';\n\ncli({\n site: 'test-devto',\n name: 'top',\n description: 'DEV.to top articles',\n domain: 'dev.to',\n strategy: Strategy.PUBLIC,\n browser: false,\n args: [\n { name: 'limit', type: 'int', default: 5, help: 'Number of articles' },\n ],\n columns: ['title', 'user', 'reactions'],\n func: async (_page, kwargs) => {\n const limit = kwargs.limit ?? 5;\n const res = await fetch('https://dev.to/api/articles?per_page=' + limit);\n const articles = await res.json();\n return articles.map((a: any) => ({\n title: a.title, user: a.user?.username, reactions: a.positive_reactions_count,\n }));\n },\n});\n", 78 "judge": { 79 "type": "arrayMinLength", 80 "minLength": 3 81 } 82 }, 83 { 84 "name": "zhihu-hot-with-top-answer", 85 "site": "test-zhihu", 86 "command": "hot-detail", 87 "adapterFile": "save-adapters/zhihu-hot-detail.ts", 88 "judge": { 89 "type": "arrayMinLength", 90 "minLength": 3 91 }, 92 "note": "6-step chain: navigate → fetch hot list API → parse big-int IDs → loop N items → fetch answer API per question → strip HTML → merge" 93 }, 94 { 95 "name": "zhihu-search-with-question-stats", 96 "site": "test-zhihu", 97 "command": "search-detail", 98 "adapterFile": "save-adapters/zhihu-search-detail.ts", 99 "judge": { 100 "type": "arrayMinLength", 101 "minLength": 3 102 }, 103 "note": "7-step chain: navigate → search API → filter by type → extract question IDs → fetch question detail per result → merge stats → format" 104 }, 105 { 106 "name": "xhs-search-scroll-extract", 107 "site": "test-xhs", 108 "command": "search-full", 109 "adapterFile": "save-adapters/xhs-search-full.ts", 110 "judge": { 111 "type": "arrayMinLength", 112 "minLength": 3 113 }, 114 "note": "6-step chain: navigate → MutationObserver wait → scroll 3x → DOM extract with URL dedup → slice + format" 115 }, 116 { 117 "name": "xhs-note-with-comments", 118 "site": "test-xhs", 119 "command": "note-comments", 120 "adapterFile": "save-adapters/xhs-note-comments.ts", 121 "judge": { 122 "type": "arrayMinLength", 123 "minLength": 1 124 }, 125 "note": "7-step chain: navigate → wait → extract note meta → scroll container 3x → extract comments DOM → merge note+comments → unified output" 126 }, 127 { 128 "name": "zhihu-question-with-related", 129 "site": "test-zhihu", 130 "command": "question-full", 131 "adapterFile": "save-adapters/zhihu-question-full.ts", 132 "judge": { 133 "type": "arrayMinLength", 134 "minLength": 2 135 }, 136 "note": "8-step chain: navigate → wait → fetch question detail → fetch answers → strip HTML → fetch related questions → merge 3 layers → format" 137 }, 138 { 139 "name": "xhs-explore-scroll-dedupe", 140 "site": "test-xhs", 141 "command": "explore-deep", 142 "adapterFile": "save-adapters/xhs-explore-deep.ts", 143 "judge": { 144 "type": "arrayMinLength", 145 "minLength": 3 146 }, 147 "note": "8-step chain: navigate → MutationObserver wait → adaptive scroll → DOM extract with dedup → parse likes → sort desc → slice → format" 148 }, 149 { 150 "name": "hn-new", 151 "site": "test-hn", 152 "command": "new", 153 "adapter": "import { cli, Strategy } from '@jackwener/opencli/registry';\n\ncli({\n site: 'test-hn',\n name: 'new',\n description: 'HackerNews newest stories',\n domain: 'news.ycombinator.com',\n strategy: Strategy.PUBLIC,\n browser: false,\n args: [\n { name: 'limit', type: 'int', default: 5, help: 'Number of stories' },\n ],\n columns: ['rank', 'title', 'score'],\n func: async (_page, kwargs) => {\n const limit = Math.min(kwargs.limit ?? 5, 10);\n const res = await fetch('https://hacker-news.firebaseio.com/v0/newstories.json');\n const ids = await res.json();\n const items = await Promise.all(ids.slice(0, limit).map(async (id: number) => {\n const r = await fetch(`https://hacker-news.firebaseio.com/v0/item/${id}.json`);\n return r.json();\n }));\n return items.map((item: any, i: number) => ({\n rank: i + 1, title: item.title, score: item.score ?? 0,\n }));\n },\n});\n", 154 "judge": { 155 "type": "arrayMinLength", 156 "minLength": 3 157 }, 158 "note": "PUBLIC strategy: HackerNews new stories using same Firebase API as hn-top/hn-ask" 159 }, 160 { 161 "name": "jsonplaceholder-todos", 162 "site": "test-jsonplaceholder", 163 "command": "todos", 164 "adapter": "import { cli, Strategy } from '@jackwener/opencli/registry';\n\ncli({\n site: 'test-jsonplaceholder',\n name: 'todos',\n description: 'JSONPlaceholder todos',\n domain: 'jsonplaceholder.typicode.com',\n strategy: Strategy.PUBLIC,\n browser: false,\n args: [\n { name: 'limit', type: 'int', default: 5, help: 'Number of todos' },\n ],\n columns: ['id', 'title', 'completed'],\n func: async (_page, kwargs) => {\n const limit = kwargs.limit ?? 5;\n const res = await fetch('https://jsonplaceholder.typicode.com/todos');\n const todos = await res.json();\n return todos.slice(0, limit).map((t: any) => ({ id: t.id, title: t.title, completed: t.completed }));\n },\n});\n", 165 "judge": { 166 "type": "arrayMinLength", 167 "minLength": 3 168 }, 169 "note": "PUBLIC strategy: JSONPlaceholder todos — same base domain as posts/users, different endpoint" 170 }, 171 { 172 "name": "hn-show", 173 "site": "test-hn", 174 "command": "show", 175 "adapter": "import { cli, Strategy } from '@jackwener/opencli/registry';\n\ncli({\n site: 'test-hn',\n name: 'show',\n description: 'HackerNews Show HN stories',\n domain: 'news.ycombinator.com',\n strategy: Strategy.PUBLIC,\n browser: false,\n args: [\n { name: 'limit', type: 'int', default: 5, help: 'Number of stories' },\n ],\n columns: ['rank', 'title', 'score'],\n func: async (_page, kwargs) => {\n const limit = Math.min(kwargs.limit ?? 5, 10);\n const res = await fetch('https://hacker-news.firebaseio.com/v0/showstories.json');\n const ids = await res.json();\n const items = await Promise.all(ids.slice(0, limit).map(async (id: number) => {\n const r = await fetch(`https://hacker-news.firebaseio.com/v0/item/${id}.json`);\n return r.json();\n }));\n return items.map((item: any, i: number) => ({\n rank: i + 1, title: item.title, score: item.score ?? 0,\n }));\n },\n});\n", 176 "judge": { 177 "type": "arrayMinLength", 178 "minLength": 3 179 }, 180 "note": "PUBLIC strategy: HackerNews show stories using same Firebase API as hn-top/hn-ask/hn-new" 181 }, 182 { 183 "name": "jsonplaceholder-comments", 184 "site": "test-jsonplaceholder", 185 "command": "comments", 186 "adapter": "import { cli, Strategy } from '@jackwener/opencli/registry';\n\ncli({\n site: 'test-jsonplaceholder',\n name: 'comments',\n description: 'JSONPlaceholder comments',\n domain: 'jsonplaceholder.typicode.com',\n strategy: Strategy.PUBLIC,\n browser: false,\n args: [\n { name: 'limit', type: 'int', default: 5, help: 'Number of comments' },\n ],\n columns: ['id', 'name', 'email'],\n func: async (_page, kwargs) => {\n const limit = kwargs.limit ?? 5;\n const res = await fetch('https://jsonplaceholder.typicode.com/comments');\n const comments = await res.json();\n return comments.slice(0, limit).map((c: any) => ({ id: c.id, name: c.name, email: c.email }));\n },\n});\n", 187 "judge": { 188 "type": "arrayMinLength", 189 "minLength": 3 190 }, 191 "note": "PUBLIC strategy: JSONPlaceholder comments — same base domain as posts/users/todos, different endpoint" 192 }, 193 { 194 "name": "jsonplaceholder-albums", 195 "site": "test-jsonplaceholder", 196 "command": "albums", 197 "adapter": "import { cli, Strategy } from '@jackwener/opencli/registry';\n\ncli({\n site: 'test-jsonplaceholder',\n name: 'albums',\n description: 'JSONPlaceholder albums',\n domain: 'jsonplaceholder.typicode.com',\n strategy: Strategy.PUBLIC,\n browser: false,\n args: [\n { name: 'limit', type: 'int', default: 5, help: 'Number of albums' },\n ],\n columns: ['id', 'userId', 'title'],\n func: async (_page, kwargs) => {\n const limit = kwargs.limit ?? 5;\n const res = await fetch('https://jsonplaceholder.typicode.com/albums');\n const albums = await res.json();\n return albums.slice(0, limit).map((a: any) => ({ id: a.id, userId: a.userId, title: a.title }));\n },\n});\n", 198 "judge": { 199 "type": "arrayMinLength", 200 "minLength": 3 201 }, 202 "note": "PUBLIC strategy: JSONPlaceholder albums — same base domain as posts/users/todos/comments, different endpoint" 203 }, 204 { 205 "name": "jsonplaceholder-photos", 206 "site": "test-jsonplaceholder", 207 "command": "photos", 208 "adapter": "import { cli, Strategy } from '@jackwener/opencli/registry';\n\ncli({\n site: 'test-jsonplaceholder',\n name: 'photos',\n description: 'JSONPlaceholder photos',\n domain: 'jsonplaceholder.typicode.com',\n strategy: Strategy.PUBLIC,\n browser: false,\n args: [\n { name: 'limit', type: 'int', default: 5, help: 'Number of photos' },\n ],\n columns: ['id', 'albumId', 'title'],\n func: async (_page, kwargs) => {\n const limit = kwargs.limit ?? 5;\n const res = await fetch('https://jsonplaceholder.typicode.com/photos');\n const photos = await res.json();\n return photos.slice(0, limit).map((p: any) => ({ id: p.id, albumId: p.albumId, title: p.title }));\n },\n});\n", 209 "judge": { 210 "type": "arrayMinLength", 211 "minLength": 3 212 }, 213 "note": "PUBLIC strategy: JSONPlaceholder photos — same base domain as posts/users/todos/comments/albums, different endpoint" 214 }, 215 { 216 "name": "hn-best", 217 "site": "test-hn", 218 "command": "best", 219 "adapter": "import { cli, Strategy } from '@jackwener/opencli/registry';\n\ncli({\n site: 'test-hn',\n name: 'best',\n description: 'HackerNews best stories',\n domain: 'news.ycombinator.com',\n strategy: Strategy.PUBLIC,\n browser: false,\n args: [\n { name: 'limit', type: 'int', default: 5, help: 'Number of stories' },\n ],\n columns: ['rank', 'title', 'score'],\n func: async (_page, kwargs) => {\n const limit = Math.min(kwargs.limit ?? 5, 10);\n const res = await fetch('https://hacker-news.firebaseio.com/v0/beststories.json');\n const ids = await res.json();\n const items = await Promise.all(ids.slice(0, limit).map(async (id: number) => {\n const r = await fetch(`https://hacker-news.firebaseio.com/v0/item/${id}.json`);\n return r.json();\n }));\n return items.map((item: any, i: number) => ({\n rank: i + 1, title: item.title, score: item.score ?? 0,\n }));\n },\n});\n", 220 "judge": { 221 "type": "arrayMinLength", 222 "minLength": 3 223 }, 224 "note": "PUBLIC strategy: HackerNews best stories using same Firebase API as hn-top/hn-ask/hn-new/hn-show" 225 }, 226 { 227 "name": "hn-jobs", 228 "site": "test-hn", 229 "command": "jobs", 230 "adapter": "import { cli, Strategy } from '@jackwener/opencli/registry';\n\ncli({\n site: 'test-hn',\n name: 'jobs',\n description: 'HackerNews job stories',\n domain: 'news.ycombinator.com',\n strategy: Strategy.PUBLIC,\n browser: false,\n args: [\n { name: 'limit', type: 'int', default: 5, help: 'Number of jobs' },\n ],\n columns: ['rank', 'title', 'url'],\n func: async (_page, kwargs) => {\n const limit = Math.min(kwargs.limit ?? 5, 10);\n const res = await fetch('https://hacker-news.firebaseio.com/v0/jobstories.json');\n const ids = await res.json();\n const items = await Promise.all(ids.slice(0, limit).map(async (id: number) => {\n const r = await fetch(`https://hacker-news.firebaseio.com/v0/item/${id}.json`);\n return r.json();\n }));\n return items.map((item: any, i: number) => ({\n rank: i + 1, title: item.title, url: item.url ?? '',\n }));\n },\n});\n", 231 "judge": { 232 "type": "arrayMinLength", 233 "minLength": 3 234 }, 235 "note": "PUBLIC strategy: HackerNews job listings using same Firebase API as other HN adapters" 236 }, 237 { 238 "name": "restcountries-list", 239 "site": "test-restcountries", 240 "command": "list", 241 "adapter": "import { cli, Strategy } from '@jackwener/opencli/registry';\n\ncli({\n site: 'test-restcountries',\n name: 'list',\n description: 'REST Countries list',\n domain: 'restcountries.com',\n strategy: Strategy.PUBLIC,\n browser: false,\n args: [\n { name: 'limit', type: 'int', default: 5, help: 'Number of countries' },\n ],\n columns: ['name', 'capital', 'region'],\n func: async (_page, kwargs) => {\n const limit = kwargs.limit ?? 5;\n const res = await fetch('https://restcountries.com/v3.1/all?fields=name,capital,region');\n const countries = await res.json();\n return countries.slice(0, limit).map((c: any) => ({\n name: c.name?.common ?? '',\n capital: c.capital?.[0] ?? '',\n region: c.region ?? '',\n }));\n },\n});\n", 242 "judge": { 243 "type": "arrayMinLength", 244 "minLength": 3 245 }, 246 "note": "PUBLIC strategy: REST Countries API — stable, no-auth, returns 250 countries with name/capital/region" 247 }, 248 { 249 "name": "nager-holidays", 250 "site": "test-nager", 251 "command": "holidays", 252 "adapter": "import { cli, Strategy } from '@jackwener/opencli/registry';\n\ncli({\n site: 'test-nager',\n name: 'holidays',\n description: 'US public holidays for current year',\n domain: 'date.nager.at',\n strategy: Strategy.PUBLIC,\n browser: false,\n args: [\n { name: 'limit', type: 'int', default: 5, help: 'Number of holidays' },\n ],\n columns: ['date', 'name', 'type'],\n func: async (_page, kwargs) => {\n const limit = kwargs.limit ?? 5;\n const year = new Date().getFullYear();\n const res = await fetch(`https://date.nager.at/api/v3/PublicHolidays/${year}/US`);\n const holidays = await res.json();\n return holidays.slice(0, limit).map((h: any) => ({\n date: h.date,\n name: h.name,\n type: (h.types || []).join(','),\n }));\n },\n});\n", 253 "judge": { 254 "type": "arrayMinLength", 255 "minLength": 3 256 }, 257 "note": "PUBLIC strategy: Nager public holidays API — stable, no-auth, returns US federal holidays by year" 258 }, 259 { 260 "name": "catfact-list", 261 "site": "test-catfact", 262 "command": "facts", 263 "adapter": "import { cli, Strategy } from '@jackwener/opencli/registry';\n\ncli({\n site: 'test-catfact',\n name: 'facts',\n description: 'Random cat facts',\n domain: 'catfact.ninja',\n strategy: Strategy.PUBLIC,\n browser: false,\n args: [\n { name: 'limit', type: 'int', default: 5, help: 'Number of facts' },\n ],\n columns: ['fact', 'length'],\n func: async (_page, kwargs) => {\n const limit = kwargs.limit ?? 5;\n const res = await fetch(`https://catfact.ninja/facts?limit=${limit}`);\n const d = await res.json();\n return d.data.map((item: any) => ({\n fact: item.fact.slice(0, 100),\n length: item.length,\n }));\n },\n});\n", 264 "judge": { 265 "type": "arrayMinLength", 266 "minLength": 3 267 }, 268 "note": "PUBLIC strategy: catfact.ninja facts API — stable, no-auth, returns random cat facts" 269 }, 270 { 271 "name": "opentdb-trivia", 272 "site": "test-opentdb", 273 "command": "easy", 274 "adapter": "import { cli, Strategy } from '@jackwener/opencli/registry';\n\ncli({\n site: 'test-opentdb',\n name: 'easy',\n description: 'Easy trivia questions from Open Trivia DB',\n domain: 'opentdb.com',\n strategy: Strategy.PUBLIC,\n browser: false,\n args: [\n { name: 'limit', type: 'int', default: 5, help: 'Number of questions' },\n ],\n columns: ['category', 'question', 'answer'],\n func: async (_page, kwargs) => {\n const limit = Math.min(kwargs.limit ?? 5, 10);\n const res = await fetch(`https://opentdb.com/api.php?amount=${limit}&difficulty=easy&type=multiple`);\n const d = await res.json();\n return d.results.map((q: any) => ({\n category: q.category,\n question: q.question.replace(/"/g, '\"').replace(/'/g, \"'\").slice(0, 80),\n answer: q.correct_answer,\n }));\n },\n});\n", 275 "judge": { 276 "type": "arrayMinLength", 277 "minLength": 3 278 }, 279 "note": "PUBLIC strategy: Open Trivia DB API — stable, no-auth, returns trivia questions with correct answers" 280 } 281 ]