@extrovert.dev/sdk 0.1.0-pre.8 → 0.1.0-pre.9

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/README.md CHANGED
@@ -30,7 +30,7 @@ for a stable release:
30
30
  npm install @extrovert.dev/sdk@next
31
31
  ```
32
32
 
33
- Pin `@extrovert.dev/sdk@0.1.0-pre.8` when a dogfood test needs a reproducible contract snapshot.
33
+ Pin `@extrovert.dev/sdk@0.1.0-pre.9` when a dogfood test needs a reproducible contract snapshot.
34
34
  Requires Node 18+ for global `fetch` and Web Crypto.
35
35
 
36
36
  ## Build and use from source
@@ -529,7 +529,7 @@ a wire protocol (there is no `/v1/contract` endpoint).
529
529
  ```ts
530
530
  import { CONTRACT_VERSION, CONTRACT_MANIFEST } from "@extrovert.dev/sdk";
531
531
 
532
- CONTRACT_VERSION; // "0.1.0-pre.8": provisional, pre-1.0; pin it
532
+ CONTRACT_VERSION; // "0.1.0-pre.9": provisional, pre-1.0; pin it
533
533
  CONTRACT_MANIFEST.stability; // "provisional"
534
534
  CONTRACT_MANIFEST.core_shapes; // ["ReviewIntent","ReviewFeedback","DiffJson","Rule","ReviewEvent"]
535
535
  ```
@@ -550,7 +550,7 @@ page and the agent skills (`extrovert-send-email`, `extrovert-writing-rules`).
550
550
  The Review-Loop shapes are an **open, documented contract: versioned *with* this SDK** (not a wire
551
551
  protocol; there is no `/v1/contract` endpoint). Three guarantees:
552
552
 
553
- - **One version, everywhere.** `CONTRACT_VERSION` is **`0.1.0-pre.8`**, reconciled across the SDK package
553
+ - **One version, everywhere.** `CONTRACT_VERSION` is **`0.1.0-pre.9`**, reconciled across the SDK package
554
554
  version, the MCP server, and the OpenAPI `info.version`. Pin it; pin `CONTRACT_MANIFEST` for the
555
555
  exact shape set you built against.
556
556
  - **Named, documented types.** The five canonical shapes: `ReviewIntent`, `ReviewFeedback`,
@@ -581,7 +581,7 @@ EXTROVERT_API_BASE_URL=mock npx tsx examples/wait-for-otp.ts
581
581
 
582
582
  ## Status
583
583
 
584
- > **Note.** This source SDK tracks the `/v1` contract at `CONTRACT_VERSION` `0.1.0-pre.8`: a
584
+ > **Note.** This source SDK tracks the `/v1` contract at `CONTRACT_VERSION` `0.1.0-pre.9`: a
585
585
  > deliberate **prerelease**, pre-1.0, expect additive change. The offline `mock` transport models the
586
586
  > live server closely enough to reproduce a 422 `intent_required` and a queued review, so build and
587
587
  > test against it before you have a key. Install from the `next` tag until a stable release is cut.
package/dist/index.cjs CHANGED
@@ -268,7 +268,7 @@ var CURRENT_API_VERSION = "2026-06-23";
268
268
  var API_VERSION_HEADER = "Extrovert-Version";
269
269
 
270
270
  // src/http.ts
271
- var SDK_VERSION = "0.1.0-pre.8";
271
+ var SDK_VERSION = "0.1.0-pre.9";
272
272
  function buildUrl(baseUrl, path, query) {
273
273
  const base = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
274
274
  const rel = path.startsWith("/") ? path : `/${path}`;
@@ -1821,8 +1821,21 @@ var MockBackend = class {
1821
1821
  * is a pure lexical filter (every token must appear in name+description) - NO LLM,
1822
1822
  * mirroring the server.
1823
1823
  */
1824
+ categoryUsage(category) {
1825
+ const now2 = Date.now();
1826
+ const rows = [...this.state.reviews.values()].filter((r) => r.category_id === category.id && Date.parse(r.created_at) <= now2);
1827
+ const count = (days) => rows.filter((r) => Date.parse(r.created_at) >= now2 - days * 864e5).length;
1828
+ return {
1829
+ ...category,
1830
+ message_count_7d: count(7),
1831
+ message_count_30d: count(30),
1832
+ message_count_90d: count(90),
1833
+ last_used_at: rows.map((r) => r.created_at).sort().slice(-1)[0],
1834
+ pending_review_count: rows.filter((r) => ["needs_review", "in_review", "chatting", "rejected", "stale", "approved"].includes(r.state)).length
1835
+ };
1836
+ }
1824
1837
  listCategories(params = {}) {
1825
- let items = [...this.state.categories.values()].filter((c) => !c.merged_into).sort((a, b) => b.created_at.localeCompare(a.created_at));
1838
+ let items = [...this.state.categories.values()].filter((c) => !c.merged_into).map((c) => this.categoryUsage(c)).sort((a, b) => b.created_at.localeCompare(a.created_at));
1826
1839
  const tokens = (params.match ?? "").trim().toLowerCase().split(/\s+/).filter(Boolean);
1827
1840
  if (tokens.length) {
1828
1841
  items = items.filter((c) => {
@@ -1830,15 +1843,22 @@ var MockBackend = class {
1830
1843
  return tokens.every((t) => hay.includes(t));
1831
1844
  });
1832
1845
  }
1833
- return { items, total: items.length };
1846
+ const field = { messages_7d: "message_count_7d", messages_90d: "message_count_90d", pending_reviews: "pending_review_count" }[params.sort] ?? "message_count_30d";
1847
+ items.sort((a, b) => (params.sort === "name" ? a.name.toLowerCase().localeCompare(b.name.toLowerCase()) : params.sort === "last_used" ? 0 : (b[field] ?? 0) - (a[field] ?? 0)) || (b.last_used_at ?? "").localeCompare(a.last_used_at ?? "") || a.id.localeCompare(b.id));
1848
+ const offset = params.page ? JSON.parse(Buffer.from(params.page, "base64url").toString()).o : 0;
1849
+ const total = items.length, limit = params.limit ?? 100;
1850
+ const next = offset + limit < total ? Buffer.from(JSON.stringify({ o: offset + limit, v: 1 })).toString("base64url") : void 0;
1851
+ return { items: items.slice(offset, offset + limit), total, next_cursor: next };
1834
1852
  }
1835
1853
  /** Get one category (mock), or undefined when not found. */
1836
1854
  getCategory(categoryId) {
1837
- return this.state.categories.get(categoryId);
1855
+ const category = this.state.categories.get(categoryId);
1856
+ return category ? this.categoryUsage(category) : void 0;
1838
1857
  }
1839
1858
  /** Propose a category (mock): stands immediately, author_kind=agent (D9). */
1840
1859
  proposeCategory(req) {
1841
1860
  const name = req.name.trim();
1861
+ if ([...this.state.categories.values()].some((c) => !c.merged_into && c.name.toLowerCase() === name.toLowerCase())) throw new ConflictError({ status: 409, code: "conflict", message: "Category name already exists; browse and reuse it." });
1842
1862
  if (!name) {
1843
1863
  throw new ValidationError({ status: 400, code: "invalid", message: "name is required" });
1844
1864
  }
@@ -3806,7 +3826,7 @@ var HttpTransport = class {
3806
3826
  });
3807
3827
  }
3808
3828
  listCategories(params, signal) {
3809
- return this.call({ method: "GET", path: "/v1/categories", query: { match: params.match }, signal });
3829
+ return this.call({ method: "GET", path: "/v1/categories", query: { ...params }, signal });
3810
3830
  }
3811
3831
  getCategory(categoryId, signal) {
3812
3832
  return this.call({ method: "GET", path: `/v1/categories/${encodeURIComponent(categoryId)}`, signal });
@@ -5315,8 +5335,8 @@ var Rules = class {
5315
5335
  /**
5316
5336
  * Save / edit a rule (append-only by supersession; D11). An agent-plane save is
5317
5337
  * ALWAYS project-layer: the saved rule's `rule_layer` is `project`, bound to the
5318
- * key's project. For org-layer house rules use learnFromReview with an authenticated human source; this method cannot author (`rule_layer:"org"`)
5319
- * rules in v1: that is a console/admin action.
5338
+ * key's project. For all authenticated reviewer feedback use learnFromReview at the intended
5339
+ * organization, project, or category scope. This method is project maintenance only.
5320
5340
  */
5321
5341
  save(req, signal) {
5322
5342
  return this.ctx.transport.saveRule(req, signal);
@@ -5593,7 +5613,7 @@ async function signWebhook(secret, body, timestampSeconds) {
5593
5613
  }
5594
5614
 
5595
5615
  // src/contract.ts
5596
- var CONTRACT_VERSION = "0.1.0-pre.8";
5616
+ var CONTRACT_VERSION = "0.1.0-pre.9";
5597
5617
  var CONTRACT_MANIFEST = {
5598
5618
  name: "extrovert.review-loop",
5599
5619
  version: CONTRACT_VERSION,