plures 1.10.0 → 1.11.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/README.md +1 -0
- package/dist/chunk-WPSUIAVZ.js +205 -0
- package/dist/cli.cjs +80 -23
- package/dist/cli.js +70 -208
- package/dist/index.cjs +3 -1
- package/dist/index.d.cts +17 -1
- package/dist/index.d.ts +17 -1
- package/dist/index.js +6 -196
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -49,6 +49,7 @@ if (event.type === "task.completed" && event.data.result?.agreement === 1) {
|
|
|
49
49
|
- `new Plures(key, { app: "support-agent" })` or `PLURES_APP`: send everything to one of your apps. `plures.apps.list()`, `.create({ name, id? })`, `.retrieve(id)`, `.update(id, { name?, id?, default? })`, `.del(id)`. Each app keeps its own tasks, ledger view, map and quality.
|
|
50
50
|
- `plures.maps.guide()`, `.retrieve(app)`, `.upload(app, map)`, `.setCheckpoint(app, id, status)`: the agent map, where your agent needs a person. Or run `npx plures map` in your repo: your own coding agent reads the code and only the map is sent.
|
|
51
51
|
- `plures.maps.createKey(app, { label? })`, `.keys(app)`, `.revokeKey(app, id)`: map keys for CI, which can only read and upload that app's map. `.workflow(app, branch?)` returns the GitHub Action that maps on every push; `npx plures map workflow` writes it to `.github/workflows/plures-map.yml`. Or use **Map with GitHub** on the app's map page and Plures opens the pull request for you, then **Turn on mapping** merges it. `plures.maps.startRun(app)` and `.runs(app)`: in CI with no Claude key, `npx plures map` starts a run where Plures runs Claude (first map free, then Claude's cost plus 30%, at most $5).
|
|
52
|
+
- `npx plures map` shows each file as Claude Code reads it and sends the file names (never their contents) to Plures, so the map page shows the reading live and the map builds itself when it lands. `plures.maps.progress(app, { session, phase, events })` is the call behind it.
|
|
52
53
|
- `plures.github.repos()`, `.connect(app, "owner/repo")`, `.turnOn(app)`, `.status(app)`, `.disconnect(app)`: Map with GitHub from code. Plures sets the repo's map key secret and opens a pull request adding the workflow; `turnOn` merges it. Installing the Plures GitHub App needs a browser once (`install_url`). From a terminal: `npx plures map github connect`, then `npx plures map github on`.
|
|
53
54
|
- `plures.quality.sample({ rubric, output, input? })`: send every output; a share is graded by people against the rubric, with personal details taken out first. `plures.quality.rubrics.create({ name, question, options, sample_rate })`, `.retrieve(name)` (score by day, this week against the four before, drift), `.list()`, `.update()`, `.del()`. `plures.quality.compare({ release, input, a, b })` and `plures.quality.release(name)` for blind release checks. `plures.quality.export(rubric)` returns graded samples for evals or fine tuning.
|
|
54
55
|
- `plures.testMix.set({ unanimous: 70, split: 20, tie: 5, expired: 5 })`, `.get()`, `.clear()`: with a test key, makes a share of test tasks split, tie or expire at random so you can test routing to your team without changing your agent. A task can override it with `metadata.test_mix`, and `metadata.test_outcome` beats both.
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
var VERSION = "1.11.0";
|
|
3
|
+
var PluresError = class extends Error {
|
|
4
|
+
constructor(message, status, type) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.status = status;
|
|
7
|
+
this.type = type;
|
|
8
|
+
}
|
|
9
|
+
status;
|
|
10
|
+
type;
|
|
11
|
+
};
|
|
12
|
+
var Plures = class {
|
|
13
|
+
base;
|
|
14
|
+
apiKey;
|
|
15
|
+
/** The app tasks go to when they don't name one. From opts.app or PLURES_APP. */
|
|
16
|
+
app;
|
|
17
|
+
ua;
|
|
18
|
+
constructor(apiKey, opts = {}) {
|
|
19
|
+
const key = apiKey ?? (typeof process !== "undefined" ? process.env?.PLURES_API_KEY : void 0);
|
|
20
|
+
if (!key) throw new Error("Pass your Plures API key, or set PLURES_API_KEY. Get a free test key at https://plures.si/?for=developers#join");
|
|
21
|
+
this.apiKey = key;
|
|
22
|
+
this.ua = opts.userAgent ?? `plures-js/${VERSION}`;
|
|
23
|
+
this.app = opts.app ?? (typeof process !== "undefined" ? process.env?.PLURES_APP : void 0) ?? null;
|
|
24
|
+
this.base = (opts.baseUrl ?? (typeof process !== "undefined" ? process.env?.PLURES_BASE_URL : void 0) ?? "https://plures.si").replace(/\/$/, "");
|
|
25
|
+
}
|
|
26
|
+
async req(method, path, body, headers = {}) {
|
|
27
|
+
for (let attempt = 0; ; attempt++) {
|
|
28
|
+
const r = await fetch(this.base + path, {
|
|
29
|
+
method,
|
|
30
|
+
headers: { authorization: `Bearer ${this.apiKey}`, "user-agent": this.ua, ...body ? { "content-type": "application/json" } : {}, ...this.app ? { "plures-app": this.app } : {}, ...headers },
|
|
31
|
+
body: body ? JSON.stringify(body) : void 0
|
|
32
|
+
});
|
|
33
|
+
if (r.status >= 500 && attempt < 2) {
|
|
34
|
+
await sleep(500 * 2 ** attempt);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const data = await r.json().catch(() => ({}));
|
|
38
|
+
if (!r.ok) throw new PluresError(data?.error?.message ?? `HTTP ${r.status}`, r.status, data?.error?.type ?? "api_error");
|
|
39
|
+
return data;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
tasks = {
|
|
43
|
+
/** Sends a question to verified people. Pass an idempotency key to make retries safe. */
|
|
44
|
+
create: (params, opts = {}) => this.req("POST", "/api/v1/tasks", params, opts.idempotencyKey ? { "idempotency-key": opts.idempotencyKey } : {}),
|
|
45
|
+
retrieve: (id) => this.req("GET", `/api/v1/tasks/${encodeURIComponent(id)}`),
|
|
46
|
+
list: (params = {}) => {
|
|
47
|
+
const q = new URLSearchParams(Object.entries(params).filter(([, v]) => v != null).map(([k, v]) => [k, String(v)]));
|
|
48
|
+
return this.req("GET", `/api/v1/tasks${q.size ? `?${q}` : ""}`);
|
|
49
|
+
},
|
|
50
|
+
cancel: (id) => this.req("POST", `/api/v1/tasks/${encodeURIComponent(id)}/cancel`),
|
|
51
|
+
/** Polls until the task is settled. Prefer webhooks in production. */
|
|
52
|
+
waitFor: async (id, opts = {}) => {
|
|
53
|
+
const end = Date.now() + (opts.timeoutMs ?? 10 * 6e4);
|
|
54
|
+
for (; ; ) {
|
|
55
|
+
const t = await this.tasks.retrieve(id);
|
|
56
|
+
if (t.status !== "pending") return t;
|
|
57
|
+
if (Date.now() > end) return t;
|
|
58
|
+
await sleep(opts.intervalMs ?? 3e3);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Test keys only. How test tasks behave when they don't set metadata.test_outcome,
|
|
64
|
+
* so an agent running unchanged sends a share of decisions to your team.
|
|
65
|
+
* A single task can override it with metadata.test_mix.
|
|
66
|
+
*/
|
|
67
|
+
testMix = {
|
|
68
|
+
get: () => this.req("GET", "/api/v1/test_mix"),
|
|
69
|
+
/** For example { unanimous: 70, split: 20, tie: 5, expired: 5 }. Pass null to turn it off. */
|
|
70
|
+
set: (mix) => this.req("PUT", "/api/v1/test_mix", { mix }),
|
|
71
|
+
clear: () => this.req("DELETE", "/api/v1/test_mix")
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Where tasks go. set() replaces what you send (team, groups or rules) and keeps the rest.
|
|
75
|
+
* For example { groups: [{ name: "Finance", emails: ["ap@acme.com"] }], rules: [{ category: "refunds", route: "team", group: "Finance" }] }.
|
|
76
|
+
*/
|
|
77
|
+
routing = {
|
|
78
|
+
get: () => this.req("GET", "/api/v1/routing"),
|
|
79
|
+
set: (update) => this.req("PUT", "/api/v1/routing", update)
|
|
80
|
+
};
|
|
81
|
+
/** Your apps. Each agent you run can be its own app, with its own tasks, ledger view and map. */
|
|
82
|
+
apps = {
|
|
83
|
+
list: () => this.req("GET", "/api/v1/apps"),
|
|
84
|
+
/** { name: "Support agent" }, with an optional id like "support-agent". */
|
|
85
|
+
create: (params) => this.req("POST", "/api/v1/apps", params),
|
|
86
|
+
retrieve: (id) => this.req("GET", `/api/v1/apps/${encodeURIComponent(id)}`),
|
|
87
|
+
/** Rename it, change its id (its tasks move with it), or make it the default. */
|
|
88
|
+
update: (id, params) => this.req("PATCH", `/api/v1/apps/${encodeURIComponent(id)}`, params),
|
|
89
|
+
/** Only an app with no tasks can be deleted. */
|
|
90
|
+
del: (id) => this.req("DELETE", `/api/v1/apps/${encodeURIComponent(id)}`)
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* Agent maps: where each app's agent needs a person. Your coding agent reads your code and
|
|
94
|
+
* writes the map; Plures gets only the map. guide() returns the steps it follows.
|
|
95
|
+
*/
|
|
96
|
+
maps = {
|
|
97
|
+
guide: async () => {
|
|
98
|
+
const r = await fetch(`${this.base}/api/v1/map_guide`);
|
|
99
|
+
return r.text();
|
|
100
|
+
},
|
|
101
|
+
retrieve: (app) => this.req("GET", `/api/v1/apps/${encodeURIComponent(app)}/map`),
|
|
102
|
+
/** Uploads a new version. Checks you already added or dismissed keep that. */
|
|
103
|
+
upload: (app, map) => this.req("PUT", `/api/v1/apps/${encodeURIComponent(app)}/map`, map),
|
|
104
|
+
setCheckpoint: (app, id, status) => this.req("PATCH", `/api/v1/apps/${encodeURIComponent(app)}/map/checkpoints/${encodeURIComponent(id)}`, { status }),
|
|
105
|
+
/** A key that can only read and upload this app's map, for CI. The key is in the response once. */
|
|
106
|
+
createKey: (app, opts = {}) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app)}/map_keys`, opts),
|
|
107
|
+
keys: (app) => this.req("GET", `/api/v1/apps/${encodeURIComponent(app)}/map_keys`),
|
|
108
|
+
revokeKey: (app, id) => this.req("DELETE", `/api/v1/apps/${encodeURIComponent(app)}/map_keys/${encodeURIComponent(id)}`),
|
|
109
|
+
/** Starts a map run where Plures runs Claude (CI with no Anthropic key). npx plures map does this. */
|
|
110
|
+
startRun: (app) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app)}/map_runs`, {}),
|
|
111
|
+
runs: (app) => this.req("GET", `/api/v1/apps/${encodeURIComponent(app)}/map_runs`),
|
|
112
|
+
/** While mapping, which files the coding agent is reading, shown live on the map page. File names only. npx plures map does this. */
|
|
113
|
+
progress: (app, p) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app)}/map/progress`, p),
|
|
114
|
+
/** The GitHub Actions workflow that maps a repo on every push to its main branch. */
|
|
115
|
+
workflow: async (app, branch = "main") => {
|
|
116
|
+
const r = await fetch(`${this.base}/api/v1/map_workflow?app=${encodeURIComponent(app)}&branch=${encodeURIComponent(branch)}`);
|
|
117
|
+
return r.text();
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* Map with GitHub: Plures adds a workflow to your repo by pull request and keeps the app's map
|
|
122
|
+
* current on every push. Installing the Plures GitHub App needs a browser once (installUrl);
|
|
123
|
+
* everything after that works here.
|
|
124
|
+
*/
|
|
125
|
+
github = {
|
|
126
|
+
/** Repos the Plures GitHub App can reach. installed is false until you install it at install_url. */
|
|
127
|
+
repos: () => this.req("GET", "/api/v1/github/repos"),
|
|
128
|
+
/** Where an app's GitHub link stands: repo, pull request, mapping on, latest run, last map. */
|
|
129
|
+
status: (app) => this.req("GET", `/api/v1/apps/${encodeURIComponent(app)}/github`),
|
|
130
|
+
/** Connects a repo ("owner/name"): sets its map key secret and opens the pull request. */
|
|
131
|
+
connect: (app, repo) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app)}/github`, { repo }),
|
|
132
|
+
/** Turns mapping on by merging the pull request. The repo's own rules (reviews, checks) still apply. */
|
|
133
|
+
turnOn: (app) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app)}/github/merge`, {}),
|
|
134
|
+
/** Revokes the repo's map key. Delete the workflow file in the repo to stop runs. */
|
|
135
|
+
disconnect: (app) => this.req("DELETE", `/api/v1/apps/${encodeURIComponent(app)}/github`)
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
138
|
+
* Quality: people grade a sample of your agent's outputs against a rubric, so you see drift
|
|
139
|
+
* early, and compare two versions blind before you ship. Personal details are taken out first.
|
|
140
|
+
*/
|
|
141
|
+
quality = {
|
|
142
|
+
/** Send every output; the rubric's sample rate decides which ones people grade. */
|
|
143
|
+
sample: (p) => this.req("POST", "/api/v1/samples", p),
|
|
144
|
+
/** A blind release check: a is the current version's output, b the new one's, for the same input. */
|
|
145
|
+
compare: (p) => this.req("POST", "/api/v1/comparisons", p),
|
|
146
|
+
/** How often people preferred the new version, with a 95% range and a verdict. */
|
|
147
|
+
release: (name, opts = {}) => this.req("GET", `/api/v1/releases/${encodeURIComponent(name)}${opts.app ? `?app=${encodeURIComponent(opts.app)}` : ""}`),
|
|
148
|
+
rubrics: {
|
|
149
|
+
list: (opts = {}) => this.req("GET", `/api/v1/rubrics${opts.app ? `?app=${encodeURIComponent(opts.app)}` : ""}`),
|
|
150
|
+
/** { name: "reply_quality", question: "Is this reply correct and kind?", options: ["Good", "Okay", "Bad"], sample_rate: 0.05 } */
|
|
151
|
+
create: (p) => this.req("POST", "/api/v1/rubrics", p),
|
|
152
|
+
retrieve: (name, opts = {}) => this.req("GET", `/api/v1/rubrics/${encodeURIComponent(name)}${opts.app ? `?app=${encodeURIComponent(opts.app)}` : ""}`),
|
|
153
|
+
update: (name, p) => this.req("PATCH", `/api/v1/rubrics/${encodeURIComponent(name)}${p.app ? `?app=${encodeURIComponent(p.app)}` : ""}`, p),
|
|
154
|
+
del: (name, opts = {}) => this.req("DELETE", `/api/v1/rubrics/${encodeURIComponent(name)}${opts.app ? `?app=${encodeURIComponent(opts.app)}` : ""}`)
|
|
155
|
+
},
|
|
156
|
+
/** Graded samples as JSON lines (input, output, grade, score, agreement), for fine tuning or an eval set. */
|
|
157
|
+
export: async (rubric, opts = {}) => {
|
|
158
|
+
const q = new URLSearchParams(Object.entries({ app: opts.app ?? this.app ?? void 0, since: opts.since }).filter(([, v]) => v));
|
|
159
|
+
const r = await fetch(`${this.base}/api/v1/rubrics/${encodeURIComponent(rubric)}/export${q.size ? `?${q}` : ""}`, { headers: { authorization: `Bearer ${this.apiKey}`, "user-agent": this.ua } });
|
|
160
|
+
if (!r.ok) {
|
|
161
|
+
const d = await r.json().catch(() => ({}));
|
|
162
|
+
throw new PluresError(d?.error?.message ?? `HTTP ${r.status}`, r.status, d?.error?.type ?? "api_error");
|
|
163
|
+
}
|
|
164
|
+
return (await r.text()).split("\n").filter(Boolean).map((l) => JSON.parse(l));
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
/** Your prepaid credit. A live task that could cost more than available_cents is refused with a 402 insufficient_credits error. */
|
|
168
|
+
balance = () => this.req("GET", "/api/v1/balance");
|
|
169
|
+
/** Creates a task and waits for people to answer it. */
|
|
170
|
+
async verify(params, opts = {}) {
|
|
171
|
+
const t = await this.tasks.create(params, { idempotencyKey: opts.idempotencyKey });
|
|
172
|
+
return t.status === "pending" ? this.tasks.waitFor(t.id, { timeoutMs: opts.timeoutMs }) : t;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Checks the Plures-Signature header on a webhook and returns the parsed event.
|
|
176
|
+
* Pass the raw request body exactly as received. Throws if the signature is wrong
|
|
177
|
+
* or older than toleranceSec.
|
|
178
|
+
*/
|
|
179
|
+
static async verifyWebhook(rawBody, signatureHeader, secret, toleranceSec = 300) {
|
|
180
|
+
const parts = Object.fromEntries((signatureHeader ?? "").split(",").map((p) => p.split("=", 2)));
|
|
181
|
+
const t = Number(parts.t);
|
|
182
|
+
if (!t || !parts.v1) throw new Error("Missing Plures-Signature header");
|
|
183
|
+
if (Math.abs(Date.now() / 1e3 - t) > toleranceSec) throw new Error("Webhook timestamp is too old");
|
|
184
|
+
const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
185
|
+
const mac = new Uint8Array(await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(`${t}.${rawBody}`)));
|
|
186
|
+
const expected = Array.from(mac, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
187
|
+
if (!timingSafeEqual(expected, parts.v1)) throw new Error("Webhook signature does not match");
|
|
188
|
+
return JSON.parse(rawBody);
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
function timingSafeEqual(a, b) {
|
|
192
|
+
if (a.length !== b.length) return false;
|
|
193
|
+
let diff = 0;
|
|
194
|
+
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
195
|
+
return diff === 0;
|
|
196
|
+
}
|
|
197
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
198
|
+
var index_default = Plures;
|
|
199
|
+
|
|
200
|
+
export {
|
|
201
|
+
VERSION,
|
|
202
|
+
PluresError,
|
|
203
|
+
Plures,
|
|
204
|
+
index_default
|
|
205
|
+
};
|
package/dist/cli.cjs
CHANGED
|
@@ -3,18 +3,20 @@
|
|
|
3
3
|
|
|
4
4
|
// src/cli.ts
|
|
5
5
|
var import_node_child_process = require("child_process");
|
|
6
|
+
var import_node_crypto = require("crypto");
|
|
7
|
+
var import_node_path = require("path");
|
|
6
8
|
var import_node_fs = require("fs");
|
|
7
9
|
|
|
8
|
-
// src/index.
|
|
9
|
-
var VERSION = "1.
|
|
10
|
+
// src/index.ts
|
|
11
|
+
var VERSION = "1.11.0";
|
|
10
12
|
var PluresError = class extends Error {
|
|
11
|
-
status;
|
|
12
|
-
type;
|
|
13
13
|
constructor(message, status, type) {
|
|
14
14
|
super(message);
|
|
15
15
|
this.status = status;
|
|
16
16
|
this.type = type;
|
|
17
17
|
}
|
|
18
|
+
status;
|
|
19
|
+
type;
|
|
18
20
|
};
|
|
19
21
|
var Plures = class {
|
|
20
22
|
base;
|
|
@@ -24,8 +26,7 @@ var Plures = class {
|
|
|
24
26
|
ua;
|
|
25
27
|
constructor(apiKey, opts = {}) {
|
|
26
28
|
const key2 = apiKey ?? (typeof process !== "undefined" ? process.env?.PLURES_API_KEY : void 0);
|
|
27
|
-
if (!key2)
|
|
28
|
-
throw new Error("Pass your Plures API key, or set PLURES_API_KEY. Get a free test key at https://plures.si/?for=developers#join");
|
|
29
|
+
if (!key2) throw new Error("Pass your Plures API key, or set PLURES_API_KEY. Get a free test key at https://plures.si/?for=developers#join");
|
|
29
30
|
this.apiKey = key2;
|
|
30
31
|
this.ua = opts.userAgent ?? `plures-js/${VERSION}`;
|
|
31
32
|
this.app = opts.app ?? (typeof process !== "undefined" ? process.env?.PLURES_APP : void 0) ?? null;
|
|
@@ -43,8 +44,7 @@ var Plures = class {
|
|
|
43
44
|
continue;
|
|
44
45
|
}
|
|
45
46
|
const data = await r.json().catch(() => ({}));
|
|
46
|
-
if (!r.ok)
|
|
47
|
-
throw new PluresError(data?.error?.message ?? `HTTP ${r.status}`, r.status, data?.error?.type ?? "api_error");
|
|
47
|
+
if (!r.ok) throw new PluresError(data?.error?.message ?? `HTTP ${r.status}`, r.status, data?.error?.type ?? "api_error");
|
|
48
48
|
return data;
|
|
49
49
|
}
|
|
50
50
|
}
|
|
@@ -62,10 +62,8 @@ var Plures = class {
|
|
|
62
62
|
const end = Date.now() + (opts.timeoutMs ?? 10 * 6e4);
|
|
63
63
|
for (; ; ) {
|
|
64
64
|
const t = await this.tasks.retrieve(id);
|
|
65
|
-
if (t.status !== "pending")
|
|
66
|
-
|
|
67
|
-
if (Date.now() > end)
|
|
68
|
-
return t;
|
|
65
|
+
if (t.status !== "pending") return t;
|
|
66
|
+
if (Date.now() > end) return t;
|
|
69
67
|
await sleep(opts.intervalMs ?? 3e3);
|
|
70
68
|
}
|
|
71
69
|
}
|
|
@@ -120,6 +118,8 @@ var Plures = class {
|
|
|
120
118
|
/** Starts a map run where Plures runs Claude (CI with no Anthropic key). npx plures map does this. */
|
|
121
119
|
startRun: (app2) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app2)}/map_runs`, {}),
|
|
122
120
|
runs: (app2) => this.req("GET", `/api/v1/apps/${encodeURIComponent(app2)}/map_runs`),
|
|
121
|
+
/** While mapping, which files the coding agent is reading, shown live on the map page. File names only. npx plures map does this. */
|
|
122
|
+
progress: (app2, p) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app2)}/map/progress`, p),
|
|
123
123
|
/** The GitHub Actions workflow that maps a repo on every push to its main branch. */
|
|
124
124
|
workflow: async (app2, branch2 = "main") => {
|
|
125
125
|
const r = await fetch(`${this.base}/api/v1/map_workflow?app=${encodeURIComponent(app2)}&branch=${encodeURIComponent(branch2)}`);
|
|
@@ -188,24 +188,19 @@ var Plures = class {
|
|
|
188
188
|
static async verifyWebhook(rawBody, signatureHeader, secret, toleranceSec = 300) {
|
|
189
189
|
const parts = Object.fromEntries((signatureHeader ?? "").split(",").map((p) => p.split("=", 2)));
|
|
190
190
|
const t = Number(parts.t);
|
|
191
|
-
if (!t || !parts.v1)
|
|
192
|
-
|
|
193
|
-
if (Math.abs(Date.now() / 1e3 - t) > toleranceSec)
|
|
194
|
-
throw new Error("Webhook timestamp is too old");
|
|
191
|
+
if (!t || !parts.v1) throw new Error("Missing Plures-Signature header");
|
|
192
|
+
if (Math.abs(Date.now() / 1e3 - t) > toleranceSec) throw new Error("Webhook timestamp is too old");
|
|
195
193
|
const key2 = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
196
194
|
const mac = new Uint8Array(await crypto.subtle.sign("HMAC", key2, new TextEncoder().encode(`${t}.${rawBody}`)));
|
|
197
195
|
const expected = Array.from(mac, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
198
|
-
if (!timingSafeEqual(expected, parts.v1))
|
|
199
|
-
throw new Error("Webhook signature does not match");
|
|
196
|
+
if (!timingSafeEqual(expected, parts.v1)) throw new Error("Webhook signature does not match");
|
|
200
197
|
return JSON.parse(rawBody);
|
|
201
198
|
}
|
|
202
199
|
};
|
|
203
200
|
function timingSafeEqual(a, b) {
|
|
204
|
-
if (a.length !== b.length)
|
|
205
|
-
return false;
|
|
201
|
+
if (a.length !== b.length) return false;
|
|
206
202
|
let diff = 0;
|
|
207
|
-
for (let i = 0; i < a.length; i++)
|
|
208
|
-
diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
203
|
+
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
209
204
|
return diff === 0;
|
|
210
205
|
}
|
|
211
206
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -309,11 +304,73 @@ With plures-mcp connected, just ask: ${cyan("Map this app for Plures")}`);
|
|
|
309
304
|
const prompt = `${guide}
|
|
310
305
|
|
|
311
306
|
Write the map to ${FILE} in the current directory now. Read the code; don't run it or change anything else.`;
|
|
307
|
+
const live = key ? client() : null;
|
|
308
|
+
const session = (0, import_node_crypto.randomBytes)(12).toString("base64url");
|
|
309
|
+
const source = process.env.GITHUB_ACTIONS ? "github" : "cli";
|
|
310
|
+
let queue = [], phase = "reading", sent = "", reads = 0;
|
|
311
|
+
const flush = async () => {
|
|
312
|
+
if (!live) return;
|
|
313
|
+
const events = queue;
|
|
314
|
+
queue = [];
|
|
315
|
+
if (!events.length && sent === phase) return;
|
|
316
|
+
sent = phase;
|
|
317
|
+
await live.maps.progress(app, { session, phase, source, events }).catch(() => {
|
|
318
|
+
});
|
|
319
|
+
};
|
|
320
|
+
const tick = setInterval(() => {
|
|
321
|
+
void flush();
|
|
322
|
+
}, 900);
|
|
323
|
+
void flush();
|
|
324
|
+
const rel = (p) => {
|
|
325
|
+
if (typeof p !== "string" || !p) return null;
|
|
326
|
+
const r = (0, import_node_path.isAbsolute)(p) ? (0, import_node_path.relative)(process.cwd(), p) : p;
|
|
327
|
+
return r.startsWith("..") ? null : r.replace(/\\/g, "/");
|
|
328
|
+
};
|
|
329
|
+
let result = "";
|
|
312
330
|
const code = await new Promise((resolve) => {
|
|
313
|
-
const p = (0, import_node_child_process.spawn)("claude", ["-p", prompt, "--allowedTools", "Read,Grep,Glob,Write,Bash(git rev-parse:*),Bash(git remote:*)"], { stdio: ["ignore", "
|
|
331
|
+
const p = (0, import_node_child_process.spawn)("claude", ["-p", prompt, "--output-format", "stream-json", "--verbose", "--allowedTools", "Read,Grep,Glob,Write,Bash(git rev-parse:*),Bash(git remote:*)"], { stdio: ["ignore", "pipe", "inherit"], env: childEnv });
|
|
332
|
+
let buf = "";
|
|
333
|
+
p.stdout.on("data", (chunk) => {
|
|
334
|
+
buf += chunk.toString();
|
|
335
|
+
let i;
|
|
336
|
+
while ((i = buf.indexOf("\n")) >= 0) {
|
|
337
|
+
const line = buf.slice(0, i);
|
|
338
|
+
buf = buf.slice(i + 1);
|
|
339
|
+
let ev;
|
|
340
|
+
try {
|
|
341
|
+
ev = JSON.parse(line);
|
|
342
|
+
} catch {
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
if (ev.type === "result" && typeof ev.result === "string") result = ev.result;
|
|
346
|
+
for (const b of ev.type === "assistant" ? ev.message?.content ?? [] : []) {
|
|
347
|
+
if (b.type !== "tool_use") continue;
|
|
348
|
+
if (b.name === "Read") {
|
|
349
|
+
const f = rel(b.input?.file_path);
|
|
350
|
+
if (!f) continue;
|
|
351
|
+
reads++;
|
|
352
|
+
queue.push({ path: f, tool: "Read" });
|
|
353
|
+
console.log(` ${dim(String(reads).padStart(3))} ${dim("reading")} ${f}`);
|
|
354
|
+
} else if (b.name === "Write" && String(b.input?.file_path ?? "").endsWith(FILE)) {
|
|
355
|
+
phase = "drawing";
|
|
356
|
+
console.log(`
|
|
357
|
+
${violet("drawing")} the map`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
});
|
|
314
362
|
p.on("close", (n) => resolve(n ?? 1));
|
|
315
363
|
p.on("error", () => resolve(1));
|
|
316
364
|
});
|
|
365
|
+
clearInterval(tick);
|
|
366
|
+
if (result) console.log(`
|
|
367
|
+
${result.trim()}
|
|
368
|
+
`);
|
|
369
|
+
const ok = code === 0 && (0, import_node_fs.existsSync)(FILE) && (0, import_node_fs.statSync)(FILE).mtimeMs !== before;
|
|
370
|
+
if (!ok) {
|
|
371
|
+
phase = "failed";
|
|
372
|
+
await flush();
|
|
373
|
+
} else await flush();
|
|
317
374
|
if (code !== 0 || !(0, import_node_fs.existsSync)(FILE) || (0, import_node_fs.statSync)(FILE).mtimeMs === before) die(`Claude Code didn't write ${FILE}. Ask it directly, then run npx plures map upload.`);
|
|
318
375
|
await upload(FILE);
|
|
319
376
|
}
|
package/dist/cli.js
CHANGED
|
@@ -1,215 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
Plures,
|
|
4
|
+
PluresError,
|
|
5
|
+
VERSION
|
|
6
|
+
} from "./chunk-WPSUIAVZ.js";
|
|
2
7
|
|
|
3
8
|
// src/cli.ts
|
|
4
9
|
import { spawnSync, spawn } from "child_process";
|
|
10
|
+
import { randomBytes } from "crypto";
|
|
11
|
+
import { relative, isAbsolute } from "path";
|
|
5
12
|
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "fs";
|
|
6
|
-
|
|
7
|
-
// src/index.js
|
|
8
|
-
var VERSION = "1.9.1";
|
|
9
|
-
var PluresError = class extends Error {
|
|
10
|
-
status;
|
|
11
|
-
type;
|
|
12
|
-
constructor(message, status, type) {
|
|
13
|
-
super(message);
|
|
14
|
-
this.status = status;
|
|
15
|
-
this.type = type;
|
|
16
|
-
}
|
|
17
|
-
};
|
|
18
|
-
var Plures = class {
|
|
19
|
-
base;
|
|
20
|
-
apiKey;
|
|
21
|
-
/** The app tasks go to when they don't name one. From opts.app or PLURES_APP. */
|
|
22
|
-
app;
|
|
23
|
-
ua;
|
|
24
|
-
constructor(apiKey, opts = {}) {
|
|
25
|
-
const key2 = apiKey ?? (typeof process !== "undefined" ? process.env?.PLURES_API_KEY : void 0);
|
|
26
|
-
if (!key2)
|
|
27
|
-
throw new Error("Pass your Plures API key, or set PLURES_API_KEY. Get a free test key at https://plures.si/?for=developers#join");
|
|
28
|
-
this.apiKey = key2;
|
|
29
|
-
this.ua = opts.userAgent ?? `plures-js/${VERSION}`;
|
|
30
|
-
this.app = opts.app ?? (typeof process !== "undefined" ? process.env?.PLURES_APP : void 0) ?? null;
|
|
31
|
-
this.base = (opts.baseUrl ?? (typeof process !== "undefined" ? process.env?.PLURES_BASE_URL : void 0) ?? "https://plures.si").replace(/\/$/, "");
|
|
32
|
-
}
|
|
33
|
-
async req(method, path, body, headers = {}) {
|
|
34
|
-
for (let attempt = 0; ; attempt++) {
|
|
35
|
-
const r = await fetch(this.base + path, {
|
|
36
|
-
method,
|
|
37
|
-
headers: { authorization: `Bearer ${this.apiKey}`, "user-agent": this.ua, ...body ? { "content-type": "application/json" } : {}, ...this.app ? { "plures-app": this.app } : {}, ...headers },
|
|
38
|
-
body: body ? JSON.stringify(body) : void 0
|
|
39
|
-
});
|
|
40
|
-
if (r.status >= 500 && attempt < 2) {
|
|
41
|
-
await sleep(500 * 2 ** attempt);
|
|
42
|
-
continue;
|
|
43
|
-
}
|
|
44
|
-
const data = await r.json().catch(() => ({}));
|
|
45
|
-
if (!r.ok)
|
|
46
|
-
throw new PluresError(data?.error?.message ?? `HTTP ${r.status}`, r.status, data?.error?.type ?? "api_error");
|
|
47
|
-
return data;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
tasks = {
|
|
51
|
-
/** Sends a question to verified people. Pass an idempotency key to make retries safe. */
|
|
52
|
-
create: (params, opts = {}) => this.req("POST", "/api/v1/tasks", params, opts.idempotencyKey ? { "idempotency-key": opts.idempotencyKey } : {}),
|
|
53
|
-
retrieve: (id) => this.req("GET", `/api/v1/tasks/${encodeURIComponent(id)}`),
|
|
54
|
-
list: (params = {}) => {
|
|
55
|
-
const q = new URLSearchParams(Object.entries(params).filter(([, v]) => v != null).map(([k, v]) => [k, String(v)]));
|
|
56
|
-
return this.req("GET", `/api/v1/tasks${q.size ? `?${q}` : ""}`);
|
|
57
|
-
},
|
|
58
|
-
cancel: (id) => this.req("POST", `/api/v1/tasks/${encodeURIComponent(id)}/cancel`),
|
|
59
|
-
/** Polls until the task is settled. Prefer webhooks in production. */
|
|
60
|
-
waitFor: async (id, opts = {}) => {
|
|
61
|
-
const end = Date.now() + (opts.timeoutMs ?? 10 * 6e4);
|
|
62
|
-
for (; ; ) {
|
|
63
|
-
const t = await this.tasks.retrieve(id);
|
|
64
|
-
if (t.status !== "pending")
|
|
65
|
-
return t;
|
|
66
|
-
if (Date.now() > end)
|
|
67
|
-
return t;
|
|
68
|
-
await sleep(opts.intervalMs ?? 3e3);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
/**
|
|
73
|
-
* Test keys only. How test tasks behave when they don't set metadata.test_outcome,
|
|
74
|
-
* so an agent running unchanged sends a share of decisions to your team.
|
|
75
|
-
* A single task can override it with metadata.test_mix.
|
|
76
|
-
*/
|
|
77
|
-
testMix = {
|
|
78
|
-
get: () => this.req("GET", "/api/v1/test_mix"),
|
|
79
|
-
/** For example { unanimous: 70, split: 20, tie: 5, expired: 5 }. Pass null to turn it off. */
|
|
80
|
-
set: (mix) => this.req("PUT", "/api/v1/test_mix", { mix }),
|
|
81
|
-
clear: () => this.req("DELETE", "/api/v1/test_mix")
|
|
82
|
-
};
|
|
83
|
-
/**
|
|
84
|
-
* Where tasks go. set() replaces what you send (team, groups or rules) and keeps the rest.
|
|
85
|
-
* For example { groups: [{ name: "Finance", emails: ["ap@acme.com"] }], rules: [{ category: "refunds", route: "team", group: "Finance" }] }.
|
|
86
|
-
*/
|
|
87
|
-
routing = {
|
|
88
|
-
get: () => this.req("GET", "/api/v1/routing"),
|
|
89
|
-
set: (update) => this.req("PUT", "/api/v1/routing", update)
|
|
90
|
-
};
|
|
91
|
-
/** Your apps. Each agent you run can be its own app, with its own tasks, ledger view and map. */
|
|
92
|
-
apps = {
|
|
93
|
-
list: () => this.req("GET", "/api/v1/apps"),
|
|
94
|
-
/** { name: "Support agent" }, with an optional id like "support-agent". */
|
|
95
|
-
create: (params) => this.req("POST", "/api/v1/apps", params),
|
|
96
|
-
retrieve: (id) => this.req("GET", `/api/v1/apps/${encodeURIComponent(id)}`),
|
|
97
|
-
/** Rename it, change its id (its tasks move with it), or make it the default. */
|
|
98
|
-
update: (id, params) => this.req("PATCH", `/api/v1/apps/${encodeURIComponent(id)}`, params),
|
|
99
|
-
/** Only an app with no tasks can be deleted. */
|
|
100
|
-
del: (id) => this.req("DELETE", `/api/v1/apps/${encodeURIComponent(id)}`)
|
|
101
|
-
};
|
|
102
|
-
/**
|
|
103
|
-
* Agent maps: where each app's agent needs a person. Your coding agent reads your code and
|
|
104
|
-
* writes the map; Plures gets only the map. guide() returns the steps it follows.
|
|
105
|
-
*/
|
|
106
|
-
maps = {
|
|
107
|
-
guide: async () => {
|
|
108
|
-
const r = await fetch(`${this.base}/api/v1/map_guide`);
|
|
109
|
-
return r.text();
|
|
110
|
-
},
|
|
111
|
-
retrieve: (app2) => this.req("GET", `/api/v1/apps/${encodeURIComponent(app2)}/map`),
|
|
112
|
-
/** Uploads a new version. Checks you already added or dismissed keep that. */
|
|
113
|
-
upload: (app2, map2) => this.req("PUT", `/api/v1/apps/${encodeURIComponent(app2)}/map`, map2),
|
|
114
|
-
setCheckpoint: (app2, id, status) => this.req("PATCH", `/api/v1/apps/${encodeURIComponent(app2)}/map/checkpoints/${encodeURIComponent(id)}`, { status }),
|
|
115
|
-
/** A key that can only read and upload this app's map, for CI. The key is in the response once. */
|
|
116
|
-
createKey: (app2, opts = {}) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app2)}/map_keys`, opts),
|
|
117
|
-
keys: (app2) => this.req("GET", `/api/v1/apps/${encodeURIComponent(app2)}/map_keys`),
|
|
118
|
-
revokeKey: (app2, id) => this.req("DELETE", `/api/v1/apps/${encodeURIComponent(app2)}/map_keys/${encodeURIComponent(id)}`),
|
|
119
|
-
/** Starts a map run where Plures runs Claude (CI with no Anthropic key). npx plures map does this. */
|
|
120
|
-
startRun: (app2) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app2)}/map_runs`, {}),
|
|
121
|
-
runs: (app2) => this.req("GET", `/api/v1/apps/${encodeURIComponent(app2)}/map_runs`),
|
|
122
|
-
/** The GitHub Actions workflow that maps a repo on every push to its main branch. */
|
|
123
|
-
workflow: async (app2, branch2 = "main") => {
|
|
124
|
-
const r = await fetch(`${this.base}/api/v1/map_workflow?app=${encodeURIComponent(app2)}&branch=${encodeURIComponent(branch2)}`);
|
|
125
|
-
return r.text();
|
|
126
|
-
}
|
|
127
|
-
};
|
|
128
|
-
/**
|
|
129
|
-
* Map with GitHub: Plures adds a workflow to your repo by pull request and keeps the app's map
|
|
130
|
-
* current on every push. Installing the Plures GitHub App needs a browser once (installUrl);
|
|
131
|
-
* everything after that works here.
|
|
132
|
-
*/
|
|
133
|
-
github = {
|
|
134
|
-
/** Repos the Plures GitHub App can reach. installed is false until you install it at install_url. */
|
|
135
|
-
repos: () => this.req("GET", "/api/v1/github/repos"),
|
|
136
|
-
/** Where an app's GitHub link stands: repo, pull request, mapping on, latest run, last map. */
|
|
137
|
-
status: (app2) => this.req("GET", `/api/v1/apps/${encodeURIComponent(app2)}/github`),
|
|
138
|
-
/** Connects a repo ("owner/name"): sets its map key secret and opens the pull request. */
|
|
139
|
-
connect: (app2, repo) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app2)}/github`, { repo }),
|
|
140
|
-
/** Turns mapping on by merging the pull request. The repo's own rules (reviews, checks) still apply. */
|
|
141
|
-
turnOn: (app2) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app2)}/github/merge`, {}),
|
|
142
|
-
/** Revokes the repo's map key. Delete the workflow file in the repo to stop runs. */
|
|
143
|
-
disconnect: (app2) => this.req("DELETE", `/api/v1/apps/${encodeURIComponent(app2)}/github`)
|
|
144
|
-
};
|
|
145
|
-
/**
|
|
146
|
-
* Quality: people grade a sample of your agent's outputs against a rubric, so you see drift
|
|
147
|
-
* early, and compare two versions blind before you ship. Personal details are taken out first.
|
|
148
|
-
*/
|
|
149
|
-
quality = {
|
|
150
|
-
/** Send every output; the rubric's sample rate decides which ones people grade. */
|
|
151
|
-
sample: (p) => this.req("POST", "/api/v1/samples", p),
|
|
152
|
-
/** A blind release check: a is the current version's output, b the new one's, for the same input. */
|
|
153
|
-
compare: (p) => this.req("POST", "/api/v1/comparisons", p),
|
|
154
|
-
/** How often people preferred the new version, with a 95% range and a verdict. */
|
|
155
|
-
release: (name, opts = {}) => this.req("GET", `/api/v1/releases/${encodeURIComponent(name)}${opts.app ? `?app=${encodeURIComponent(opts.app)}` : ""}`),
|
|
156
|
-
rubrics: {
|
|
157
|
-
list: (opts = {}) => this.req("GET", `/api/v1/rubrics${opts.app ? `?app=${encodeURIComponent(opts.app)}` : ""}`),
|
|
158
|
-
/** { name: "reply_quality", question: "Is this reply correct and kind?", options: ["Good", "Okay", "Bad"], sample_rate: 0.05 } */
|
|
159
|
-
create: (p) => this.req("POST", "/api/v1/rubrics", p),
|
|
160
|
-
retrieve: (name, opts = {}) => this.req("GET", `/api/v1/rubrics/${encodeURIComponent(name)}${opts.app ? `?app=${encodeURIComponent(opts.app)}` : ""}`),
|
|
161
|
-
update: (name, p) => this.req("PATCH", `/api/v1/rubrics/${encodeURIComponent(name)}${p.app ? `?app=${encodeURIComponent(p.app)}` : ""}`, p),
|
|
162
|
-
del: (name, opts = {}) => this.req("DELETE", `/api/v1/rubrics/${encodeURIComponent(name)}${opts.app ? `?app=${encodeURIComponent(opts.app)}` : ""}`)
|
|
163
|
-
},
|
|
164
|
-
/** Graded samples as JSON lines (input, output, grade, score, agreement), for fine tuning or an eval set. */
|
|
165
|
-
export: async (rubric, opts = {}) => {
|
|
166
|
-
const q = new URLSearchParams(Object.entries({ app: opts.app ?? this.app ?? void 0, since: opts.since }).filter(([, v]) => v));
|
|
167
|
-
const r = await fetch(`${this.base}/api/v1/rubrics/${encodeURIComponent(rubric)}/export${q.size ? `?${q}` : ""}`, { headers: { authorization: `Bearer ${this.apiKey}`, "user-agent": this.ua } });
|
|
168
|
-
if (!r.ok) {
|
|
169
|
-
const d = await r.json().catch(() => ({}));
|
|
170
|
-
throw new PluresError(d?.error?.message ?? `HTTP ${r.status}`, r.status, d?.error?.type ?? "api_error");
|
|
171
|
-
}
|
|
172
|
-
return (await r.text()).split("\n").filter(Boolean).map((l) => JSON.parse(l));
|
|
173
|
-
}
|
|
174
|
-
};
|
|
175
|
-
/** Your prepaid credit. A live task that could cost more than available_cents is refused with a 402 insufficient_credits error. */
|
|
176
|
-
balance = () => this.req("GET", "/api/v1/balance");
|
|
177
|
-
/** Creates a task and waits for people to answer it. */
|
|
178
|
-
async verify(params, opts = {}) {
|
|
179
|
-
const t = await this.tasks.create(params, { idempotencyKey: opts.idempotencyKey });
|
|
180
|
-
return t.status === "pending" ? this.tasks.waitFor(t.id, { timeoutMs: opts.timeoutMs }) : t;
|
|
181
|
-
}
|
|
182
|
-
/**
|
|
183
|
-
* Checks the Plures-Signature header on a webhook and returns the parsed event.
|
|
184
|
-
* Pass the raw request body exactly as received. Throws if the signature is wrong
|
|
185
|
-
* or older than toleranceSec.
|
|
186
|
-
*/
|
|
187
|
-
static async verifyWebhook(rawBody, signatureHeader, secret, toleranceSec = 300) {
|
|
188
|
-
const parts = Object.fromEntries((signatureHeader ?? "").split(",").map((p) => p.split("=", 2)));
|
|
189
|
-
const t = Number(parts.t);
|
|
190
|
-
if (!t || !parts.v1)
|
|
191
|
-
throw new Error("Missing Plures-Signature header");
|
|
192
|
-
if (Math.abs(Date.now() / 1e3 - t) > toleranceSec)
|
|
193
|
-
throw new Error("Webhook timestamp is too old");
|
|
194
|
-
const key2 = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
195
|
-
const mac = new Uint8Array(await crypto.subtle.sign("HMAC", key2, new TextEncoder().encode(`${t}.${rawBody}`)));
|
|
196
|
-
const expected = Array.from(mac, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
197
|
-
if (!timingSafeEqual(expected, parts.v1))
|
|
198
|
-
throw new Error("Webhook signature does not match");
|
|
199
|
-
return JSON.parse(rawBody);
|
|
200
|
-
}
|
|
201
|
-
};
|
|
202
|
-
function timingSafeEqual(a, b) {
|
|
203
|
-
if (a.length !== b.length)
|
|
204
|
-
return false;
|
|
205
|
-
let diff = 0;
|
|
206
|
-
for (let i = 0; i < a.length; i++)
|
|
207
|
-
diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
208
|
-
return diff === 0;
|
|
209
|
-
}
|
|
210
|
-
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
211
|
-
|
|
212
|
-
// src/cli.ts
|
|
213
13
|
var argv = process.argv.slice(2);
|
|
214
14
|
var flag = (name) => {
|
|
215
15
|
const i = argv.indexOf(`--${name}`);
|
|
@@ -308,11 +108,73 @@ With plures-mcp connected, just ask: ${cyan("Map this app for Plures")}`);
|
|
|
308
108
|
const prompt = `${guide}
|
|
309
109
|
|
|
310
110
|
Write the map to ${FILE} in the current directory now. Read the code; don't run it or change anything else.`;
|
|
111
|
+
const live = key ? client() : null;
|
|
112
|
+
const session = randomBytes(12).toString("base64url");
|
|
113
|
+
const source = process.env.GITHUB_ACTIONS ? "github" : "cli";
|
|
114
|
+
let queue = [], phase = "reading", sent = "", reads = 0;
|
|
115
|
+
const flush = async () => {
|
|
116
|
+
if (!live) return;
|
|
117
|
+
const events = queue;
|
|
118
|
+
queue = [];
|
|
119
|
+
if (!events.length && sent === phase) return;
|
|
120
|
+
sent = phase;
|
|
121
|
+
await live.maps.progress(app, { session, phase, source, events }).catch(() => {
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
const tick = setInterval(() => {
|
|
125
|
+
void flush();
|
|
126
|
+
}, 900);
|
|
127
|
+
void flush();
|
|
128
|
+
const rel = (p) => {
|
|
129
|
+
if (typeof p !== "string" || !p) return null;
|
|
130
|
+
const r = isAbsolute(p) ? relative(process.cwd(), p) : p;
|
|
131
|
+
return r.startsWith("..") ? null : r.replace(/\\/g, "/");
|
|
132
|
+
};
|
|
133
|
+
let result = "";
|
|
311
134
|
const code = await new Promise((resolve) => {
|
|
312
|
-
const p = spawn("claude", ["-p", prompt, "--allowedTools", "Read,Grep,Glob,Write,Bash(git rev-parse:*),Bash(git remote:*)"], { stdio: ["ignore", "
|
|
135
|
+
const p = spawn("claude", ["-p", prompt, "--output-format", "stream-json", "--verbose", "--allowedTools", "Read,Grep,Glob,Write,Bash(git rev-parse:*),Bash(git remote:*)"], { stdio: ["ignore", "pipe", "inherit"], env: childEnv });
|
|
136
|
+
let buf = "";
|
|
137
|
+
p.stdout.on("data", (chunk) => {
|
|
138
|
+
buf += chunk.toString();
|
|
139
|
+
let i;
|
|
140
|
+
while ((i = buf.indexOf("\n")) >= 0) {
|
|
141
|
+
const line = buf.slice(0, i);
|
|
142
|
+
buf = buf.slice(i + 1);
|
|
143
|
+
let ev;
|
|
144
|
+
try {
|
|
145
|
+
ev = JSON.parse(line);
|
|
146
|
+
} catch {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (ev.type === "result" && typeof ev.result === "string") result = ev.result;
|
|
150
|
+
for (const b of ev.type === "assistant" ? ev.message?.content ?? [] : []) {
|
|
151
|
+
if (b.type !== "tool_use") continue;
|
|
152
|
+
if (b.name === "Read") {
|
|
153
|
+
const f = rel(b.input?.file_path);
|
|
154
|
+
if (!f) continue;
|
|
155
|
+
reads++;
|
|
156
|
+
queue.push({ path: f, tool: "Read" });
|
|
157
|
+
console.log(` ${dim(String(reads).padStart(3))} ${dim("reading")} ${f}`);
|
|
158
|
+
} else if (b.name === "Write" && String(b.input?.file_path ?? "").endsWith(FILE)) {
|
|
159
|
+
phase = "drawing";
|
|
160
|
+
console.log(`
|
|
161
|
+
${violet("drawing")} the map`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
});
|
|
313
166
|
p.on("close", (n) => resolve(n ?? 1));
|
|
314
167
|
p.on("error", () => resolve(1));
|
|
315
168
|
});
|
|
169
|
+
clearInterval(tick);
|
|
170
|
+
if (result) console.log(`
|
|
171
|
+
${result.trim()}
|
|
172
|
+
`);
|
|
173
|
+
const ok = code === 0 && existsSync(FILE) && statSync(FILE).mtimeMs !== before;
|
|
174
|
+
if (!ok) {
|
|
175
|
+
phase = "failed";
|
|
176
|
+
await flush();
|
|
177
|
+
} else await flush();
|
|
316
178
|
if (code !== 0 || !existsSync(FILE) || statSync(FILE).mtimeMs === before) die(`Claude Code didn't write ${FILE}. Ask it directly, then run npx plures map upload.`);
|
|
317
179
|
await upload(FILE);
|
|
318
180
|
}
|
package/dist/index.cjs
CHANGED
|
@@ -26,7 +26,7 @@ __export(index_exports, {
|
|
|
26
26
|
default: () => index_default
|
|
27
27
|
});
|
|
28
28
|
module.exports = __toCommonJS(index_exports);
|
|
29
|
-
var VERSION = "1.
|
|
29
|
+
var VERSION = "1.11.0";
|
|
30
30
|
var PluresError = class extends Error {
|
|
31
31
|
constructor(message, status, type) {
|
|
32
32
|
super(message);
|
|
@@ -136,6 +136,8 @@ var Plures = class {
|
|
|
136
136
|
/** Starts a map run where Plures runs Claude (CI with no Anthropic key). npx plures map does this. */
|
|
137
137
|
startRun: (app) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app)}/map_runs`, {}),
|
|
138
138
|
runs: (app) => this.req("GET", `/api/v1/apps/${encodeURIComponent(app)}/map_runs`),
|
|
139
|
+
/** While mapping, which files the coding agent is reading, shown live on the map page. File names only. npx plures map does this. */
|
|
140
|
+
progress: (app, p) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app)}/map/progress`, p),
|
|
139
141
|
/** The GitHub Actions workflow that maps a repo on every push to its main branch. */
|
|
140
142
|
workflow: async (app, branch = "main") => {
|
|
141
143
|
const r = await fetch(`${this.base}/api/v1/map_workflow?app=${encodeURIComponent(app)}&branch=${encodeURIComponent(branch)}`);
|
package/dist/index.d.cts
CHANGED
|
@@ -350,7 +350,7 @@ type WebhookEvent = {
|
|
|
350
350
|
created_at: string;
|
|
351
351
|
data: Task;
|
|
352
352
|
};
|
|
353
|
-
declare const VERSION = "1.
|
|
353
|
+
declare const VERSION = "1.11.0";
|
|
354
354
|
type GitHubLink = {
|
|
355
355
|
object: "github_link";
|
|
356
356
|
app: string;
|
|
@@ -507,6 +507,22 @@ declare class Plures {
|
|
|
507
507
|
created_at: string;
|
|
508
508
|
}[];
|
|
509
509
|
}>;
|
|
510
|
+
/** While mapping, which files the coding agent is reading, shown live on the map page. File names only. npx plures map does this. */
|
|
511
|
+
progress: (app: string, p: {
|
|
512
|
+
session: string;
|
|
513
|
+
phase?: "reading" | "drawing" | "done" | "failed";
|
|
514
|
+
source?: "cli" | "github";
|
|
515
|
+
events?: {
|
|
516
|
+
path: string;
|
|
517
|
+
tool?: string;
|
|
518
|
+
}[];
|
|
519
|
+
}) => Promise<{
|
|
520
|
+
object: "map_progress";
|
|
521
|
+
app: string;
|
|
522
|
+
session: string;
|
|
523
|
+
phase: string;
|
|
524
|
+
received: number;
|
|
525
|
+
}>;
|
|
510
526
|
/** The GitHub Actions workflow that maps a repo on every push to its main branch. */
|
|
511
527
|
workflow: (app: string, branch?: string) => Promise<string>;
|
|
512
528
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -350,7 +350,7 @@ type WebhookEvent = {
|
|
|
350
350
|
created_at: string;
|
|
351
351
|
data: Task;
|
|
352
352
|
};
|
|
353
|
-
declare const VERSION = "1.
|
|
353
|
+
declare const VERSION = "1.11.0";
|
|
354
354
|
type GitHubLink = {
|
|
355
355
|
object: "github_link";
|
|
356
356
|
app: string;
|
|
@@ -507,6 +507,22 @@ declare class Plures {
|
|
|
507
507
|
created_at: string;
|
|
508
508
|
}[];
|
|
509
509
|
}>;
|
|
510
|
+
/** While mapping, which files the coding agent is reading, shown live on the map page. File names only. npx plures map does this. */
|
|
511
|
+
progress: (app: string, p: {
|
|
512
|
+
session: string;
|
|
513
|
+
phase?: "reading" | "drawing" | "done" | "failed";
|
|
514
|
+
source?: "cli" | "github";
|
|
515
|
+
events?: {
|
|
516
|
+
path: string;
|
|
517
|
+
tool?: string;
|
|
518
|
+
}[];
|
|
519
|
+
}) => Promise<{
|
|
520
|
+
object: "map_progress";
|
|
521
|
+
app: string;
|
|
522
|
+
session: string;
|
|
523
|
+
phase: string;
|
|
524
|
+
received: number;
|
|
525
|
+
}>;
|
|
510
526
|
/** The GitHub Actions workflow that maps a repo on every push to its main branch. */
|
|
511
527
|
workflow: (app: string, branch?: string) => Promise<string>;
|
|
512
528
|
};
|
package/dist/index.js
CHANGED
|
@@ -1,199 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
this.type = type;
|
|
8
|
-
}
|
|
9
|
-
status;
|
|
10
|
-
type;
|
|
11
|
-
};
|
|
12
|
-
var Plures = class {
|
|
13
|
-
base;
|
|
14
|
-
apiKey;
|
|
15
|
-
/** The app tasks go to when they don't name one. From opts.app or PLURES_APP. */
|
|
16
|
-
app;
|
|
17
|
-
ua;
|
|
18
|
-
constructor(apiKey, opts = {}) {
|
|
19
|
-
const key = apiKey ?? (typeof process !== "undefined" ? process.env?.PLURES_API_KEY : void 0);
|
|
20
|
-
if (!key) throw new Error("Pass your Plures API key, or set PLURES_API_KEY. Get a free test key at https://plures.si/?for=developers#join");
|
|
21
|
-
this.apiKey = key;
|
|
22
|
-
this.ua = opts.userAgent ?? `plures-js/${VERSION}`;
|
|
23
|
-
this.app = opts.app ?? (typeof process !== "undefined" ? process.env?.PLURES_APP : void 0) ?? null;
|
|
24
|
-
this.base = (opts.baseUrl ?? (typeof process !== "undefined" ? process.env?.PLURES_BASE_URL : void 0) ?? "https://plures.si").replace(/\/$/, "");
|
|
25
|
-
}
|
|
26
|
-
async req(method, path, body, headers = {}) {
|
|
27
|
-
for (let attempt = 0; ; attempt++) {
|
|
28
|
-
const r = await fetch(this.base + path, {
|
|
29
|
-
method,
|
|
30
|
-
headers: { authorization: `Bearer ${this.apiKey}`, "user-agent": this.ua, ...body ? { "content-type": "application/json" } : {}, ...this.app ? { "plures-app": this.app } : {}, ...headers },
|
|
31
|
-
body: body ? JSON.stringify(body) : void 0
|
|
32
|
-
});
|
|
33
|
-
if (r.status >= 500 && attempt < 2) {
|
|
34
|
-
await sleep(500 * 2 ** attempt);
|
|
35
|
-
continue;
|
|
36
|
-
}
|
|
37
|
-
const data = await r.json().catch(() => ({}));
|
|
38
|
-
if (!r.ok) throw new PluresError(data?.error?.message ?? `HTTP ${r.status}`, r.status, data?.error?.type ?? "api_error");
|
|
39
|
-
return data;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
tasks = {
|
|
43
|
-
/** Sends a question to verified people. Pass an idempotency key to make retries safe. */
|
|
44
|
-
create: (params, opts = {}) => this.req("POST", "/api/v1/tasks", params, opts.idempotencyKey ? { "idempotency-key": opts.idempotencyKey } : {}),
|
|
45
|
-
retrieve: (id) => this.req("GET", `/api/v1/tasks/${encodeURIComponent(id)}`),
|
|
46
|
-
list: (params = {}) => {
|
|
47
|
-
const q = new URLSearchParams(Object.entries(params).filter(([, v]) => v != null).map(([k, v]) => [k, String(v)]));
|
|
48
|
-
return this.req("GET", `/api/v1/tasks${q.size ? `?${q}` : ""}`);
|
|
49
|
-
},
|
|
50
|
-
cancel: (id) => this.req("POST", `/api/v1/tasks/${encodeURIComponent(id)}/cancel`),
|
|
51
|
-
/** Polls until the task is settled. Prefer webhooks in production. */
|
|
52
|
-
waitFor: async (id, opts = {}) => {
|
|
53
|
-
const end = Date.now() + (opts.timeoutMs ?? 10 * 6e4);
|
|
54
|
-
for (; ; ) {
|
|
55
|
-
const t = await this.tasks.retrieve(id);
|
|
56
|
-
if (t.status !== "pending") return t;
|
|
57
|
-
if (Date.now() > end) return t;
|
|
58
|
-
await sleep(opts.intervalMs ?? 3e3);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
};
|
|
62
|
-
/**
|
|
63
|
-
* Test keys only. How test tasks behave when they don't set metadata.test_outcome,
|
|
64
|
-
* so an agent running unchanged sends a share of decisions to your team.
|
|
65
|
-
* A single task can override it with metadata.test_mix.
|
|
66
|
-
*/
|
|
67
|
-
testMix = {
|
|
68
|
-
get: () => this.req("GET", "/api/v1/test_mix"),
|
|
69
|
-
/** For example { unanimous: 70, split: 20, tie: 5, expired: 5 }. Pass null to turn it off. */
|
|
70
|
-
set: (mix) => this.req("PUT", "/api/v1/test_mix", { mix }),
|
|
71
|
-
clear: () => this.req("DELETE", "/api/v1/test_mix")
|
|
72
|
-
};
|
|
73
|
-
/**
|
|
74
|
-
* Where tasks go. set() replaces what you send (team, groups or rules) and keeps the rest.
|
|
75
|
-
* For example { groups: [{ name: "Finance", emails: ["ap@acme.com"] }], rules: [{ category: "refunds", route: "team", group: "Finance" }] }.
|
|
76
|
-
*/
|
|
77
|
-
routing = {
|
|
78
|
-
get: () => this.req("GET", "/api/v1/routing"),
|
|
79
|
-
set: (update) => this.req("PUT", "/api/v1/routing", update)
|
|
80
|
-
};
|
|
81
|
-
/** Your apps. Each agent you run can be its own app, with its own tasks, ledger view and map. */
|
|
82
|
-
apps = {
|
|
83
|
-
list: () => this.req("GET", "/api/v1/apps"),
|
|
84
|
-
/** { name: "Support agent" }, with an optional id like "support-agent". */
|
|
85
|
-
create: (params) => this.req("POST", "/api/v1/apps", params),
|
|
86
|
-
retrieve: (id) => this.req("GET", `/api/v1/apps/${encodeURIComponent(id)}`),
|
|
87
|
-
/** Rename it, change its id (its tasks move with it), or make it the default. */
|
|
88
|
-
update: (id, params) => this.req("PATCH", `/api/v1/apps/${encodeURIComponent(id)}`, params),
|
|
89
|
-
/** Only an app with no tasks can be deleted. */
|
|
90
|
-
del: (id) => this.req("DELETE", `/api/v1/apps/${encodeURIComponent(id)}`)
|
|
91
|
-
};
|
|
92
|
-
/**
|
|
93
|
-
* Agent maps: where each app's agent needs a person. Your coding agent reads your code and
|
|
94
|
-
* writes the map; Plures gets only the map. guide() returns the steps it follows.
|
|
95
|
-
*/
|
|
96
|
-
maps = {
|
|
97
|
-
guide: async () => {
|
|
98
|
-
const r = await fetch(`${this.base}/api/v1/map_guide`);
|
|
99
|
-
return r.text();
|
|
100
|
-
},
|
|
101
|
-
retrieve: (app) => this.req("GET", `/api/v1/apps/${encodeURIComponent(app)}/map`),
|
|
102
|
-
/** Uploads a new version. Checks you already added or dismissed keep that. */
|
|
103
|
-
upload: (app, map) => this.req("PUT", `/api/v1/apps/${encodeURIComponent(app)}/map`, map),
|
|
104
|
-
setCheckpoint: (app, id, status) => this.req("PATCH", `/api/v1/apps/${encodeURIComponent(app)}/map/checkpoints/${encodeURIComponent(id)}`, { status }),
|
|
105
|
-
/** A key that can only read and upload this app's map, for CI. The key is in the response once. */
|
|
106
|
-
createKey: (app, opts = {}) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app)}/map_keys`, opts),
|
|
107
|
-
keys: (app) => this.req("GET", `/api/v1/apps/${encodeURIComponent(app)}/map_keys`),
|
|
108
|
-
revokeKey: (app, id) => this.req("DELETE", `/api/v1/apps/${encodeURIComponent(app)}/map_keys/${encodeURIComponent(id)}`),
|
|
109
|
-
/** Starts a map run where Plures runs Claude (CI with no Anthropic key). npx plures map does this. */
|
|
110
|
-
startRun: (app) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app)}/map_runs`, {}),
|
|
111
|
-
runs: (app) => this.req("GET", `/api/v1/apps/${encodeURIComponent(app)}/map_runs`),
|
|
112
|
-
/** The GitHub Actions workflow that maps a repo on every push to its main branch. */
|
|
113
|
-
workflow: async (app, branch = "main") => {
|
|
114
|
-
const r = await fetch(`${this.base}/api/v1/map_workflow?app=${encodeURIComponent(app)}&branch=${encodeURIComponent(branch)}`);
|
|
115
|
-
return r.text();
|
|
116
|
-
}
|
|
117
|
-
};
|
|
118
|
-
/**
|
|
119
|
-
* Map with GitHub: Plures adds a workflow to your repo by pull request and keeps the app's map
|
|
120
|
-
* current on every push. Installing the Plures GitHub App needs a browser once (installUrl);
|
|
121
|
-
* everything after that works here.
|
|
122
|
-
*/
|
|
123
|
-
github = {
|
|
124
|
-
/** Repos the Plures GitHub App can reach. installed is false until you install it at install_url. */
|
|
125
|
-
repos: () => this.req("GET", "/api/v1/github/repos"),
|
|
126
|
-
/** Where an app's GitHub link stands: repo, pull request, mapping on, latest run, last map. */
|
|
127
|
-
status: (app) => this.req("GET", `/api/v1/apps/${encodeURIComponent(app)}/github`),
|
|
128
|
-
/** Connects a repo ("owner/name"): sets its map key secret and opens the pull request. */
|
|
129
|
-
connect: (app, repo) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app)}/github`, { repo }),
|
|
130
|
-
/** Turns mapping on by merging the pull request. The repo's own rules (reviews, checks) still apply. */
|
|
131
|
-
turnOn: (app) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app)}/github/merge`, {}),
|
|
132
|
-
/** Revokes the repo's map key. Delete the workflow file in the repo to stop runs. */
|
|
133
|
-
disconnect: (app) => this.req("DELETE", `/api/v1/apps/${encodeURIComponent(app)}/github`)
|
|
134
|
-
};
|
|
135
|
-
/**
|
|
136
|
-
* Quality: people grade a sample of your agent's outputs against a rubric, so you see drift
|
|
137
|
-
* early, and compare two versions blind before you ship. Personal details are taken out first.
|
|
138
|
-
*/
|
|
139
|
-
quality = {
|
|
140
|
-
/** Send every output; the rubric's sample rate decides which ones people grade. */
|
|
141
|
-
sample: (p) => this.req("POST", "/api/v1/samples", p),
|
|
142
|
-
/** A blind release check: a is the current version's output, b the new one's, for the same input. */
|
|
143
|
-
compare: (p) => this.req("POST", "/api/v1/comparisons", p),
|
|
144
|
-
/** How often people preferred the new version, with a 95% range and a verdict. */
|
|
145
|
-
release: (name, opts = {}) => this.req("GET", `/api/v1/releases/${encodeURIComponent(name)}${opts.app ? `?app=${encodeURIComponent(opts.app)}` : ""}`),
|
|
146
|
-
rubrics: {
|
|
147
|
-
list: (opts = {}) => this.req("GET", `/api/v1/rubrics${opts.app ? `?app=${encodeURIComponent(opts.app)}` : ""}`),
|
|
148
|
-
/** { name: "reply_quality", question: "Is this reply correct and kind?", options: ["Good", "Okay", "Bad"], sample_rate: 0.05 } */
|
|
149
|
-
create: (p) => this.req("POST", "/api/v1/rubrics", p),
|
|
150
|
-
retrieve: (name, opts = {}) => this.req("GET", `/api/v1/rubrics/${encodeURIComponent(name)}${opts.app ? `?app=${encodeURIComponent(opts.app)}` : ""}`),
|
|
151
|
-
update: (name, p) => this.req("PATCH", `/api/v1/rubrics/${encodeURIComponent(name)}${p.app ? `?app=${encodeURIComponent(p.app)}` : ""}`, p),
|
|
152
|
-
del: (name, opts = {}) => this.req("DELETE", `/api/v1/rubrics/${encodeURIComponent(name)}${opts.app ? `?app=${encodeURIComponent(opts.app)}` : ""}`)
|
|
153
|
-
},
|
|
154
|
-
/** Graded samples as JSON lines (input, output, grade, score, agreement), for fine tuning or an eval set. */
|
|
155
|
-
export: async (rubric, opts = {}) => {
|
|
156
|
-
const q = new URLSearchParams(Object.entries({ app: opts.app ?? this.app ?? void 0, since: opts.since }).filter(([, v]) => v));
|
|
157
|
-
const r = await fetch(`${this.base}/api/v1/rubrics/${encodeURIComponent(rubric)}/export${q.size ? `?${q}` : ""}`, { headers: { authorization: `Bearer ${this.apiKey}`, "user-agent": this.ua } });
|
|
158
|
-
if (!r.ok) {
|
|
159
|
-
const d = await r.json().catch(() => ({}));
|
|
160
|
-
throw new PluresError(d?.error?.message ?? `HTTP ${r.status}`, r.status, d?.error?.type ?? "api_error");
|
|
161
|
-
}
|
|
162
|
-
return (await r.text()).split("\n").filter(Boolean).map((l) => JSON.parse(l));
|
|
163
|
-
}
|
|
164
|
-
};
|
|
165
|
-
/** Your prepaid credit. A live task that could cost more than available_cents is refused with a 402 insufficient_credits error. */
|
|
166
|
-
balance = () => this.req("GET", "/api/v1/balance");
|
|
167
|
-
/** Creates a task and waits for people to answer it. */
|
|
168
|
-
async verify(params, opts = {}) {
|
|
169
|
-
const t = await this.tasks.create(params, { idempotencyKey: opts.idempotencyKey });
|
|
170
|
-
return t.status === "pending" ? this.tasks.waitFor(t.id, { timeoutMs: opts.timeoutMs }) : t;
|
|
171
|
-
}
|
|
172
|
-
/**
|
|
173
|
-
* Checks the Plures-Signature header on a webhook and returns the parsed event.
|
|
174
|
-
* Pass the raw request body exactly as received. Throws if the signature is wrong
|
|
175
|
-
* or older than toleranceSec.
|
|
176
|
-
*/
|
|
177
|
-
static async verifyWebhook(rawBody, signatureHeader, secret, toleranceSec = 300) {
|
|
178
|
-
const parts = Object.fromEntries((signatureHeader ?? "").split(",").map((p) => p.split("=", 2)));
|
|
179
|
-
const t = Number(parts.t);
|
|
180
|
-
if (!t || !parts.v1) throw new Error("Missing Plures-Signature header");
|
|
181
|
-
if (Math.abs(Date.now() / 1e3 - t) > toleranceSec) throw new Error("Webhook timestamp is too old");
|
|
182
|
-
const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
183
|
-
const mac = new Uint8Array(await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(`${t}.${rawBody}`)));
|
|
184
|
-
const expected = Array.from(mac, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
185
|
-
if (!timingSafeEqual(expected, parts.v1)) throw new Error("Webhook signature does not match");
|
|
186
|
-
return JSON.parse(rawBody);
|
|
187
|
-
}
|
|
188
|
-
};
|
|
189
|
-
function timingSafeEqual(a, b) {
|
|
190
|
-
if (a.length !== b.length) return false;
|
|
191
|
-
let diff = 0;
|
|
192
|
-
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
193
|
-
return diff === 0;
|
|
194
|
-
}
|
|
195
|
-
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
196
|
-
var index_default = Plures;
|
|
1
|
+
import {
|
|
2
|
+
Plures,
|
|
3
|
+
PluresError,
|
|
4
|
+
VERSION,
|
|
5
|
+
index_default
|
|
6
|
+
} from "./chunk-WPSUIAVZ.js";
|
|
197
7
|
export {
|
|
198
8
|
Plures,
|
|
199
9
|
PluresError,
|