{"id":38112915,"url":"https://github.com/dmmulroy/better-result","last_synced_at":"2026-04-06T02:01:20.613Z","repository":{"id":332502818,"uuid":"1130779351","full_name":"dmmulroy/better-result","owner":"dmmulroy","description":"Lightweight Result type for TypeScript with generator-based composition.","archived":false,"fork":false,"pushed_at":"2026-03-30T13:31:19.000Z","size":147,"stargazers_count":1035,"open_issues_count":12,"forks_count":23,"subscribers_count":2,"default_branch":"main","last_synced_at":"2026-03-30T15:24:46.851Z","etag":null,"topics":["error-handling","result-type","rust","try-catch","typescript"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dmmulroy.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-01-09T02:11:52.000Z","updated_at":"2026-03-30T15:21:37.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/dmmulroy/better-result","commit_stats":null,"previous_names":["dmmulroy/better-result"],"tags_count":11,"template":false,"template_full_name":null,"purl":"pkg:github/dmmulroy/better-result","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmmulroy%2Fbetter-result","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmmulroy%2Fbetter-result/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmmulroy%2Fbetter-result/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmmulroy%2Fbetter-result/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dmmulroy","download_url":"https://codeload.github.com/dmmulroy/better-result/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmmulroy%2Fbetter-result/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31456664,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-05T21:22:52.476Z","status":"online","status_checked_at":"2026-04-06T02:00:07.287Z","response_time":112,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["error-handling","result-type","rust","try-catch","typescript"],"created_at":"2026-01-16T22:03:49.942Z","updated_at":"2026-04-06T02:01:20.593Z","avatar_url":"https://github.com/dmmulroy.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# better-result\n\nLightweight Result type for TypeScript with generator-based composition.\n\n## Install\n\n```sh\nnpm install better-result\n```\n\nOr with Bun / pnpm:\n\n```sh\nbun add better-result\npnpm add better-result\n```\n\n## Quick Start\n\n```ts\nimport { Result } from \"better-result\";\n\n// Wrap throwing functions\nconst parsed = Result.try(() =\u003e JSON.parse(input));\n\n// Check and use\nif (Result.isOk(parsed)) {\n  console.log(parsed.value);\n} else {\n  console.error(parsed.error);\n}\n\n// Or use pattern matching\nconst message = parsed.match({\n  ok: (data) =\u003e `Got: ${data.name}`,\n  err: (e) =\u003e `Failed: ${e.message}`,\n});\n```\n\n## Contents\n\n- [Creating Results](#creating-results)\n- [Transforming Results](#transforming-results)\n- [Handling Errors](#handling-errors)\n- [Observing Results](#observing-results)\n- [Extracting Values](#extracting-values)\n- [Generator Composition](#generator-composition)\n- [Retry Support](#retry-support)\n- [UnhandledException](#unhandledexception)\n- [Panic](#panic)\n- [Tagged Errors](#tagged-errors)\n- [Serialization](#serialization)\n- [API Reference](#api-reference)\n- [Agents \u0026 AI](#agents--ai)\n\n## Creating Results\n\n```ts\n// Success\nconst ok = Result.ok(42);\n\n// Error\nconst err = Result.err(new Error(\"failed\"));\n\n// From throwing function\nconst result = Result.try(() =\u003e riskyOperation());\n\n// From promise\nconst result = await Result.tryPromise(() =\u003e fetch(url));\n\n// With custom error handling\nconst result = Result.try({\n  try: () =\u003e JSON.parse(input),\n  catch: (e) =\u003e new ParseError(e),\n});\n```\n\n## Transforming Results\n\n```ts\nconst result = Result.ok(2)\n  .map((x) =\u003e x * 2) // Ok(4)\n  .andThen(\n    (\n      x, // Chain Result-returning functions\n    ) =\u003e (x \u003e 0 ? Result.ok(x) : Result.err(\"negative\")),\n  );\n\n// Standalone functions (data-first or data-last)\nResult.map(result, (x) =\u003e x + 1);\nResult.map((x) =\u003e x + 1)(result); // Pipeable\n```\n\n## Handling Errors\n\n```ts\n// Transform error type\nconst result = fetchUser(id).mapError((e) =\u003e new AppError(`Failed to fetch user: ${e.message}`));\n\n// Recover from specific errors while preserving the same success type\nconst result = fetchUser(id).tryRecover((e) =\u003e\n  e._tag === \"NotFoundError\" ? Result.ok(defaultUser) : Result.err(e),\n);\n\n// Async recovery follows the same pattern\n// If fetchUser is async and returns Promise\u003cResult\u003cUser, E\u003e\u003e, await it first.\nconst result = await (\n  await fetchUser(id)\n).tryRecoverAsync(async (e) =\u003e\n  e._tag === \"NetworkError\" ? Result.ok(await readUserFromCache(id)) : Result.err(e),\n);\n```\n\n## Observing Results\n\nUse `tap` / `tapAsync` for success-side logging or tracing, `tapError` / `tapErrorAsync` for error-side logging or tracing, and `tapBoth` / `tapBothAsync` when you want to observe either branch with one handler object. These methods do not transform the `Result` — they always return the original value unchanged.\n\n```ts\nconst result = Result.try(() =\u003e JSON.parse(input))\n  .tap((value) =\u003e {\n    console.debug(\"parsed payload\", value);\n  })\n  .tapError((error) =\u003e {\n    console.error(\"failed to parse payload\", error);\n  });\n```\n\nIf you want to observe both branches symmetrically with one call, use `tapBoth`:\n\n```ts\nconst result = Result.try(() =\u003e JSON.parse(input)).tapBoth({\n  ok: (value) =\u003e {\n    console.info(\"decoded payload\", value);\n  },\n  err: (error) =\u003e {\n    console.warn(\"decode failed\", error);\n  },\n});\n```\n\nAsync side effects follow the same pattern:\n\n```ts\nconst result = await Result.err(\"request failed\").tapErrorAsync(async (error) =\u003e {\n  await trace(\"request.failed\", { error });\n});\n```\n\n`tapBothAsync` works the same way for async observers on either branch:\n\n```ts\nconst observed = await Result.tapBothAsync(Result.try(() =\u003e JSON.parse(input)), {\n  ok: async (value) =\u003e {\n    await trace(\"payload.decoded\", { value });\n  },\n  err: async (error) =\u003e {\n    await trace(\"payload.decode_failed\", { error });\n  },\n});\n```\n\nStatic helpers support both data-first and data-last styles:\n\n```ts\nconst traced = Result.tapError(Result.err(\"cache miss\"), (error) =\u003e {\n  console.warn(\"cache lookup failed\", error);\n});\n\nconst traceError = Result.tapErrorAsync(async (error: string) =\u003e {\n  await trace(\"cache.lookup_failed\", { error });\n});\n\nawait traceError(Result.err(\"cache miss\"));\n```\n\nIf you prefer, you can still observe both branches by chaining `tap` and `tapError` separately.\n\nThrown or rejected side-effect callbacks become `Panic`, just like other Result callbacks.\n\n## Extracting Values\n\n```ts\n// Unwrap (throws on Err)\nconst value = result.unwrap();\nconst value = result.unwrap(\"custom error message\");\n\n// With fallback\nconst value = result.unwrapOr(defaultValue);\n\n// Pattern match\nconst value = result.match({\n  ok: (v) =\u003e v,\n  err: (e) =\u003e fallback,\n});\n```\n\n## Generator Composition\n\nChain multiple Results without nested callbacks or early returns:\n\n```ts\nconst result = Result.gen(function* () {\n  const a = yield* parseNumber(inputA); // Unwraps or short-circuits\n  const b = yield* parseNumber(inputB);\n  const c = yield* divide(a, b);\n  return Result.ok(c);\n});\n// Result\u003cnumber, ParseError | DivisionError\u003e\n```\n\nAsync version with `Result.await`:\n\n```ts\nconst result = await Result.gen(async function* () {\n  const user = yield* Result.await(fetchUser(id));\n  const posts = yield* Result.await(fetchPosts(user.id));\n  return Result.ok({ user, posts });\n});\n```\n\nErrors from all yielded Results are automatically collected into the final error union type.\n\n### Normalizing Error Types\n\nUse `mapError` on the output of `Result.gen()` to unify multiple error types into a single type:\n\n```ts\nclass ParseError extends TaggedError(\"ParseError\")\u003c{ message: string }\u003e() {}\nclass ValidationError extends TaggedError(\"ValidationError\")\u003c{ message: string }\u003e() {}\nclass AppError extends TaggedError(\"AppError\")\u003c{ source: string; message: string }\u003e() {}\n\nconst result = Result.gen(function* () {\n  const parsed = yield* parseInput(input); // Err: ParseError\n  const valid = yield* validate(parsed); // Err: ValidationError\n  return Result.ok(valid);\n}).mapError((e): AppError =\u003e new AppError({ source: e._tag, message: e.message }));\n// Result\u003cValidatedData, AppError\u003e - error union normalized to single type\n```\n\n## Retry Support\n\n```ts\nconst result = await Result.tryPromise(() =\u003e fetch(url), {\n  retry: {\n    times: 3,\n    delayMs: 100,\n    backoff: \"exponential\", // or \"linear\" | \"constant\"\n  },\n});\n```\n\n### Conditional Retry\n\nRetry only for specific error types using `shouldRetry`:\n\n```ts\nclass NetworkError extends TaggedError(\"NetworkError\")\u003c{ message: string }\u003e() {}\nclass ValidationError extends TaggedError(\"ValidationError\")\u003c{ message: string }\u003e() {}\n\nconst result = await Result.tryPromise(\n  {\n    try: () =\u003e fetchData(url),\n    catch: (e) =\u003e\n      e instanceof TypeError // Network failures often throw TypeError\n        ? new NetworkError({ message: (e as Error).message })\n        : new ValidationError({ message: String(e) }),\n  },\n  {\n    retry: {\n      times: 3,\n      delayMs: 100,\n      backoff: \"exponential\",\n      shouldRetry: (e) =\u003e e._tag === \"NetworkError\", // Only retry network errors\n    },\n  },\n);\n```\n\n### Async Retry Decisions\n\nFor retry decisions that require async operations (rate limits, feature flags, etc.), enrich the error in the `catch` handler instead of making `shouldRetry` async:\n\n```ts\nclass ApiError extends TaggedError(\"ApiError\")\u003c{\n  message: string;\n  rateLimited: boolean;\n}\u003e() {}\n\nconst result = await Result.tryPromise(\n  {\n    try: () =\u003e callApi(url),\n    catch: async (e) =\u003e {\n      // Fetch async state in catch handler\n      const retryAfter = await redis.get(`ratelimit:${userId}`);\n      return new ApiError({\n        message: (e as Error).message,\n        rateLimited: retryAfter !== null,\n      });\n    },\n  },\n  {\n    retry: {\n      times: 3,\n      delayMs: 100,\n      backoff: \"exponential\",\n      shouldRetry: (e) =\u003e !e.rateLimited, // Sync predicate uses enriched error\n    },\n  },\n);\n```\n\n## UnhandledException\n\nWhen `Result.try()` or `Result.tryPromise()` catches an exception without a custom handler, the error type is `UnhandledException`:\n\n```ts\nimport { Result, UnhandledException } from \"better-result\";\n\n// Automatic — error type is UnhandledException\nconst result = Result.try(() =\u003e JSON.parse(input));\n//    ^? Result\u003cunknown, UnhandledException\u003e\n\n// Custom handler — you control the error type\nconst result = Result.try({\n  try: () =\u003e JSON.parse(input),\n  catch: (e) =\u003e new ParseError(e),\n});\n//    ^? Result\u003cunknown, ParseError\u003e\n\n// Same for async\nawait Result.tryPromise(() =\u003e fetch(url));\n//    ^? Promise\u003cResult\u003cResponse, UnhandledException\u003e\u003e\n```\n\nAccess the original exception via `.cause`:\n\n```ts\nif (Result.isError(result)) {\n  const original = result.error.cause;\n  if (original instanceof SyntaxError) {\n    // Handle JSON parse error\n  }\n}\n```\n\n## Panic\n\nThrown (not returned) when user callbacks throw inside Result operations. Represents a defect in your code, not a domain error.\n\n```ts\nimport { Panic, isPanic } from \"better-result\";\n\n// Callback throws → Panic\nResult.ok(1).map(() =\u003e {\n  throw new Error(\"bug\");\n}); // throws Panic\n\n// Generator cleanup throws → Panic\nResult.gen(function* () {\n  try {\n    yield* Result.err(\"expected failure\");\n  } finally {\n    throw new Error(\"cleanup bug\");\n  }\n}); // throws Panic\n\n// Catch handler throws → Panic\nResult.try({\n  try: () =\u003e riskyOp(),\n  catch: () =\u003e {\n    throw new Error(\"bug in handler\");\n  },\n}); // throws Panic\n\n// Catching Panic (for error reporting)\ntry {\n  result.map(() =\u003e {\n    throw new Error(\"bug\");\n  });\n} catch (error) {\n  if (isPanic(error)) {\n    // isPanic() is a type guard function\n    console.error(\"Defect:\", error.message, error.cause);\n  }\n\n  if (Panic.is(error)) {\n    // Panic.is() is a static method (same behavior)\n  }\n\n  if (error instanceof Panic) {\n    // instanceof works too\n  }\n}\n```\n\n**Why Panic?** `Err` is for recoverable domain errors. Panic is for bugs — like Rust's `panic!()`. If your `.map()` callback throws, that's not an error to handle, it's a defect to fix. Returning `Err` would collapse type safety (`Result\u003cT, E\u003e` becomes `Result\u003cT, E | unknown\u003e`).\n\n**Panic properties:**\n\n| Property  | Type      | Description                   |\n| --------- | --------- | ----------------------------- |\n| `message` | `string`  | Describes where/what panicked |\n| `cause`   | `unknown` | The exception that was thrown |\n\nPanic also provides `toJSON()` for error reporting services (Sentry, etc.).\n\n## Tagged Errors\n\nBuild exhaustive error handling with discriminated unions:\n\n```ts\nimport { TaggedError, matchError, matchErrorPartial } from \"better-result\";\n\n// Factory API: TaggedError(\"Tag\")\u003cProps\u003e()\nclass NotFoundError extends TaggedError(\"NotFoundError\")\u003c{\n  id: string;\n  message: string;\n}\u003e() {}\n\nclass ValidationError extends TaggedError(\"ValidationError\")\u003c{\n  field: string;\n  message: string;\n}\u003e() {}\n\ntype AppError = NotFoundError | ValidationError;\n\n// Create errors with object args\nconst err = new NotFoundError({ id: \"123\", message: \"User not found\" });\n\n// Exhaustive matching\nmatchError(error, {\n  NotFoundError: (e) =\u003e `Missing: ${e.id}`,\n  ValidationError: (e) =\u003e `Bad field: ${e.field}`,\n});\n\n// Partial matching with fallback\nmatchErrorPartial(\n  error,\n  { NotFoundError: (e) =\u003e `Missing: ${e.id}` },\n  (e) =\u003e `Unknown: ${e.message}`,\n);\n\n// Type guards\nTaggedError.is(value); // any tagged error\nNotFoundError.is(value); // specific class\n```\n\nFor errors with computed messages, add a custom constructor:\n\n```ts\nclass NetworkError extends TaggedError(\"NetworkError\")\u003c{\n  url: string;\n  status: number;\n  message: string;\n}\u003e() {\n  constructor(args: { url: string; status: number }) {\n    super({ ...args, message: `Request to ${args.url} failed: ${args.status}` });\n  }\n}\n\nnew NetworkError({ url: \"/api\", status: 404 });\n```\n\n## Serialization\n\nConvert Results to plain objects for RPC, storage, or server actions:\n\n```ts\nimport { Result, SerializedResult, ResultDeserializationError } from \"better-result\";\n\n// Serialize to plain object\nconst result = Result.ok(42);\nconst serialized = Result.serialize(result);\n// { status: \"ok\", value: 42 }\n\n// Deserialize back to Result instance\nconst deserialized = Result.deserialize\u003cnumber, never\u003e(serialized);\n// Ok(42) - can use .map(), .andThen(), etc.\n\n// Invalid input returns ResultDeserializationError\nconst invalid = Result.deserialize({ foo: \"bar\" });\nif (Result.isError(invalid) \u0026\u0026 ResultDeserializationError.is(invalid.error)) {\n  console.log(\"Bad input:\", invalid.error.value);\n}\n\n// Typed boundary for Next.js server actions\nasync function createUser(data: FormData): Promise\u003cSerializedResult\u003cUser, ValidationError\u003e\u003e {\n  const result = await validateAndCreate(data);\n  return Result.serialize(result);\n}\n\n// Client-side\nconst serialized = await createUser(formData);\nconst result = Result.deserialize\u003cUser, ValidationError\u003e(serialized);\n```\n\n## API Reference\n\n### Result\n\n| Method                               | Description                                                                              |\n| ------------------------------------ | ---------------------------------------------------------------------------------------- |\n| `Result.ok(value)`                   | Create success                                                                           |\n| `Result.err(error)`                  | Create error                                                                             |\n| `Result.try(fn)`                     | Wrap throwing function                                                                   |\n| `Result.tryPromise(fn, config?)`     | Wrap async function with optional retry                                                  |\n| `Result.isOk(result)`                | Type guard for Ok                                                                        |\n| `Result.isError(result)`             | Type guard for Err                                                                       |\n| `Result.gen(fn)`                     | Generator composition                                                                    |\n| `Result.tryRecover(result, fn)`      | Recover error into same success type                                                     |\n| `Result.tryRecoverAsync(result, fn)` | Async recover error into same success type                                               |\n| `Result.tap(result, fn)`             | Run side effect on success and return original result                                    |\n| `Result.tapAsync(result, fn)`        | Run async side effect on success and return original result                              |\n| `Result.tapError(result, fn)`        | Run side effect on error and return original result                                      |\n| `Result.tapErrorAsync(result, fn)`   | Run async side effect on error and return original result                                |\n| `Result.tapBoth(result, handlers)`   | Run side effect on either branch and return original result                              |\n| `Result.tapBothAsync(result, handlers)` | Run async side effect on either branch and return original result                     |\n| `Result.await(promise)`              | Wrap Promise\u003cResult\u003e for generators                                                      |\n| `Result.serialize(result)`           | Convert Result to plain object                                                           |\n| `Result.deserialize(value)`          | Rehydrate serialized Result (returns `Err\u003cResultDeserializationError\u003e` on invalid input) |\n| `Result.partition(results)`          | Split array into [okValues, errValues]                                                   |\n| `Result.flatten(result)`             | Flatten nested Result                                                                    |\n\n### Instance Methods\n\n| Method                 | Description                                |\n| ---------------------- | ------------------------------------------ |\n| `.isOk()`              | Type guard, narrows to Ok                  |\n| `.isErr()`             | Type guard, narrows to Err                 |\n| `.map(fn)`             | Transform success value                    |\n| `.mapError(fn)`        | Transform error value                      |\n| `.tryRecover(fn)`      | Recover error into same success type       |\n| `.tryRecoverAsync(fn)` | Async recover error into same success type |\n| `.andThen(fn)`         | Chain Result-returning function            |\n| `.andThenAsync(fn)`    | Chain async Result-returning function      |\n| `.match({ ok, err })`  | Pattern match                              |\n| `.unwrap(message?)`    | Extract value or throw                     |\n| `.unwrapOr(fallback)`  | Extract value or return fallback           |\n| `.tap(fn)`             | Side effect on success                     |\n| `.tapAsync(fn)`        | Async side effect on success               |\n| `.tapError(fn)`        | Side effect on error                       |\n| `.tapErrorAsync(fn)`   | Async side effect on error                 |\n| `.tapBoth(handlers)`   | Side effect on either branch               |\n| `.tapBothAsync(handlers)` | Async side effect on either branch      |\n\n### TaggedError\n\n| Method                                 | Description                        |\n| -------------------------------------- | ---------------------------------- |\n| `TaggedError(tag)\u003cProps\u003e()`            | Factory for tagged error class     |\n| `TaggedError.is(value)`                | Type guard for any TaggedError     |\n| `matchError(err, handlers)`            | Exhaustive pattern match by `_tag` |\n| `matchErrorPartial(err, handlers, fb)` | Partial match with fallback        |\n| `isTaggedError(value)`                 | Type guard (standalone function)   |\n| `panic(message, cause?)`               | Throw unrecoverable Panic          |\n| `isPanic(value)`                       | Type guard for Panic               |\n\n### Type Helpers\n\n| Type                     | Description                  |\n| ------------------------ | ---------------------------- |\n| `InferOk\u003cR\u003e`             | Extract Ok type from Result  |\n| `InferErr\u003cR\u003e`            | Extract Err type from Result |\n| `SerializedResult\u003cT, E\u003e` | Plain object form of Result  |\n| `SerializedOk\u003cT\u003e`        | Plain object form of Ok      |\n| `SerializedErr\u003cE\u003e`       | Plain object form of Err     |\n\n## Agents \u0026 AI\n\nbetter-result ships with portable `SKILL.md` skills instead of an interactive CLI.\n\n### Available skills\n\n- `better-result-adopt` — adopt `better-result` in an existing codebase\n- `better-result-migrate-v2` — migrate v1 `TaggedError` usage to the v2 API\n\nThese skills are designed to work with SKILL.md-compatible agents and skills.sh-compatible tooling.\n\n### Install with skills.sh-compatible tooling\n\n```sh\nnpx skills add dmmulroy/better-result@better-result-adopt\nnpx skills add dmmulroy/better-result@better-result-migrate-v2\n```\n\nTo install globally without prompts:\n\n```sh\nnpx skills add dmmulroy/better-result@better-result-adopt -g -y\n```\n\n### Manual installation\n\nIf your agent does not support skills.sh installation, copy one of these directories into the agent's skills folder:\n\n- `skills/better-result-adopt/`\n- `skills/better-result-migrate-v2/`\n\n### What the skills do\n\n`better-result-adopt` guides an agent through:\n\n- converting try/catch to `Result.try` / `Result.tryPromise`\n- defining `TaggedError` classes for domain errors\n- refactoring nested error handling into `Result.gen`\n- replacing nullable or sentinel error returns with `Result`\n\n`better-result-migrate-v2` guides an agent through:\n\n- migrating `TaggedError` classes from v1 to v2 factory syntax\n- updating constructor call sites to the new object form\n- replacing `TaggedError.match*` helpers with standalone helpers\n- updating imports and verifying no old API usages remain\n\n### Optional source context\n\nFor richer AI context in a consuming project:\n\n```sh\nnpx opensrc better-result\n```\n\nSee [skills/README.md](skills/README.md) for a concise skill-install reference.\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdmmulroy%2Fbetter-result","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdmmulroy%2Fbetter-result","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdmmulroy%2Fbetter-result/lists"}