@extrovert.dev/sdk 0.1.0-pre.7 → 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.7` 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.7": 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.7`**, 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.7`: 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.7";
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}`;
@@ -938,6 +938,8 @@ ${parent.text}`;
938
938
  var MockBackend = class {
939
939
  constructor() {
940
940
  this.state = freshState();
941
+ /** Save / edit a rule (mock) - append-only by supersession (D11). */
942
+ this.learnedRules = /* @__PURE__ */ new Map();
941
943
  }
942
944
  reset() {
943
945
  this.state = freshState();
@@ -1819,8 +1821,21 @@ var MockBackend = class {
1819
1821
  * is a pure lexical filter (every token must appear in name+description) - NO LLM,
1820
1822
  * mirroring the server.
1821
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
+ }
1822
1837
  listCategories(params = {}) {
1823
- 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));
1824
1839
  const tokens = (params.match ?? "").trim().toLowerCase().split(/\s+/).filter(Boolean);
1825
1840
  if (tokens.length) {
1826
1841
  items = items.filter((c) => {
@@ -1828,15 +1843,22 @@ var MockBackend = class {
1828
1843
  return tokens.every((t) => hay.includes(t));
1829
1844
  });
1830
1845
  }
1831
- 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 };
1832
1852
  }
1833
1853
  /** Get one category (mock), or undefined when not found. */
1834
1854
  getCategory(categoryId) {
1835
- return this.state.categories.get(categoryId);
1855
+ const category = this.state.categories.get(categoryId);
1856
+ return category ? this.categoryUsage(category) : void 0;
1836
1857
  }
1837
1858
  /** Propose a category (mock): stands immediately, author_kind=agent (D9). */
1838
1859
  proposeCategory(req) {
1839
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." });
1840
1862
  if (!name) {
1841
1863
  throw new ValidationError({ status: 400, code: "invalid", message: "name is required" });
1842
1864
  }
@@ -2042,7 +2064,27 @@ var MockBackend = class {
2042
2064
  composition_token_expires_at: params.scope ? void 0 : new Date(Date.now() + 6e5).toISOString()
2043
2065
  };
2044
2066
  }
2045
- /** Save / edit a rule (mock) - append-only by supersession (D11). */
2067
+ learnReviewRule(reviewId, req) {
2068
+ const fingerprint = JSON.stringify({ reviewId, req });
2069
+ const prior = this.learnedRules.get(req.client_id);
2070
+ if (prior) {
2071
+ if (prior.fingerprint !== fingerprint) throw new ValidationError({ status: 409, code: "conflict", message: "learning retry identity changed" });
2072
+ return structuredClone(prior.result);
2073
+ }
2074
+ const turn = (this.state.reviewTurns.get(reviewId) ?? []).find((t) => t.id === req.source_turn_id);
2075
+ if (!turn || turn.actor_kind !== "human" || !turn.actor_id) throw new ValidationError({ status: 403, code: "forbidden_scope", message: "learning requires authenticated human feedback" });
2076
+ const rule = this.saveRule({ ...req, scope: req.target === "category" ? "category" : "general", source_review_id: reviewId, source_turn_id: turn.id });
2077
+ rule.source_turn_id = turn.id;
2078
+ rule.rule_layer = req.target === "org_house" ? "org" : "project";
2079
+ if (req.target === "org_house") delete rule.project_id;
2080
+ rule.source_review_id = reviewId;
2081
+ rule.source_turn_id = turn.id;
2082
+ const audit = [...this.state.ruleAudit.values()].find((entry) => entry.entity_id === rule.id);
2083
+ audit.after_json = ruleSnapshotJSON(rule);
2084
+ const result = { rule, source_review_id: reviewId, source_turn_id: turn.id, human_id: turn.actor_id, audit_id: audit.id, propagation: "queued" };
2085
+ this.learnedRules.set(req.client_id, { fingerprint, result: structuredClone(result) });
2086
+ return result;
2087
+ }
2046
2088
  saveRule(req) {
2047
2089
  const text = req.rule_text.trim();
2048
2090
  if (!text) {
@@ -3683,6 +3725,7 @@ var HttpTransport = class {
3683
3725
  state: Array.isArray(params.state) ? params.state.join(",") : params.state,
3684
3726
  category_id: params.category_id,
3685
3727
  inbox: params.inbox,
3728
+ composer: params.composer,
3686
3729
  limit: params.limit,
3687
3730
  page: params.page
3688
3731
  };
@@ -3765,10 +3808,12 @@ var HttpTransport = class {
3765
3808
  });
3766
3809
  }
3767
3810
  waitForReviewEvent(params, signal) {
3811
+ const waitSeconds = Math.min(55, Math.max(1, params.wait_seconds ?? 55));
3768
3812
  return this.call({
3769
3813
  method: "GET",
3770
3814
  path: "/v1/reviews/events/wait",
3771
- query: { review_id: params.review_id, limit: params.limit, wait_seconds: params.wait_seconds },
3815
+ query: { review_id: params.review_id, limit: params.limit, wait_seconds: waitSeconds },
3816
+ timeoutMs: (waitSeconds + 10) * 1e3,
3772
3817
  signal
3773
3818
  });
3774
3819
  }
@@ -3781,7 +3826,7 @@ var HttpTransport = class {
3781
3826
  });
3782
3827
  }
3783
3828
  listCategories(params, signal) {
3784
- 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 });
3785
3830
  }
3786
3831
  getCategory(categoryId, signal) {
3787
3832
  return this.call({ method: "GET", path: `/v1/categories/${encodeURIComponent(categoryId)}`, signal });
@@ -3837,6 +3882,9 @@ var HttpTransport = class {
3837
3882
  signal
3838
3883
  });
3839
3884
  }
3885
+ learnReviewRule(reviewId, req, signal) {
3886
+ return this.call({ method: "POST", path: `/v1/reviews/${encodeURIComponent(reviewId)}/learned-rules`, body: req, signal });
3887
+ }
3840
3888
  saveRule(req, signal) {
3841
3889
  return this.call({
3842
3890
  method: "PUT",
@@ -4244,6 +4292,9 @@ var MockTransport = class {
4244
4292
  async getRules(params) {
4245
4293
  return this.backend.getRules(params);
4246
4294
  }
4295
+ async learnReviewRule(reviewId, req) {
4296
+ return this.backend.learnReviewRule(reviewId, req);
4297
+ }
4247
4298
  async saveRule(req) {
4248
4299
  return this.backend.saveRule(req);
4249
4300
  }
@@ -5273,6 +5324,10 @@ var Rules = class {
5273
5324
  constructor(ctx) {
5274
5325
  this.ctx = ctx;
5275
5326
  }
5327
+ /** Learn category or organization house rules from verified human review feedback. */
5328
+ learnFromReview(reviewId, req, signal) {
5329
+ return this.ctx.transport.learnReviewRule(reviewId, req, signal);
5330
+ }
5276
5331
  /** Get the ORDERED active rule set (precedence ladder applied; NO LLM). */
5277
5332
  get(params = {}, signal) {
5278
5333
  return this.ctx.transport.getRules(params, signal);
@@ -5280,8 +5335,8 @@ var Rules = class {
5280
5335
  /**
5281
5336
  * Save / edit a rule (append-only by supersession; D11). An agent-plane save is
5282
5337
  * ALWAYS project-layer: the saved rule's `rule_layer` is `project`, bound to the
5283
- * key's project. Agents cannot author org-layer / house-style (`rule_layer:"org"`)
5284
- * 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.
5285
5340
  */
5286
5341
  save(req, signal) {
5287
5342
  return this.ctx.transport.saveRule(req, signal);
@@ -5558,7 +5613,7 @@ async function signWebhook(secret, body, timestampSeconds) {
5558
5613
  }
5559
5614
 
5560
5615
  // src/contract.ts
5561
- var CONTRACT_VERSION = "0.1.0-pre.7";
5616
+ var CONTRACT_VERSION = "0.1.0-pre.9";
5562
5617
  var CONTRACT_MANIFEST = {
5563
5618
  name: "extrovert.review-loop",
5564
5619
  version: CONTRACT_VERSION,
@@ -5589,6 +5644,8 @@ var CONTRACT_MANIFEST = {
5589
5644
  // realtime (M3)
5590
5645
  "ReviewEventReason",
5591
5646
  "ReviewEventsResult",
5647
+ "LearnReviewRuleRequest",
5648
+ "LearnedReviewRule",
5592
5649
  "ReviewEventCursor",
5593
5650
  // chat / revision / restamp (M5/M7)
5594
5651
  "PostReviewChatRequest",