stitchkit 0.31.0 → 0.32.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/cli.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  emitResult,
5
5
  parseCliArgs,
6
6
  pollUntilDone
7
- } from "./index-zj1s68tb.js";
7
+ } from "./index-36qnfzxe.js";
8
8
  import"./index-0ed3bx43.js";
9
9
  import"./index-zza375qp.js";
10
10
  import"./index-c7nyw0yt.js";
@@ -348,8 +348,8 @@ function toolResultFromError(err) {
348
348
  }
349
349
  async function executeToolMethod(method, toolName, rawArgs, context, hooks, lifecycle, coerceJson = false, onOutputStrip) {
350
350
  const startedAt = Date.now();
351
- const finish = async (result) => {
352
- await hooks?.afterToolCall?.(toolName, rawArgs, result, Date.now() - startedAt, context, method);
351
+ const finish = async (result, thrown) => {
352
+ await hooks?.afterToolCall?.(toolName, rawArgs, result, Date.now() - startedAt, context, method, thrown);
353
353
  return result;
354
354
  };
355
355
  if (hooks?.beforeToolCall) {
@@ -430,7 +430,7 @@ async function executeToolMethod(method, toolName, rawArgs, context, hooks, life
430
430
  console.error("[stitchkit] onToolError hook failed:", hookErr);
431
431
  }
432
432
  }
433
- return finish(toolResultFromError(err));
433
+ return finish(toolResultFromError(err), err);
434
434
  }
435
435
  }
436
436
 
@@ -1 +1 @@
1
- {"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../../src/observability/audit.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,aAAa,EAAc,MAAM,kBAAkB,CAAC;AAElE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,EAAe,KAAK,eAAe,EAAmB,MAAM,YAAY,CAAC;AAGhF,oCAAoC;AACpC,MAAM,WAAW,WAAW;IAC1B;;;;OAIG;IACH,KAAK,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD,iFAAiF;IACjF,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,OAAO,CAAC;IAC1C,yEAAyE;IACzE,QAAQ,CAAC,EAAE,eAAe,CAAC;CAC5B;AAED,qEAAqE;AACrE,MAAM,WAAW,SAAS;IACxB;;;;OAIG;IACH,IAAI,EAAE,CAAC,CAAC,EACN,OAAO,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,KAAK,OAAO,CAAC,QAAQ,CAAC,KACpD,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IACpD;;;OAGG;IACH,QAAQ,EAAE,aAAa,CAAC;CACzB;AAmBD,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,CA6G9D"}
1
+ {"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../../src/observability/audit.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,aAAa,EAAc,MAAM,kBAAkB,CAAC;AAElE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,EAAe,KAAK,eAAe,EAAmB,MAAM,YAAY,CAAC;AAGhF,oCAAoC;AACpC,MAAM,WAAW,WAAW;IAC1B;;;;OAIG;IACH,KAAK,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD,iFAAiF;IACjF,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,OAAO,CAAC;IAC1C,yEAAyE;IACzE,QAAQ,CAAC,EAAE,eAAe,CAAC;CAC5B;AAED,qEAAqE;AACrE,MAAM,WAAW,SAAS;IACxB;;;;OAIG;IACH,IAAI,EAAE,CAAC,CAAC,EACN,OAAO,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,KAAK,OAAO,CAAC,QAAQ,CAAC,KACpD,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IACpD;;;OAGG;IACH,QAAQ,EAAE,aAAa,CAAC;CACzB;AAyCD,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,CA6G9D"}
@@ -109,6 +109,15 @@ function toolErrorMessage(result) {
109
109
  }
110
110
  return hint;
111
111
  }
112
+ function auditErrorMessage(result, thrown) {
113
+ if (result.code === "INTERNAL_SERVER_ERROR" && thrown !== undefined) {
114
+ if (thrown instanceof Error)
115
+ return thrown.message;
116
+ if (typeof thrown === "string")
117
+ return thrown;
118
+ }
119
+ return toolErrorMessage(result);
120
+ }
112
121
  function readString(value) {
113
122
  return typeof value === "string" ? value : undefined;
114
123
  }
@@ -169,7 +178,7 @@ function createAuditHook(config) {
169
178
  };
170
179
  };
171
180
  const toolCall = {
172
- afterToolCall: (toolName, args, result, durationMs, context, endpoint) => {
181
+ afterToolCall: (toolName, args, result, durationMs, context, endpoint, thrown) => {
173
182
  const requestCtx = getRequestContext();
174
183
  const span = requestCtx ? childSpan(requestCtx.trace) : createTraceContext();
175
184
  const measure = result.ok ? measureSize(result.data) : { resultSize: null, responseBytes: 0 };
@@ -189,7 +198,7 @@ function createAuditHook(config) {
189
198
  statusCode: result.ok ? 200 : 400,
190
199
  durationMs,
191
200
  errorCode: result.ok ? undefined : result.code,
192
- errorMessage: result.ok ? undefined : toolErrorMessage(result),
201
+ errorMessage: result.ok ? undefined : auditErrorMessage(result, thrown),
193
202
  ...!result.ok && result.details !== undefined && {
194
203
  errorDetail: sanitizePayload(result.details, sanitize)
195
204
  },
@@ -15,7 +15,18 @@ export interface ToolCallContext {
15
15
  }
16
16
  export interface ToolCallHooks {
17
17
  beforeToolCall?: (toolName: string, args: Record<string, unknown>, context: ToolCallContext, endpoint: MethodDef) => void | Promise<void>;
18
- afterToolCall?: (toolName: string, args: Record<string, unknown>, result: ToolResult, durationMs: number, context: ToolCallContext, endpoint: MethodDef) => void | Promise<void>;
18
+ /**
19
+ * Every finished call, success and failure alike — the record of the call.
20
+ *
21
+ * `error` is the value **as thrown**, present only when the call failed by
22
+ * throwing (never for an argument-validation failure, an output-schema
23
+ * mismatch or a `beforeToolCall` rejection — those never had a raw value).
24
+ * It is the same value `onToolError` receives, handed here too so a single
25
+ * hook can build one row that names the cause: the `result` alone cannot,
26
+ * because an unexpected throw is scrubbed to a bare `INTERNAL_SERVER_ERROR`.
27
+ * A six-parameter hook is unaffected.
28
+ */
29
+ afterToolCall?: (toolName: string, args: Record<string, unknown>, result: ToolResult, durationMs: number, context: ToolCallContext, endpoint: MethodDef, error?: unknown) => void | Promise<void>;
19
30
  /**
20
31
  * The handler path threw — the value **as thrown**, before it is normalised
21
32
  * into a `ToolResult`. The tool-side answer to HTTP's `hooks.onError`, and the
@@ -1 +1 @@
1
- {"version":3,"file":"execute.d.ts","sourceRoot":"","sources":["../../src/tools/execute.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAGnE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAIjD,MAAM,MAAM,UAAU,GAClB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,GAC3B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAElE,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,eAAe,CAAC;IACxB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,aAAa;IAC5B,cAAc,CAAC,EAAE,CACf,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,EAAE,eAAe,EACxB,QAAQ,EAAE,SAAS,KAChB,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,aAAa,CAAC,EAAE,CACd,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,MAAM,EAAE,UAAU,EAClB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,eAAe,EACxB,QAAQ,EAAE,SAAS,KAChB,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,WAAW,CAAC,EAAE,CACZ,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,OAAO,EACd,OAAO,EAAE,eAAe,EACxB,QAAQ,EAAE,SAAS,KAChB,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B;AAED;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;AAEjF;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,aAAa;IAC5B,oDAAoD;IACpD,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,cAAc,EAAE,QAAQ,EAAE,SAAS,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClF,0DAA0D;IAC1D,WAAW,CAAC,EAAE,CACZ,GAAG,EAAE,cAAc,EACnB,MAAM,EAAE,OAAO,EACf,QAAQ,EAAE,SAAS,KAChB,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACjC;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,UAAU,EAAE;IAAE,EAAE,EAAE,KAAK,CAAA;CAAE,CAAC,CAQpF;AAED,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,SAAS,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,EAC5C,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChC,OAAO,EAAE,eAAe,EACxB,KAAK,CAAC,EAAE,aAAa,EACrB,SAAS,CAAC,EAAE,aAAa,EACzB,UAAU,UAAQ,EAClB,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,IAAI,GACxC,OAAO,CAAC,UAAU,CAAC,CAmIrB"}
1
+ {"version":3,"file":"execute.d.ts","sourceRoot":"","sources":["../../src/tools/execute.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAGnE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAIjD,MAAM,MAAM,UAAU,GAClB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,GAC3B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAElE,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,eAAe,CAAC;IACxB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,aAAa;IAC5B,cAAc,CAAC,EAAE,CACf,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,EAAE,eAAe,EACxB,QAAQ,EAAE,SAAS,KAChB,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B;;;;;;;;;;OAUG;IACH,aAAa,CAAC,EAAE,CACd,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,MAAM,EAAE,UAAU,EAClB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,eAAe,EACxB,QAAQ,EAAE,SAAS,EACnB,KAAK,CAAC,EAAE,OAAO,KACZ,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,WAAW,CAAC,EAAE,CACZ,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,OAAO,EACd,OAAO,EAAE,eAAe,EACxB,QAAQ,EAAE,SAAS,KAChB,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B;AAED;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;AAEjF;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,aAAa;IAC5B,oDAAoD;IACpD,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,cAAc,EAAE,QAAQ,EAAE,SAAS,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClF,0DAA0D;IAC1D,WAAW,CAAC,EAAE,CACZ,GAAG,EAAE,cAAc,EACnB,MAAM,EAAE,OAAO,EACf,QAAQ,EAAE,SAAS,KAChB,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACjC;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,UAAU,EAAE;IAAE,EAAE,EAAE,KAAK,CAAA;CAAE,CAAC,CAQpF;AAED,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,SAAS,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,EAC5C,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChC,OAAO,EAAE,eAAe,EACxB,KAAK,CAAC,EAAE,aAAa,EACrB,SAAS,CAAC,EAAE,aAAa,EACzB,UAAU,UAAQ,EAClB,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,IAAI,GACxC,OAAO,CAAC,UAAU,CAAC,CAwIrB"}
package/dist/tools.js CHANGED
@@ -21,7 +21,7 @@ import {
21
21
  readCapped,
22
22
  toolResultFromError,
23
23
  writeDownload
24
- } from "./index-zj1s68tb.js";
24
+ } from "./index-36qnfzxe.js";
25
25
  import {
26
26
  toJsonSchema
27
27
  } from "./index-0ed3bx43.js";
package/llms-full.txt CHANGED
@@ -3024,9 +3024,10 @@ metric, a custom log line, anything that is not a full audit row.
3024
3024
 
3025
3025
  `afterHandle(ctx, result, endpoint)` runs after a handler returns;
3026
3026
  `onError(ctx, error, endpoint)` when one throws. `afterToolCall(toolName, args,
3027
- result, durationMs, context)` runs after every tool call — success and error
3028
- alike — carrying the tool name, the arguments, the result, the duration and the
3029
- call context.
3027
+ result, durationMs, context, endpoint, error)` runs after every tool call —
3028
+ success and error alike — carrying the tool name, the arguments, the result, the
3029
+ duration, the call context, the endpoint identity, and (only when the call failed
3030
+ by throwing) the raw thrown value.
3030
3031
 
3031
3032
  ```ts
3032
3033
  createMcpHandler({
@@ -3086,35 +3087,43 @@ observe. (This is also why it lives on `ToolCallHooks` rather than being an
3086
3087
  `Response`, which a tool call has no use for, and a whole `createServer` hooks
3087
3088
  object must stay assignable to `ToolLifecycle`.)
3088
3089
 
3089
- **`setRequestError` is the wrong sink here mind which record you are filling.**
3090
- It writes to the *request* context, and `createAuditHook`'s **tool** row does not
3091
- read it: `errorCode` / `errorMessage` / `errorDetail` on a tool event come from
3092
- the `ToolResult`, and only `userId` / `clientId` / `ipAddress` / `userAgent` /
3093
- `dimensions` come from the context. So calling it in `onToolError` leaves the
3094
- tool audit row exactly as scrubbed as before and for MCP over HTTP it also
3095
- writes the cause into the log line of the enclosing `/mcp` request, turning one
3096
- incident into two records.
3090
+ **Do not reach for `setRequestError` here.** It writes to the *request* context,
3091
+ which `createAuditHook`'s **tool** row does not read: a tool event takes
3092
+ `errorCode` / `errorMessage` / `errorDetail` from the `ToolResult`, and only
3093
+ identity and `dimensions` from the context. Calling it in `onToolError` would
3094
+ leave the tool row exactly as scrubbed as before and for MCP over HTTP it would
3095
+ also write the cause into the log line of the enclosing `/mcp` request, turning
3096
+ one incident into two records. It is right for the **HTTP** path, where the
3097
+ request *is* the record.
3097
3098
 
3098
- Route the cause to your own sink from `onToolError`, as above, and let the audit
3099
- row stay the record of the *call*. If you want one row carrying both, correlate
3100
- the two hooks yourself — key the cause by the call and read it back in
3101
- `afterToolCall`:
3099
+ ### One row that names the cause
3102
3100
 
3103
- ```ts
3104
- const causes = new WeakMap<object, unknown>()
3101
+ You do not need to correlate the two hooks yourself. `afterToolCall` receives the
3102
+ same raw value as a **seventh parameter**, so one hook can build one record:
3105
3103
 
3104
+ ```ts
3106
3105
  hooks: {
3107
- onToolError: (_tool, error, context) => { causes.set(context, error) },
3108
- afterToolCall: (toolName, _args, result, durationMs, context, endpoint) => {
3109
- void writeRow({ toolName, result, durationMs, cause: causes.get(context), endpoint })
3106
+ afterToolCall: (toolName, args, result, durationMs, context, endpoint, error) => {
3107
+ void writeRow({ toolName, result, durationMs, cause: error, endpoint })
3110
3108
  },
3111
3109
  }
3112
3110
  ```
3113
3111
 
3114
- Every mount builds a fresh context object per call and hands the same reference
3115
- to both hooks, so it is a safe key and nothing leaks. `setRequestError` remains
3116
- right for the **HTTP** path, where the request *is* the record see the fields
3117
- section above.
3112
+ `error` is present only when the call failed by **throwing** a
3113
+ validation failure or a `beforeToolCall` rejection leaves it `undefined`, because
3114
+ neither ever had a raw value to lose. The parameter is additive: a six-parameter
3115
+ hook written before it keeps compiling and keeps firing.
3116
+
3117
+ `createAuditHook` uses it already. Where the envelope was scrubbed to
3118
+ `INTERNAL_SERVER_ERROR`, the row's `errorMessage` becomes the real message
3119
+ instead of the placeholder; a truthful envelope (a thrown `AppError`, a
3120
+ `ZodError`) is left alone, `errorCode` and `errorDetail` are untouched, and the
3121
+ stack is not written — that is `onToolError`'s job, for a tracker that wants it.
3122
+ The caller still receives the scrubbed envelope in every case: the raw text
3123
+ reaches your server-side record, never the response. → ADR 0042
3124
+
3125
+ So the two hooks divide by **purpose**, not by capability — `onToolError` for a
3126
+ sink of your own (a tracker, a stack, an alert), `afterToolCall` for the record.
3118
3127
 
3119
3128
  ### Keying a row on (service, action)
3120
3129
 
@@ -3887,7 +3896,7 @@ Server-only. Turns contracts into MCP and AI-agent tools. Needs the
3887
3896
  | `Toolkit` | _type_ | the context-pinned tool surface from `createToolkit` |
3888
3897
  | `ToolExtend` | _type_ | extra-args extension for `mountMcp` / `mountAgent` |
3889
3898
  | `ToolLifecycle` | _type_ | `beforeHandle` / `afterHandle` gate for tool calls — [guide](../guide/mcp-and-agents.md#guarding-tools--lifecycle) |
3890
- | `ToolCallHooks` | _type_ | `beforeToolCall` / `afterToolCall` / `onToolError` observability hooks — the last carries the value **as thrown**, before normalisation scrubs an unexpected error to `INTERNAL_SERVER_ERROR` ([guide](../guide/observability.md#the-cause-behind-a-failed-tool-call)) |
3899
+ | `ToolCallHooks` | _type_ | `beforeToolCall` / `afterToolCall` / `onToolError` observability hooks — the raw thrown value reaches both of the last two (`afterToolCall`'s 7th parameter), because normalisation scrubs an unexpected error to `INTERNAL_SERVER_ERROR` ([guide](../guide/observability.md#the-cause-behind-a-failed-tool-call)) |
3891
3900
  | `ErrorHintFn` | _type_ | `(toolName, errorCode) => string \| null` — a per-tool recovery hint, shared by every mount |
3892
3901
  | `ToolResult` | _type_ | the result of one tool call |
3893
3902
  | `ToolCallContext` | _type_ | the context every tool hook receives — `{ source }` plus whatever the mount's `context` added |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stitchkit",
3
- "version": "0.31.0",
3
+ "version": "0.32.0",
4
4
  "description": "Contract-first backend framework — one defineContract() into an HTTP API, MCP tools, AI-agent tools and a typed client. Bun and Node.",
5
5
  "keywords": [
6
6
  "bun",