@cratis/pi 7.87.1 → 7.89.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.
@@ -36,6 +36,7 @@ Stop-and-fix signals during implementation and review:
36
36
  | A reusable component reaches into domain strings, generated proxies, identity, query, command, or navigation | It is no longer a primitive | Move it to the slice, or make it explicitly Cratis-aware and test/story it as such |
37
37
  | Class/style string copied 3× or grown hard to scan | Styling changes become search-and-replace bugs | Extract to a shared style constant or a focused child component |
38
38
  | `style={{ ... }}` with static token names | Bypasses theming and review | Move to `className`; keep `style` only for runtime values |
39
+ | A domain constant, threshold or eligibility rule in a `.ts`/`.tsx` file (`canApprove`, `maxAttempts = 2`) deciding whether an action shows | The client re-derives what the server knows, so the two drift and the UI breaks silently | Project the affordance as a read-model property server-side and render it. Judgment-shaped; no gate |
39
40
 
40
41
  ## Definition of done for a frontend change
41
42
 
@@ -200,6 +200,7 @@ language policy always follows the consuming repository's `go.mod` minimum.
200
200
  | `cratis/content` | Release notes, social feed posts, voice review, content length, and developer marketing and distribution |
201
201
  | `cratis/review` | Code review, performance, security |
202
202
  | `cratis/codescene` | Opt-in CodeScene Code Health change gates, refactoring, debt prioritization and risk-based testing; requires an account and available MCP tools; each user registers the MCP server (see below) |
203
+ | `cratis/playwright` | Opt-in Playwright browser automation MCP server declaration for exploratory checks; carries no skills or rules and each user registers the server (see below) |
203
204
  | `cratis/studio` | Studio MCP safety guidance |
204
205
  | `cratis/cli` | CLI operations |
205
206
  | `cratis/cli/terminal-workbench` | Human-operated terminal Workbench |
@@ -227,6 +228,27 @@ CodeScene needs an account and OAuth `login`; without an account or available
227
228
  CodeScene tools, skip its steps and say so rather than blocking contributors.
228
229
  The MCP catalog has no install-hints field and does not install the executable.
229
230
 
231
+ ### Browser automation with Playwright
232
+
233
+ Select `cratis/playwright` explicitly to declare the `playwright` stdio server
234
+ (`npx -y @playwright/mcp@0.0.83 --headless --isolated`) in `mcp-servers.json`.
235
+ The flags keep the browser headless and isolated: no persistent profile, so no
236
+ personal cookies or logins are reused. The profile carries no skills or rules, and
237
+ no other profile composes it.
238
+
239
+ Today `cratis ai install` registers only Screenplay and Pi's `cratis-mcp` extension
240
+ loads only Screenplay, so Playwright, like CodeScene, is not registered for you.
241
+ Each user registers it in their own user-level MCP configuration, never in committed
242
+ project files. For Claude Code:
243
+
244
+ ```bash
245
+ claude mcp add --scope user playwright -- npx -y @playwright/mcp@0.0.83 --headless --isolated
246
+ ```
247
+
248
+ Pi users register it through a Pi MCP adapter or extension of their choice. Use it
249
+ for exploratory browser checks; repeatable checks stay in the repository's own
250
+ Playwright tests. Without the server registered, skip browser steps and say so.
251
+
230
252
  ### Event modeling with Screenplay
231
253
 
232
254
  **Model first.** Decide opt-in per `general.md` "Model first"; where `cratis-screenplay-modeling-lifecycle` is installed, run its `scripts/model-first-status.mjs` for the decision, model root and drafts. An accepted model under the root covering the scope is the contract and the source
@@ -228,6 +228,8 @@ findings). Each reactor class should have a focused purpose. Multiple handler me
228
228
  7. **No state** — Reactors should be stateless. Inject dependencies via primary constructor, but do not store mutable state on the class.
229
229
  8. **A new side-effect reactor must not act on history.** A reactor that is new in a release (or has a new id) catches up from the start of the event log when it first registers, and `[OnceOnly]` does not cover that first catch-up because it is not a replay. If its handlers cause side effects (jobs, AI calls, external calls, notifications), start the observer at the tail on first registration or make the handler ignore facts that predate its registration, and specify that history causes nothing.
230
230
  9. **An unattended, irreversible pass reads the log, not the sink — and carries a fuse.** A reactor that deletes, revokes, bills or notifies *because a read model says something is absent* is trusting a materialized sink that is only as current as its observer: a paused partition, a replay in progress or a lagging projection makes "absent" true of the sink and false of the world. Decide from a **`[Passive]`** read model (computed on demand from the events at the moment the reactor runs) or from the events themselves. And the code shape cannot tell an **empty subject set** ("nothing to consider" — fail-safe) from an **empty qualifying set** ("nothing survived the filter" — fail-destructive: every subject is about to be acted on), so an unattended pass states its expected population, refuses when the count is implausible, and caps how many subjects one run may touch — the same discipline as [guards-and-fuses.md](./guards-and-fuses.instructions.md), applied to a reactor. A sweep that decides from a read model also refuses while that model's observer is replaying or behind, because a rebuilding model reads as "nothing was ever processed"; specify the replaying case.
231
+ 10. **Provider clients stay behind a translation or adapter reactor.** A `[Command]` or `IReactor` in a domain slice that injects or imports a provider client (GitHub, payment, mail SDK) is a finding: put the client behind one adapter slice and let domain slices work with our events and commands. Judgment-shaped today; a candidate for a namespace-boundary analyzer.
232
+ 11. **A reactor must not react to its own consequences.** A reactor that posts comments or creates artifacts must guard against re-triggering on what it created (marker, author check, or event-type filter), or one input loops. Judgment-shaped; no gate.
231
233
 
232
234
  ## Failure transitions **[convention]**
233
235
 
@@ -245,6 +245,7 @@ Apply authorization attributes on the `[Command]` record or query method. **[con
245
245
  - **Events are not read models** — never append events for totals, counts, statuses, search indexes, or dashboards. Project those from source events into read models.
246
246
  - **Never duplicate one fact as an aggregate event *plus* a per-item fanout.** If a command fans out per item (one `RequestSentToPartner` per partner), don't also emit an aggregate event carrying the whole list. Keep the fanout as the single source of truth and project any aggregate from it; pick per-item vs. single-with-list event from how consumers read it — never both.
247
247
  - **Command rejection normally appends no event.** Validation and business-rule rejections are command results, not facts. Add a rejection/failed event only when the business must audit or react to that failure later.
248
+ - **A settings event records the decision, not the whole blob.** Split composite configuration into one event per concern; a whole-object `…Set` event is last-writer-wins CRUD and turns every new field into a schema change. The tell is a `…Set`/`…Updated` event carrying a full settings record. Judgment-shaped; no gate.
248
249
  - **One event name = one meaning.** Don't reuse a vague event for several domain facts; if facts can occur, be absent, or have consumers independently, split them into separate event types.
249
250
  - **Never persist clock-relative state.** "Overdue", "expired", "due in 3 days" are true *now* and false later, and a projection only moves when an event arrives — so a projected `IsOverdue` is wrong the moment the clock crosses the line with no event to flip it. Store the **deadline** (a `DateOnly`/`DateTimeOffset` the business supplied) and compute the comparison at the query or UI edge, refreshing when time alone changes the answer. Only when *crossing* the deadline is itself a business fact (a fee is charged, a reminder goes out) does a scheduled process append an event for it.
250
251
 
@@ -308,6 +309,8 @@ query reference for versioned behavior and composition trade-offs.
308
309
 
309
310
  Ladder, first that fits: (1) direct read-model injection — guarded under read protection `On`, advisory under `Off`; keyed by the command's own event-source id, or by `[LookUpBy]`; (2) a `[Passive]` projection; (3) `IReadModels.GetInstanceById<T>((EventSourceId)key)` for a different/derived key; (4) a materialized projection. A lookup *interface* is justified **only** for a genuine non-key search (e.g. find-by-email) — never to wrap a keyed `GetInstanceById`.
310
311
 
312
+ **[convention] One read model per consumer scenario, and dispatch never reads a UI model.** When a second consumer needs different semantics (board vs list vs count vs control flow), add a projection instead of extending the existing model; a command, reactor or job deciding what to do reads a model built for that decision, never a screen's listing model. The tell is a read model many slices import or one that projects many unrelated slices. Judgment-shaped; no gate.
313
+
311
314
  **[contract] Passive state is not sink state.** `IMongoCollection<T>` cannot
312
315
  compute a passive read model; any leftover documents are not maintained state.
313
316
  Read through Chronicle by key or supported injection. **[convention]** Never
@@ -420,6 +423,10 @@ Redaction is a system-event request that removes payload availability from the e
420
423
 
421
424
  Cross-slice access is **read-only and goes through Chronicle** — inject another slice's read model (validator/`Provide()`/`Handle()`), read by explicit key with `IReadModels.GetInstanceById`, mark a `[Passive]` projection for a current read (a rule that must hold under concurrency needs read protection `On`, a stream decision or a constraint, see the `cratis-chronicle-decision-reads` skill's `references/choosing-how-a-rule-holds.md`), or react to its events. **Never** instantiate or DI another slice's command/handler/service.
422
425
 
426
+ **[convention] Prompts are content.** In an AI-backed feature, keep prompt bodies as resources (embedded files/templates) assembled by a typed composer, not as string literals in classes; the tell is a class dominated by multi-line prompt text. Judgment-shaped; no gate.
427
+
428
+ **[convention] The third copy of a job shape is the deadline for the abstraction.** Two similar jobs or decision flows are fine; when a third appears, extract the shared template instead of copying again. Judgment-shaped; no gate.
429
+
423
430
  **DCB (Dynamic Consistency Boundary):** the boundary is the event state explicitly
424
431
  enrolled in guarded decisions, not every model a command reads. Use stream
425
432
  reads/exact scopes or read-model parameters under read protection `On`
@@ -16,6 +16,14 @@
16
16
  "command": "cs-mcp",
17
17
  "args": [],
18
18
  "description": "Review Code Health changes and prioritize maintainability risks with CodeScene; requires a CodeScene account and OAuth login."
19
+ },
20
+ {
21
+ "id": "playwright",
22
+ "profiles": ["cratis/playwright"],
23
+ "transport": "stdio",
24
+ "command": "npx",
25
+ "args": ["-y", "@playwright/mcp@0.0.83", "--headless", "--isolated"],
26
+ "description": "Drive a headless, isolated browser with Playwright for exploratory checks; requires Node.js and downloads the package and a browser on first use."
19
27
  }
20
28
  ]
21
29
  }
@@ -35,6 +35,7 @@ Stop-and-fix signals during implementation and review:
35
35
  | A reusable component reaches into domain strings, generated proxies, identity, query, command, or navigation | It is no longer a primitive | Move it to the slice, or make it explicitly Cratis-aware and test/story it as such |
36
36
  | Class/style string copied 3× or grown hard to scan | Styling changes become search-and-replace bugs | Extract to a shared style constant or a focused child component |
37
37
  | `style={{ ... }}` with static token names | Bypasses theming and review | Move to `className`; keep `style` only for runtime values |
38
+ | A domain constant, threshold or eligibility rule in a `.ts`/`.tsx` file (`canApprove`, `maxAttempts = 2`) deciding whether an action shows | The client re-derives what the server knows, so the two drift and the UI breaks silently | Project the affordance as a read-model property server-side and render it. Judgment-shaped; no gate |
38
39
 
39
40
  ## Definition of done for a frontend change
40
41
 
@@ -199,6 +199,7 @@ language policy always follows the consuming repository's `go.mod` minimum.
199
199
  | `cratis/content` | Release notes, social feed posts, voice review, content length, and developer marketing and distribution |
200
200
  | `cratis/review` | Code review, performance, security |
201
201
  | `cratis/codescene` | Opt-in CodeScene Code Health change gates, refactoring, debt prioritization and risk-based testing; requires an account and available MCP tools; each user registers the MCP server (see below) |
202
+ | `cratis/playwright` | Opt-in Playwright browser automation MCP server declaration for exploratory checks; carries no skills or rules and each user registers the server (see below) |
202
203
  | `cratis/studio` | Studio MCP safety guidance |
203
204
  | `cratis/cli` | CLI operations |
204
205
  | `cratis/cli/terminal-workbench` | Human-operated terminal Workbench |
@@ -226,6 +227,27 @@ CodeScene needs an account and OAuth `login`; without an account or available
226
227
  CodeScene tools, skip its steps and say so rather than blocking contributors.
227
228
  The MCP catalog has no install-hints field and does not install the executable.
228
229
 
230
+ ### Browser automation with Playwright
231
+
232
+ Select `cratis/playwright` explicitly to declare the `playwright` stdio server
233
+ (`npx -y @playwright/mcp@0.0.83 --headless --isolated`) in `mcp-servers.json`.
234
+ The flags keep the browser headless and isolated: no persistent profile, so no
235
+ personal cookies or logins are reused. The profile carries no skills or rules, and
236
+ no other profile composes it.
237
+
238
+ Today `cratis ai install` registers only Screenplay and Pi's `cratis-mcp` extension
239
+ loads only Screenplay, so Playwright, like CodeScene, is not registered for you.
240
+ Each user registers it in their own user-level MCP configuration, never in committed
241
+ project files. For Claude Code:
242
+
243
+ ```bash
244
+ claude mcp add --scope user playwright -- npx -y @playwright/mcp@0.0.83 --headless --isolated
245
+ ```
246
+
247
+ Pi users register it through a Pi MCP adapter or extension of their choice. Use it
248
+ for exploratory browser checks; repeatable checks stay in the repository's own
249
+ Playwright tests. Without the server registered, skip browser steps and say so.
250
+
229
251
  ### Event modeling with Screenplay
230
252
 
231
253
  **Model first.** Decide opt-in per `general.md` "Model first"; where `cratis-screenplay-modeling-lifecycle` is installed, run its `scripts/model-first-status.mjs` for the decision, model root and drafts. An accepted model under the root covering the scope is the contract and the source
@@ -227,6 +227,8 @@ findings). Each reactor class should have a focused purpose. Multiple handler me
227
227
  7. **No state** — Reactors should be stateless. Inject dependencies via primary constructor, but do not store mutable state on the class.
228
228
  8. **A new side-effect reactor must not act on history.** A reactor that is new in a release (or has a new id) catches up from the start of the event log when it first registers, and `[OnceOnly]` does not cover that first catch-up because it is not a replay. If its handlers cause side effects (jobs, AI calls, external calls, notifications), start the observer at the tail on first registration or make the handler ignore facts that predate its registration, and specify that history causes nothing.
229
229
  9. **An unattended, irreversible pass reads the log, not the sink — and carries a fuse.** A reactor that deletes, revokes, bills or notifies *because a read model says something is absent* is trusting a materialized sink that is only as current as its observer: a paused partition, a replay in progress or a lagging projection makes "absent" true of the sink and false of the world. Decide from a **`[Passive]`** read model (computed on demand from the events at the moment the reactor runs) or from the events themselves. And the code shape cannot tell an **empty subject set** ("nothing to consider" — fail-safe) from an **empty qualifying set** ("nothing survived the filter" — fail-destructive: every subject is about to be acted on), so an unattended pass states its expected population, refuses when the count is implausible, and caps how many subjects one run may touch — the same discipline as [guards-and-fuses.md](./guards-and-fuses.md), applied to a reactor. A sweep that decides from a read model also refuses while that model's observer is replaying or behind, because a rebuilding model reads as "nothing was ever processed"; specify the replaying case.
230
+ 10. **Provider clients stay behind a translation or adapter reactor.** A `[Command]` or `IReactor` in a domain slice that injects or imports a provider client (GitHub, payment, mail SDK) is a finding: put the client behind one adapter slice and let domain slices work with our events and commands. Judgment-shaped today; a candidate for a namespace-boundary analyzer.
231
+ 11. **A reactor must not react to its own consequences.** A reactor that posts comments or creates artifacts must guard against re-triggering on what it created (marker, author check, or event-type filter), or one input loops. Judgment-shaped; no gate.
230
232
 
231
233
  ## Failure transitions **[convention]**
232
234
 
@@ -244,6 +244,7 @@ Apply authorization attributes on the `[Command]` record or query method. **[con
244
244
  - **Events are not read models** — never append events for totals, counts, statuses, search indexes, or dashboards. Project those from source events into read models.
245
245
  - **Never duplicate one fact as an aggregate event *plus* a per-item fanout.** If a command fans out per item (one `RequestSentToPartner` per partner), don't also emit an aggregate event carrying the whole list. Keep the fanout as the single source of truth and project any aggregate from it; pick per-item vs. single-with-list event from how consumers read it — never both.
246
246
  - **Command rejection normally appends no event.** Validation and business-rule rejections are command results, not facts. Add a rejection/failed event only when the business must audit or react to that failure later.
247
+ - **A settings event records the decision, not the whole blob.** Split composite configuration into one event per concern; a whole-object `…Set` event is last-writer-wins CRUD and turns every new field into a schema change. The tell is a `…Set`/`…Updated` event carrying a full settings record. Judgment-shaped; no gate.
247
248
  - **One event name = one meaning.** Don't reuse a vague event for several domain facts; if facts can occur, be absent, or have consumers independently, split them into separate event types.
248
249
  - **Never persist clock-relative state.** "Overdue", "expired", "due in 3 days" are true *now* and false later, and a projection only moves when an event arrives — so a projected `IsOverdue` is wrong the moment the clock crosses the line with no event to flip it. Store the **deadline** (a `DateOnly`/`DateTimeOffset` the business supplied) and compute the comparison at the query or UI edge, refreshing when time alone changes the answer. Only when *crossing* the deadline is itself a business fact (a fee is charged, a reminder goes out) does a scheduled process append an event for it.
249
250
 
@@ -307,6 +308,8 @@ query reference for versioned behavior and composition trade-offs.
307
308
 
308
309
  Ladder, first that fits: (1) direct read-model injection — guarded under read protection `On`, advisory under `Off`; keyed by the command's own event-source id, or by `[LookUpBy]`; (2) a `[Passive]` projection; (3) `IReadModels.GetInstanceById<T>((EventSourceId)key)` for a different/derived key; (4) a materialized projection. A lookup *interface* is justified **only** for a genuine non-key search (e.g. find-by-email) — never to wrap a keyed `GetInstanceById`.
309
310
 
311
+ **[convention] One read model per consumer scenario, and dispatch never reads a UI model.** When a second consumer needs different semantics (board vs list vs count vs control flow), add a projection instead of extending the existing model; a command, reactor or job deciding what to do reads a model built for that decision, never a screen's listing model. The tell is a read model many slices import or one that projects many unrelated slices. Judgment-shaped; no gate.
312
+
310
313
  **[contract] Passive state is not sink state.** `IMongoCollection<T>` cannot
311
314
  compute a passive read model; any leftover documents are not maintained state.
312
315
  Read through Chronicle by key or supported injection. **[convention]** Never
@@ -419,6 +422,10 @@ Redaction is a system-event request that removes payload availability from the e
419
422
 
420
423
  Cross-slice access is **read-only and goes through Chronicle** — inject another slice's read model (validator/`Provide()`/`Handle()`), read by explicit key with `IReadModels.GetInstanceById`, mark a `[Passive]` projection for a current read (a rule that must hold under concurrency needs read protection `On`, a stream decision or a constraint, see the `cratis-chronicle-decision-reads` skill's `references/choosing-how-a-rule-holds.md`), or react to its events. **Never** instantiate or DI another slice's command/handler/service.
421
424
 
425
+ **[convention] Prompts are content.** In an AI-backed feature, keep prompt bodies as resources (embedded files/templates) assembled by a typed composer, not as string literals in classes; the tell is a class dominated by multi-line prompt text. Judgment-shaped; no gate.
426
+
427
+ **[convention] The third copy of a job shape is the deadline for the abstraction.** Two similar jobs or decision flows are fine; when a third appears, extract the shared template instead of copying again. Judgment-shaped; no gate.
428
+
422
429
  **DCB (Dynamic Consistency Boundary):** the boundary is the event state explicitly
423
430
  enrolled in guarded decisions, not every model a command reads. Use stream
424
431
  reads/exact scopes or read-model parameters under read protection `On`
@@ -214,8 +214,8 @@ Stage renders two projection forms; the restrictions differ.
214
214
  1. The identifier always equals the projection's effective key: the inline `key`, else the
215
215
  `from` block `key`, else the event source. An event-source key (default or
216
216
  `key $eventSourceId`) makes the identifier the event source; an event-property or literal
217
- key makes it that key. Stage refuses a literal key (`STAGE-ESM-017`; scoped keys are an
218
- event property or the event source). Never map `$eventSourceId` onto the identifier when the key is
217
+ key makes it that key. A literal key (`key "global"`) renders only at the root of a scope as
218
+ Chronicle's constant key; every other scoped key is an event property or the event source. Never map `$eventSourceId` onto the identifier when the key is
219
219
  something else (`from PaymentRecorded key invoiceId`): the runtime fails with "affected key
220
220
  disagrees with read-model identifier".
221
221
  2. Executable model. A flat-bound projection must map the identifier like any required
@@ -237,8 +237,10 @@ Stage renders two projection forms; the restrictions differ.
237
237
  the event source, drop the explicit key and the projection is scoped, which admits literals.
238
238
  - **Scoped:** every other projection, including a plain single `from E` with the default or
239
239
  `$eventSourceId` key. Forms: several distinct `from` blocks, `remove with`, root
240
- `join ... on`, `children` with an identified element (one level, no children or joins
241
- inside a child; `remove with` and `remove via join` inside a child are admitted), `nested`
240
+ `join ... on`, `children` with an identified element (one level, no children inside a child;
241
+ a `join` inside a child matches the element's own identity, so it names no `on` of its own and its
242
+ mappings count towards the element's required properties; `remove with` and `remove via join`
243
+ inside a child are admitted), `nested`
242
244
  over an optional composite property, and `every`/`all` with set-from-event-source mappings. Keys
243
245
  are an event property or the event source. The key establishes the identifier, so it must
244
246
  end up unmapped: a mapping onto it is refused unless it copies the key's own event
@@ -256,9 +258,24 @@ Stage renders two projection forms; the restrictions differ.
256
258
  - Refused in a scope (`STAGE-ESM-017`): composite keys; empty/literal `all` mappings;
257
259
  unmapped `all` properties outside that nullable subset; `every` with
258
260
  children included beside children or nested blocks; root `remove via join`; joins,
259
- children or join removals inside `nested`; a `clear` inside `nested`; recursive children
260
- and joins inside a child; duplicate event contracts among `from`, join or
261
+ children or join removals inside `nested`; a `clear` inside `nested`; recursive children;
262
+ duplicate event contracts among `from`, join or
261
263
  removal blocks; event or read-model property names that collide with Chronicle syntax.
264
+ - Running totals: `add`, `subtract`, `increment` and `decrement` start from 0 where nothing was set. `add x by 0`
265
+ (or `subtract x by 0`) on a required plain whole number or decimal only makes the total exist and renders as nothing;
266
+ any other constant operand is refused (Chronicle's `Add` takes an event property), and so is a concept total at zero
267
+ (a never-set concept is null in Chronicle). An optional total (plain or concept) that is also cleared, or set from a
268
+ value that may be absent, is refused: Chronicle cannot add to a property it has set to null. The added event property
269
+ must be the total's type, a concept into the number it holds, a number into a concept, or a concept into another concept
270
+ of the same number (the target concept declares the conversion); a concept into an optional plain number is refused
271
+ (`STAGE-ESM-017`: Chronicle rejects the double conversion).
272
+ - `$eventSourceId` in a mapping of a block keyed by an event property (`sku = $eventSourceId`) renders
273
+ when every command that produces the event addresses it with a value of the target's type (and no
274
+ reaction produces it under another type); the same mapping onto the key itself is always refused.
275
+ - Events a state-view slice declares (a projection removes with an event no command of the slice produces) are generated
276
+ with the slice.
277
+ - A command whose productions address different event sources (`for orderId`, `for productId`, ESM v30) renders; each
278
+ event goes to the source its own destination names.
262
279
  - A scope cannot also carry flat transitions.
263
280
 
264
281
  The example in [render-example.md](render-example.md) uses a scope (`from` plus `remove with`).
@@ -629,6 +629,13 @@
629
629
  "language-agnostic"
630
630
  ]
631
631
  },
632
+ {
633
+ "id": "cratis/playwright",
634
+ "availableTargets": [],
635
+ "languages": [
636
+ "language-agnostic"
637
+ ]
638
+ },
632
639
  {
633
640
  "id": "cratis/components",
634
641
  "availableTargets": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cratis/pi",
3
- "version": "7.87.1",
3
+ "version": "7.89.0",
4
4
  "description": "Configuration-aware Cratis AI integration for Pi",
5
5
  "type": "module",
6
6
  "files": [