@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 +4 -4
- package/dist/index.cjs +67 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +46 -9
- package/dist/index.d.ts +46 -9
- package/dist/index.js +67 -10
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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: {
|
|
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.
|
|
5284
|
-
*
|
|
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.
|
|
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",
|