plures 1.9.1 → 1.10.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 +2 -1
- package/dist/cli.cjs +91 -15
- package/dist/cli.js +260 -8
- package/dist/index.cjs +18 -1
- package/dist/index.d.cts +64 -2
- package/dist/index.d.ts +64 -2
- package/dist/index.js +196 -6
- package/package.json +1 -1
- package/dist/chunk-23ZYPCPM.js +0 -186
package/README.md
CHANGED
|
@@ -48,7 +48,8 @@ if (event.type === "task.completed" && event.data.result?.agreement === 1) {
|
|
|
48
48
|
- `plures.tasks.retrieve(id)`, `plures.tasks.list({ status?, limit?, starting_after?, app? })`, `plures.tasks.cancel(id)`
|
|
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
|
-
- `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. `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).
|
|
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
|
+
- `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`.
|
|
52
53
|
- `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.
|
|
53
54
|
- `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.
|
|
54
55
|
- `plures.routing.get()`, `plures.routing.set({ groups, rules, team })`: named teams of your own people and rules by category, like `{ groups: [{ name: "Finance", emails: ["ap@acme.com"] }], rules: [{ category: "refunds", route: "team", group: "Finance" }] }`. What you send replaces what's there; the rest stays.
|
package/dist/cli.cjs
CHANGED
|
@@ -5,16 +5,16 @@
|
|
|
5
5
|
var import_node_child_process = require("child_process");
|
|
6
6
|
var import_node_fs = require("fs");
|
|
7
7
|
|
|
8
|
-
// src/index.
|
|
8
|
+
// src/index.js
|
|
9
9
|
var VERSION = "1.9.1";
|
|
10
10
|
var PluresError = class extends Error {
|
|
11
|
+
status;
|
|
12
|
+
type;
|
|
11
13
|
constructor(message, status, type) {
|
|
12
14
|
super(message);
|
|
13
15
|
this.status = status;
|
|
14
16
|
this.type = type;
|
|
15
17
|
}
|
|
16
|
-
status;
|
|
17
|
-
type;
|
|
18
18
|
};
|
|
19
19
|
var Plures = class {
|
|
20
20
|
base;
|
|
@@ -24,7 +24,8 @@ var Plures = class {
|
|
|
24
24
|
ua;
|
|
25
25
|
constructor(apiKey, opts = {}) {
|
|
26
26
|
const key2 = apiKey ?? (typeof process !== "undefined" ? process.env?.PLURES_API_KEY : void 0);
|
|
27
|
-
if (!key2)
|
|
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");
|
|
28
29
|
this.apiKey = key2;
|
|
29
30
|
this.ua = opts.userAgent ?? `plures-js/${VERSION}`;
|
|
30
31
|
this.app = opts.app ?? (typeof process !== "undefined" ? process.env?.PLURES_APP : void 0) ?? null;
|
|
@@ -42,7 +43,8 @@ var Plures = class {
|
|
|
42
43
|
continue;
|
|
43
44
|
}
|
|
44
45
|
const data = await r.json().catch(() => ({}));
|
|
45
|
-
if (!r.ok)
|
|
46
|
+
if (!r.ok)
|
|
47
|
+
throw new PluresError(data?.error?.message ?? `HTTP ${r.status}`, r.status, data?.error?.type ?? "api_error");
|
|
46
48
|
return data;
|
|
47
49
|
}
|
|
48
50
|
}
|
|
@@ -60,8 +62,10 @@ var Plures = class {
|
|
|
60
62
|
const end = Date.now() + (opts.timeoutMs ?? 10 * 6e4);
|
|
61
63
|
for (; ; ) {
|
|
62
64
|
const t = await this.tasks.retrieve(id);
|
|
63
|
-
if (t.status !== "pending")
|
|
64
|
-
|
|
65
|
+
if (t.status !== "pending")
|
|
66
|
+
return t;
|
|
67
|
+
if (Date.now() > end)
|
|
68
|
+
return t;
|
|
65
69
|
await sleep(opts.intervalMs ?? 3e3);
|
|
66
70
|
}
|
|
67
71
|
}
|
|
@@ -122,6 +126,23 @@ var Plures = class {
|
|
|
122
126
|
return r.text();
|
|
123
127
|
}
|
|
124
128
|
};
|
|
129
|
+
/**
|
|
130
|
+
* Map with GitHub: Plures adds a workflow to your repo by pull request and keeps the app's map
|
|
131
|
+
* current on every push. Installing the Plures GitHub App needs a browser once (installUrl);
|
|
132
|
+
* everything after that works here.
|
|
133
|
+
*/
|
|
134
|
+
github = {
|
|
135
|
+
/** Repos the Plures GitHub App can reach. installed is false until you install it at install_url. */
|
|
136
|
+
repos: () => this.req("GET", "/api/v1/github/repos"),
|
|
137
|
+
/** Where an app's GitHub link stands: repo, pull request, mapping on, latest run, last map. */
|
|
138
|
+
status: (app2) => this.req("GET", `/api/v1/apps/${encodeURIComponent(app2)}/github`),
|
|
139
|
+
/** Connects a repo ("owner/name"): sets its map key secret and opens the pull request. */
|
|
140
|
+
connect: (app2, repo) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app2)}/github`, { repo }),
|
|
141
|
+
/** Turns mapping on by merging the pull request. The repo's own rules (reviews, checks) still apply. */
|
|
142
|
+
turnOn: (app2) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app2)}/github/merge`, {}),
|
|
143
|
+
/** Revokes the repo's map key. Delete the workflow file in the repo to stop runs. */
|
|
144
|
+
disconnect: (app2) => this.req("DELETE", `/api/v1/apps/${encodeURIComponent(app2)}/github`)
|
|
145
|
+
};
|
|
125
146
|
/**
|
|
126
147
|
* Quality: people grade a sample of your agent's outputs against a rubric, so you see drift
|
|
127
148
|
* early, and compare two versions blind before you ship. Personal details are taken out first.
|
|
@@ -167,19 +188,24 @@ var Plures = class {
|
|
|
167
188
|
static async verifyWebhook(rawBody, signatureHeader, secret, toleranceSec = 300) {
|
|
168
189
|
const parts = Object.fromEntries((signatureHeader ?? "").split(",").map((p) => p.split("=", 2)));
|
|
169
190
|
const t = Number(parts.t);
|
|
170
|
-
if (!t || !parts.v1)
|
|
171
|
-
|
|
191
|
+
if (!t || !parts.v1)
|
|
192
|
+
throw new Error("Missing Plures-Signature header");
|
|
193
|
+
if (Math.abs(Date.now() / 1e3 - t) > toleranceSec)
|
|
194
|
+
throw new Error("Webhook timestamp is too old");
|
|
172
195
|
const key2 = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
173
196
|
const mac = new Uint8Array(await crypto.subtle.sign("HMAC", key2, new TextEncoder().encode(`${t}.${rawBody}`)));
|
|
174
197
|
const expected = Array.from(mac, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
175
|
-
if (!timingSafeEqual(expected, parts.v1))
|
|
198
|
+
if (!timingSafeEqual(expected, parts.v1))
|
|
199
|
+
throw new Error("Webhook signature does not match");
|
|
176
200
|
return JSON.parse(rawBody);
|
|
177
201
|
}
|
|
178
202
|
};
|
|
179
203
|
function timingSafeEqual(a, b) {
|
|
180
|
-
if (a.length !== b.length)
|
|
204
|
+
if (a.length !== b.length)
|
|
205
|
+
return false;
|
|
181
206
|
let diff = 0;
|
|
182
|
-
for (let i = 0; i < a.length; i++)
|
|
207
|
+
for (let i = 0; i < a.length; i++)
|
|
208
|
+
diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
183
209
|
return diff === 0;
|
|
184
210
|
}
|
|
185
211
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -301,11 +327,58 @@ async function workflow() {
|
|
|
301
327
|
(0, import_node_fs.writeFileSync)(path, await r.text());
|
|
302
328
|
console.log(`${bold("Wrote " + path)}: maps this repo on every push to ${b}.
|
|
303
329
|
|
|
304
|
-
Then add
|
|
330
|
+
Then add a repository secret (Settings, Secrets and variables, Actions):
|
|
305
331
|
${cyan("PLURES_MAP_KEY")} a map key: ${dim(`${base}/dashboard/apps/${app}/map`)} or ${dim("POST /api/v1/apps/" + app + "/map_keys")}
|
|
306
|
-
|
|
332
|
+
Optional: ${cyan("ANTHROPIC_API_KEY")}, or ${cyan("CLAUDE_CODE_OAUTH_TOKEN")} from ${dim("claude setup-token")}, to map on your own key for free.
|
|
333
|
+
Without it Plures maps it: the first map is free.
|
|
307
334
|
|
|
308
|
-
Or let Plures do all of this: ${bold("
|
|
335
|
+
Or let Plures do all of this: ${bold("npx plures map github connect")}.`);
|
|
336
|
+
}
|
|
337
|
+
function originRepo() {
|
|
338
|
+
const u = (0, import_node_child_process.spawnSync)("git", ["remote", "get-url", "origin"], { encoding: "utf8" }).stdout?.trim() ?? "";
|
|
339
|
+
return /github\.com[:/]([\w.-]+\/[\w.-]+?)(?:\.git)?$/.exec(u)?.[1];
|
|
340
|
+
}
|
|
341
|
+
async function github(action, arg) {
|
|
342
|
+
const p = client();
|
|
343
|
+
const show = (g) => {
|
|
344
|
+
if (!g.connected) return console.log(`${bold(app)} has no repo connected. Run ${cyan("npx plures map github connect")} in the repo, or pass ${cyan("owner/repo")}.`);
|
|
345
|
+
const on = g.mapping_on ? cyan("on") : coral("off until the pull request is merged");
|
|
346
|
+
console.log(`${bold(app)} maps from ${bold(g.repo)}: mapping ${on}.
|
|
347
|
+
Pull request ${g.pull_request?.state} ${dim(g.pull_request?.url ?? "")}
|
|
348
|
+
Mapped by ${g.mapped_by === "your_anthropic_key" ? "your Anthropic key, free" : g.mapped_by === "plures" ? `Plures${g.first_map_free ? ", first map free" : ""}` : coral("needs ANTHROPIC_API_KEY in the repo's secrets")}
|
|
349
|
+
Latest run ${g.latest_run ? `${g.latest_run.conclusion ?? g.latest_run.status} ${dim(g.latest_run.url)}` : dim("none yet")}
|
|
350
|
+
Map ${g.mapped_at ? `uploaded ${g.mapped_at}` : dim("arrives a few minutes after the first run")}`);
|
|
351
|
+
if (g.pull_request?.state === "open") console.log(`
|
|
352
|
+
Turn it on: ${cyan("npx plures map github on")}${app !== "default" ? ` --app ${app}` : ""}`);
|
|
353
|
+
};
|
|
354
|
+
try {
|
|
355
|
+
if (!action || action === "status") return show(await p.github.status(app));
|
|
356
|
+
if (action === "on") {
|
|
357
|
+
const r = await p.github.turnOn(app);
|
|
358
|
+
console.log(r.already_merged ? "Mapping was already on." : `${bold("Mapping is on.")} The first run is starting on GitHub.`);
|
|
359
|
+
return show(await p.github.status(app));
|
|
360
|
+
}
|
|
361
|
+
if (action === "off") {
|
|
362
|
+
await p.github.disconnect(app);
|
|
363
|
+
return console.log(`Disconnected. The repo's map key is revoked. Delete ${cyan(".github/workflows/plures-map.yml")} in the repo to stop runs.`);
|
|
364
|
+
}
|
|
365
|
+
if (action === "connect") {
|
|
366
|
+
const repo = arg ?? originRepo();
|
|
367
|
+
if (!repo) die("Pass the repo as owner/name, or run this inside a clone of it.");
|
|
368
|
+
const list = await p.github.repos();
|
|
369
|
+
if (!list.installed || !list.data.some((r) => r.repo.toLowerCase() === repo.toLowerCase()))
|
|
370
|
+
die(`The Plures GitHub App can't reach ${repo} yet. Open this once, pick the repo, then run this again:
|
|
371
|
+
${list.install_url}?app=${app}`);
|
|
372
|
+
const g = await p.github.connect(app, repo);
|
|
373
|
+
console.log(`${bold("Pull request opened")} in ${repo}: ${g.pull_request?.url}
|
|
374
|
+
It adds one workflow file. Review it, then turn mapping on with ${cyan("npx plures map github on")}${app !== "default" ? ` --app ${app}` : ""}, or merge it on GitHub.`);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
die("npx plures map github [connect [owner/repo] | on | off]");
|
|
378
|
+
} catch (e) {
|
|
379
|
+
if (e instanceof PluresError) die(e.message);
|
|
380
|
+
throw e;
|
|
381
|
+
}
|
|
309
382
|
}
|
|
310
383
|
async function main() {
|
|
311
384
|
const [cmd, sub, arg] = argv;
|
|
@@ -316,6 +389,8 @@ async function main() {
|
|
|
316
389
|
npx plures map upload [file] upload ${FILE}
|
|
317
390
|
npx plures map guide print the steps for any coding agent
|
|
318
391
|
npx plures map workflow add a GitHub Action that maps on every push
|
|
392
|
+
npx plures map github connect let Plures add that Action by pull request
|
|
393
|
+
npx plures map github on merge it, which turns mapping on
|
|
319
394
|
|
|
320
395
|
Docs: https://plures.si/docs`);
|
|
321
396
|
return;
|
|
@@ -323,6 +398,7 @@ Docs: https://plures.si/docs`);
|
|
|
323
398
|
if (sub === "upload") return upload(arg ?? FILE);
|
|
324
399
|
if (sub === "guide") return void console.log(await (await fetch(`${base}/api/v1/map_guide`)).text());
|
|
325
400
|
if (sub === "workflow") return workflow();
|
|
401
|
+
if (sub === "github") return github(arg, argv[3]);
|
|
326
402
|
return map();
|
|
327
403
|
}
|
|
328
404
|
main().catch((e) => die(String(e?.message ?? e)));
|
package/dist/cli.js
CHANGED
|
@@ -1,13 +1,215 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
Plures,
|
|
4
|
-
PluresError,
|
|
5
|
-
VERSION
|
|
6
|
-
} from "./chunk-23ZYPCPM.js";
|
|
7
2
|
|
|
8
3
|
// src/cli.ts
|
|
9
4
|
import { spawnSync, spawn } from "child_process";
|
|
10
5
|
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
|
|
11
213
|
var argv = process.argv.slice(2);
|
|
12
214
|
var flag = (name) => {
|
|
13
215
|
const i = argv.indexOf(`--${name}`);
|
|
@@ -124,11 +326,58 @@ async function workflow() {
|
|
|
124
326
|
writeFileSync(path, await r.text());
|
|
125
327
|
console.log(`${bold("Wrote " + path)}: maps this repo on every push to ${b}.
|
|
126
328
|
|
|
127
|
-
Then add
|
|
329
|
+
Then add a repository secret (Settings, Secrets and variables, Actions):
|
|
128
330
|
${cyan("PLURES_MAP_KEY")} a map key: ${dim(`${base}/dashboard/apps/${app}/map`)} or ${dim("POST /api/v1/apps/" + app + "/map_keys")}
|
|
129
|
-
|
|
331
|
+
Optional: ${cyan("ANTHROPIC_API_KEY")}, or ${cyan("CLAUDE_CODE_OAUTH_TOKEN")} from ${dim("claude setup-token")}, to map on your own key for free.
|
|
332
|
+
Without it Plures maps it: the first map is free.
|
|
130
333
|
|
|
131
|
-
Or let Plures do all of this: ${bold("
|
|
334
|
+
Or let Plures do all of this: ${bold("npx plures map github connect")}.`);
|
|
335
|
+
}
|
|
336
|
+
function originRepo() {
|
|
337
|
+
const u = spawnSync("git", ["remote", "get-url", "origin"], { encoding: "utf8" }).stdout?.trim() ?? "";
|
|
338
|
+
return /github\.com[:/]([\w.-]+\/[\w.-]+?)(?:\.git)?$/.exec(u)?.[1];
|
|
339
|
+
}
|
|
340
|
+
async function github(action, arg) {
|
|
341
|
+
const p = client();
|
|
342
|
+
const show = (g) => {
|
|
343
|
+
if (!g.connected) return console.log(`${bold(app)} has no repo connected. Run ${cyan("npx plures map github connect")} in the repo, or pass ${cyan("owner/repo")}.`);
|
|
344
|
+
const on = g.mapping_on ? cyan("on") : coral("off until the pull request is merged");
|
|
345
|
+
console.log(`${bold(app)} maps from ${bold(g.repo)}: mapping ${on}.
|
|
346
|
+
Pull request ${g.pull_request?.state} ${dim(g.pull_request?.url ?? "")}
|
|
347
|
+
Mapped by ${g.mapped_by === "your_anthropic_key" ? "your Anthropic key, free" : g.mapped_by === "plures" ? `Plures${g.first_map_free ? ", first map free" : ""}` : coral("needs ANTHROPIC_API_KEY in the repo's secrets")}
|
|
348
|
+
Latest run ${g.latest_run ? `${g.latest_run.conclusion ?? g.latest_run.status} ${dim(g.latest_run.url)}` : dim("none yet")}
|
|
349
|
+
Map ${g.mapped_at ? `uploaded ${g.mapped_at}` : dim("arrives a few minutes after the first run")}`);
|
|
350
|
+
if (g.pull_request?.state === "open") console.log(`
|
|
351
|
+
Turn it on: ${cyan("npx plures map github on")}${app !== "default" ? ` --app ${app}` : ""}`);
|
|
352
|
+
};
|
|
353
|
+
try {
|
|
354
|
+
if (!action || action === "status") return show(await p.github.status(app));
|
|
355
|
+
if (action === "on") {
|
|
356
|
+
const r = await p.github.turnOn(app);
|
|
357
|
+
console.log(r.already_merged ? "Mapping was already on." : `${bold("Mapping is on.")} The first run is starting on GitHub.`);
|
|
358
|
+
return show(await p.github.status(app));
|
|
359
|
+
}
|
|
360
|
+
if (action === "off") {
|
|
361
|
+
await p.github.disconnect(app);
|
|
362
|
+
return console.log(`Disconnected. The repo's map key is revoked. Delete ${cyan(".github/workflows/plures-map.yml")} in the repo to stop runs.`);
|
|
363
|
+
}
|
|
364
|
+
if (action === "connect") {
|
|
365
|
+
const repo = arg ?? originRepo();
|
|
366
|
+
if (!repo) die("Pass the repo as owner/name, or run this inside a clone of it.");
|
|
367
|
+
const list = await p.github.repos();
|
|
368
|
+
if (!list.installed || !list.data.some((r) => r.repo.toLowerCase() === repo.toLowerCase()))
|
|
369
|
+
die(`The Plures GitHub App can't reach ${repo} yet. Open this once, pick the repo, then run this again:
|
|
370
|
+
${list.install_url}?app=${app}`);
|
|
371
|
+
const g = await p.github.connect(app, repo);
|
|
372
|
+
console.log(`${bold("Pull request opened")} in ${repo}: ${g.pull_request?.url}
|
|
373
|
+
It adds one workflow file. Review it, then turn mapping on with ${cyan("npx plures map github on")}${app !== "default" ? ` --app ${app}` : ""}, or merge it on GitHub.`);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
die("npx plures map github [connect [owner/repo] | on | off]");
|
|
377
|
+
} catch (e) {
|
|
378
|
+
if (e instanceof PluresError) die(e.message);
|
|
379
|
+
throw e;
|
|
380
|
+
}
|
|
132
381
|
}
|
|
133
382
|
async function main() {
|
|
134
383
|
const [cmd, sub, arg] = argv;
|
|
@@ -139,6 +388,8 @@ async function main() {
|
|
|
139
388
|
npx plures map upload [file] upload ${FILE}
|
|
140
389
|
npx plures map guide print the steps for any coding agent
|
|
141
390
|
npx plures map workflow add a GitHub Action that maps on every push
|
|
391
|
+
npx plures map github connect let Plures add that Action by pull request
|
|
392
|
+
npx plures map github on merge it, which turns mapping on
|
|
142
393
|
|
|
143
394
|
Docs: https://plures.si/docs`);
|
|
144
395
|
return;
|
|
@@ -146,6 +397,7 @@ Docs: https://plures.si/docs`);
|
|
|
146
397
|
if (sub === "upload") return upload(arg ?? FILE);
|
|
147
398
|
if (sub === "guide") return void console.log(await (await fetch(`${base}/api/v1/map_guide`)).text());
|
|
148
399
|
if (sub === "workflow") return workflow();
|
|
400
|
+
if (sub === "github") return github(arg, argv[3]);
|
|
149
401
|
return map();
|
|
150
402
|
}
|
|
151
403
|
main().catch((e) => die(String(e?.message ?? e)));
|
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.10.0";
|
|
30
30
|
var PluresError = class extends Error {
|
|
31
31
|
constructor(message, status, type) {
|
|
32
32
|
super(message);
|
|
@@ -142,6 +142,23 @@ var Plures = class {
|
|
|
142
142
|
return r.text();
|
|
143
143
|
}
|
|
144
144
|
};
|
|
145
|
+
/**
|
|
146
|
+
* Map with GitHub: Plures adds a workflow to your repo by pull request and keeps the app's map
|
|
147
|
+
* current on every push. Installing the Plures GitHub App needs a browser once (installUrl);
|
|
148
|
+
* everything after that works here.
|
|
149
|
+
*/
|
|
150
|
+
github = {
|
|
151
|
+
/** Repos the Plures GitHub App can reach. installed is false until you install it at install_url. */
|
|
152
|
+
repos: () => this.req("GET", "/api/v1/github/repos"),
|
|
153
|
+
/** Where an app's GitHub link stands: repo, pull request, mapping on, latest run, last map. */
|
|
154
|
+
status: (app) => this.req("GET", `/api/v1/apps/${encodeURIComponent(app)}/github`),
|
|
155
|
+
/** Connects a repo ("owner/name"): sets its map key secret and opens the pull request. */
|
|
156
|
+
connect: (app, repo) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app)}/github`, { repo }),
|
|
157
|
+
/** Turns mapping on by merging the pull request. The repo's own rules (reviews, checks) still apply. */
|
|
158
|
+
turnOn: (app) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app)}/github/merge`, {}),
|
|
159
|
+
/** Revokes the repo's map key. Delete the workflow file in the repo to stop runs. */
|
|
160
|
+
disconnect: (app) => this.req("DELETE", `/api/v1/apps/${encodeURIComponent(app)}/github`)
|
|
161
|
+
};
|
|
145
162
|
/**
|
|
146
163
|
* Quality: people grade a sample of your agent's outputs against a rubric, so you see drift
|
|
147
164
|
* early, and compare two versions blind before you ship. Personal details are taken out first.
|
package/dist/index.d.cts
CHANGED
|
@@ -350,7 +350,30 @@ type WebhookEvent = {
|
|
|
350
350
|
created_at: string;
|
|
351
351
|
data: Task;
|
|
352
352
|
};
|
|
353
|
-
declare const VERSION = "1.
|
|
353
|
+
declare const VERSION = "1.10.0";
|
|
354
|
+
type GitHubLink = {
|
|
355
|
+
object: "github_link";
|
|
356
|
+
app: string;
|
|
357
|
+
installed: boolean;
|
|
358
|
+
install_url: string;
|
|
359
|
+
connected: boolean;
|
|
360
|
+
repo?: string;
|
|
361
|
+
pull_request?: {
|
|
362
|
+
url: string | null;
|
|
363
|
+
state: "open" | "merged" | "closed" | "unknown";
|
|
364
|
+
};
|
|
365
|
+
mapping_on?: boolean;
|
|
366
|
+
mapped_by?: "plures" | "your_anthropic_key" | "needs_key";
|
|
367
|
+
first_map_free?: boolean;
|
|
368
|
+
latest_run?: {
|
|
369
|
+
status: string;
|
|
370
|
+
conclusion: string | null;
|
|
371
|
+
url: string;
|
|
372
|
+
created_at: string;
|
|
373
|
+
} | null;
|
|
374
|
+
mapped_at?: string | null;
|
|
375
|
+
connected_at?: string;
|
|
376
|
+
};
|
|
354
377
|
declare class PluresError extends Error {
|
|
355
378
|
readonly status: number;
|
|
356
379
|
readonly type: string;
|
|
@@ -487,6 +510,45 @@ declare class Plures {
|
|
|
487
510
|
/** The GitHub Actions workflow that maps a repo on every push to its main branch. */
|
|
488
511
|
workflow: (app: string, branch?: string) => Promise<string>;
|
|
489
512
|
};
|
|
513
|
+
/**
|
|
514
|
+
* Map with GitHub: Plures adds a workflow to your repo by pull request and keeps the app's map
|
|
515
|
+
* current on every push. Installing the Plures GitHub App needs a browser once (installUrl);
|
|
516
|
+
* everything after that works here.
|
|
517
|
+
*/
|
|
518
|
+
github: {
|
|
519
|
+
/** Repos the Plures GitHub App can reach. installed is false until you install it at install_url. */
|
|
520
|
+
repos: () => Promise<{
|
|
521
|
+
object: "list";
|
|
522
|
+
installed: boolean;
|
|
523
|
+
install_url: string;
|
|
524
|
+
data: {
|
|
525
|
+
object: "github_repo";
|
|
526
|
+
repo: string;
|
|
527
|
+
private: boolean;
|
|
528
|
+
account: string;
|
|
529
|
+
default_branch: string;
|
|
530
|
+
}[];
|
|
531
|
+
}>;
|
|
532
|
+
/** Where an app's GitHub link stands: repo, pull request, mapping on, latest run, last map. */
|
|
533
|
+
status: (app: string) => Promise<GitHubLink>;
|
|
534
|
+
/** Connects a repo ("owner/name"): sets its map key secret and opens the pull request. */
|
|
535
|
+
connect: (app: string, repo: string) => Promise<GitHubLink>;
|
|
536
|
+
/** Turns mapping on by merging the pull request. The repo's own rules (reviews, checks) still apply. */
|
|
537
|
+
turnOn: (app: string) => Promise<{
|
|
538
|
+
object: "github_merge";
|
|
539
|
+
app: string;
|
|
540
|
+
merged: true;
|
|
541
|
+
already_merged: boolean;
|
|
542
|
+
sha: string | null;
|
|
543
|
+
}>;
|
|
544
|
+
/** Revokes the repo's map key. Delete the workflow file in the repo to stop runs. */
|
|
545
|
+
disconnect: (app: string) => Promise<{
|
|
546
|
+
object: "github_link";
|
|
547
|
+
app: string;
|
|
548
|
+
connected: false;
|
|
549
|
+
deleted: true;
|
|
550
|
+
}>;
|
|
551
|
+
};
|
|
490
552
|
/**
|
|
491
553
|
* Quality: people grade a sample of your agent's outputs against a rubric, so you see drift
|
|
492
554
|
* early, and compare two versions blind before you ship. Personal details are taken out first.
|
|
@@ -578,4 +640,4 @@ declare class Plures {
|
|
|
578
640
|
static verifyWebhook(rawBody: string, signatureHeader: string | null, secret: string, toleranceSec?: number): Promise<WebhookEvent>;
|
|
579
641
|
}
|
|
580
642
|
|
|
581
|
-
export { type AgentMap, type App, type Balance, type Consensus, type CreateTask, type Escalation, type MapCheckpoint, type MapDraft, type MapKey, type MapNode, type Mix, type Option, Plures, PluresError, type Release, type Result, type Route, type Routing, type RoutingRule, type RoutingUpdate, type Rubric, type RubricParams, type Sample, type StoredAgentMap, type Task, type TaskStatus, type TeamTally, type TeamVote, type TestMix, VERSION, type WebhookEvent, Plures as default };
|
|
643
|
+
export { type AgentMap, type App, type Balance, type Consensus, type CreateTask, type Escalation, type GitHubLink, type MapCheckpoint, type MapDraft, type MapKey, type MapNode, type Mix, type Option, Plures, PluresError, type Release, type Result, type Route, type Routing, type RoutingRule, type RoutingUpdate, type Rubric, type RubricParams, type Sample, type StoredAgentMap, type Task, type TaskStatus, type TeamTally, type TeamVote, type TestMix, VERSION, type WebhookEvent, Plures as default };
|
package/dist/index.d.ts
CHANGED
|
@@ -350,7 +350,30 @@ type WebhookEvent = {
|
|
|
350
350
|
created_at: string;
|
|
351
351
|
data: Task;
|
|
352
352
|
};
|
|
353
|
-
declare const VERSION = "1.
|
|
353
|
+
declare const VERSION = "1.10.0";
|
|
354
|
+
type GitHubLink = {
|
|
355
|
+
object: "github_link";
|
|
356
|
+
app: string;
|
|
357
|
+
installed: boolean;
|
|
358
|
+
install_url: string;
|
|
359
|
+
connected: boolean;
|
|
360
|
+
repo?: string;
|
|
361
|
+
pull_request?: {
|
|
362
|
+
url: string | null;
|
|
363
|
+
state: "open" | "merged" | "closed" | "unknown";
|
|
364
|
+
};
|
|
365
|
+
mapping_on?: boolean;
|
|
366
|
+
mapped_by?: "plures" | "your_anthropic_key" | "needs_key";
|
|
367
|
+
first_map_free?: boolean;
|
|
368
|
+
latest_run?: {
|
|
369
|
+
status: string;
|
|
370
|
+
conclusion: string | null;
|
|
371
|
+
url: string;
|
|
372
|
+
created_at: string;
|
|
373
|
+
} | null;
|
|
374
|
+
mapped_at?: string | null;
|
|
375
|
+
connected_at?: string;
|
|
376
|
+
};
|
|
354
377
|
declare class PluresError extends Error {
|
|
355
378
|
readonly status: number;
|
|
356
379
|
readonly type: string;
|
|
@@ -487,6 +510,45 @@ declare class Plures {
|
|
|
487
510
|
/** The GitHub Actions workflow that maps a repo on every push to its main branch. */
|
|
488
511
|
workflow: (app: string, branch?: string) => Promise<string>;
|
|
489
512
|
};
|
|
513
|
+
/**
|
|
514
|
+
* Map with GitHub: Plures adds a workflow to your repo by pull request and keeps the app's map
|
|
515
|
+
* current on every push. Installing the Plures GitHub App needs a browser once (installUrl);
|
|
516
|
+
* everything after that works here.
|
|
517
|
+
*/
|
|
518
|
+
github: {
|
|
519
|
+
/** Repos the Plures GitHub App can reach. installed is false until you install it at install_url. */
|
|
520
|
+
repos: () => Promise<{
|
|
521
|
+
object: "list";
|
|
522
|
+
installed: boolean;
|
|
523
|
+
install_url: string;
|
|
524
|
+
data: {
|
|
525
|
+
object: "github_repo";
|
|
526
|
+
repo: string;
|
|
527
|
+
private: boolean;
|
|
528
|
+
account: string;
|
|
529
|
+
default_branch: string;
|
|
530
|
+
}[];
|
|
531
|
+
}>;
|
|
532
|
+
/** Where an app's GitHub link stands: repo, pull request, mapping on, latest run, last map. */
|
|
533
|
+
status: (app: string) => Promise<GitHubLink>;
|
|
534
|
+
/** Connects a repo ("owner/name"): sets its map key secret and opens the pull request. */
|
|
535
|
+
connect: (app: string, repo: string) => Promise<GitHubLink>;
|
|
536
|
+
/** Turns mapping on by merging the pull request. The repo's own rules (reviews, checks) still apply. */
|
|
537
|
+
turnOn: (app: string) => Promise<{
|
|
538
|
+
object: "github_merge";
|
|
539
|
+
app: string;
|
|
540
|
+
merged: true;
|
|
541
|
+
already_merged: boolean;
|
|
542
|
+
sha: string | null;
|
|
543
|
+
}>;
|
|
544
|
+
/** Revokes the repo's map key. Delete the workflow file in the repo to stop runs. */
|
|
545
|
+
disconnect: (app: string) => Promise<{
|
|
546
|
+
object: "github_link";
|
|
547
|
+
app: string;
|
|
548
|
+
connected: false;
|
|
549
|
+
deleted: true;
|
|
550
|
+
}>;
|
|
551
|
+
};
|
|
490
552
|
/**
|
|
491
553
|
* Quality: people grade a sample of your agent's outputs against a rubric, so you see drift
|
|
492
554
|
* early, and compare two versions blind before you ship. Personal details are taken out first.
|
|
@@ -578,4 +640,4 @@ declare class Plures {
|
|
|
578
640
|
static verifyWebhook(rawBody: string, signatureHeader: string | null, secret: string, toleranceSec?: number): Promise<WebhookEvent>;
|
|
579
641
|
}
|
|
580
642
|
|
|
581
|
-
export { type AgentMap, type App, type Balance, type Consensus, type CreateTask, type Escalation, type MapCheckpoint, type MapDraft, type MapKey, type MapNode, type Mix, type Option, Plures, PluresError, type Release, type Result, type Route, type Routing, type RoutingRule, type RoutingUpdate, type Rubric, type RubricParams, type Sample, type StoredAgentMap, type Task, type TaskStatus, type TeamTally, type TeamVote, type TestMix, VERSION, type WebhookEvent, Plures as default };
|
|
643
|
+
export { type AgentMap, type App, type Balance, type Consensus, type CreateTask, type Escalation, type GitHubLink, type MapCheckpoint, type MapDraft, type MapKey, type MapNode, type Mix, type Option, Plures, PluresError, type Release, type Result, type Route, type Routing, type RoutingRule, type RoutingUpdate, type Rubric, type RubricParams, type Sample, type StoredAgentMap, type Task, type TaskStatus, type TeamTally, type TeamVote, type TestMix, VERSION, type WebhookEvent, Plures as default };
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,199 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
// src/index.ts
|
|
2
|
+
var VERSION = "1.10.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
|
+
/** 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;
|
|
7
197
|
export {
|
|
8
198
|
Plures,
|
|
9
199
|
PluresError,
|
package/package.json
CHANGED
package/dist/chunk-23ZYPCPM.js
DELETED
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
// src/index.ts
|
|
2
|
-
var VERSION = "1.9.1";
|
|
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
|
-
/** 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
|
-
* Quality: people grade a sample of your agent's outputs against a rubric, so you see drift
|
|
120
|
-
* early, and compare two versions blind before you ship. Personal details are taken out first.
|
|
121
|
-
*/
|
|
122
|
-
quality = {
|
|
123
|
-
/** Send every output; the rubric's sample rate decides which ones people grade. */
|
|
124
|
-
sample: (p) => this.req("POST", "/api/v1/samples", p),
|
|
125
|
-
/** A blind release check: a is the current version's output, b the new one's, for the same input. */
|
|
126
|
-
compare: (p) => this.req("POST", "/api/v1/comparisons", p),
|
|
127
|
-
/** How often people preferred the new version, with a 95% range and a verdict. */
|
|
128
|
-
release: (name, opts = {}) => this.req("GET", `/api/v1/releases/${encodeURIComponent(name)}${opts.app ? `?app=${encodeURIComponent(opts.app)}` : ""}`),
|
|
129
|
-
rubrics: {
|
|
130
|
-
list: (opts = {}) => this.req("GET", `/api/v1/rubrics${opts.app ? `?app=${encodeURIComponent(opts.app)}` : ""}`),
|
|
131
|
-
/** { name: "reply_quality", question: "Is this reply correct and kind?", options: ["Good", "Okay", "Bad"], sample_rate: 0.05 } */
|
|
132
|
-
create: (p) => this.req("POST", "/api/v1/rubrics", p),
|
|
133
|
-
retrieve: (name, opts = {}) => this.req("GET", `/api/v1/rubrics/${encodeURIComponent(name)}${opts.app ? `?app=${encodeURIComponent(opts.app)}` : ""}`),
|
|
134
|
-
update: (name, p) => this.req("PATCH", `/api/v1/rubrics/${encodeURIComponent(name)}${p.app ? `?app=${encodeURIComponent(p.app)}` : ""}`, p),
|
|
135
|
-
del: (name, opts = {}) => this.req("DELETE", `/api/v1/rubrics/${encodeURIComponent(name)}${opts.app ? `?app=${encodeURIComponent(opts.app)}` : ""}`)
|
|
136
|
-
},
|
|
137
|
-
/** Graded samples as JSON lines (input, output, grade, score, agreement), for fine tuning or an eval set. */
|
|
138
|
-
export: async (rubric, opts = {}) => {
|
|
139
|
-
const q = new URLSearchParams(Object.entries({ app: opts.app ?? this.app ?? void 0, since: opts.since }).filter(([, v]) => v));
|
|
140
|
-
const r = await fetch(`${this.base}/api/v1/rubrics/${encodeURIComponent(rubric)}/export${q.size ? `?${q}` : ""}`, { headers: { authorization: `Bearer ${this.apiKey}`, "user-agent": this.ua } });
|
|
141
|
-
if (!r.ok) {
|
|
142
|
-
const d = await r.json().catch(() => ({}));
|
|
143
|
-
throw new PluresError(d?.error?.message ?? `HTTP ${r.status}`, r.status, d?.error?.type ?? "api_error");
|
|
144
|
-
}
|
|
145
|
-
return (await r.text()).split("\n").filter(Boolean).map((l) => JSON.parse(l));
|
|
146
|
-
}
|
|
147
|
-
};
|
|
148
|
-
/** Your prepaid credit. A live task that could cost more than available_cents is refused with a 402 insufficient_credits error. */
|
|
149
|
-
balance = () => this.req("GET", "/api/v1/balance");
|
|
150
|
-
/** Creates a task and waits for people to answer it. */
|
|
151
|
-
async verify(params, opts = {}) {
|
|
152
|
-
const t = await this.tasks.create(params, { idempotencyKey: opts.idempotencyKey });
|
|
153
|
-
return t.status === "pending" ? this.tasks.waitFor(t.id, { timeoutMs: opts.timeoutMs }) : t;
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* Checks the Plures-Signature header on a webhook and returns the parsed event.
|
|
157
|
-
* Pass the raw request body exactly as received. Throws if the signature is wrong
|
|
158
|
-
* or older than toleranceSec.
|
|
159
|
-
*/
|
|
160
|
-
static async verifyWebhook(rawBody, signatureHeader, secret, toleranceSec = 300) {
|
|
161
|
-
const parts = Object.fromEntries((signatureHeader ?? "").split(",").map((p) => p.split("=", 2)));
|
|
162
|
-
const t = Number(parts.t);
|
|
163
|
-
if (!t || !parts.v1) throw new Error("Missing Plures-Signature header");
|
|
164
|
-
if (Math.abs(Date.now() / 1e3 - t) > toleranceSec) throw new Error("Webhook timestamp is too old");
|
|
165
|
-
const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
166
|
-
const mac = new Uint8Array(await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(`${t}.${rawBody}`)));
|
|
167
|
-
const expected = Array.from(mac, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
168
|
-
if (!timingSafeEqual(expected, parts.v1)) throw new Error("Webhook signature does not match");
|
|
169
|
-
return JSON.parse(rawBody);
|
|
170
|
-
}
|
|
171
|
-
};
|
|
172
|
-
function timingSafeEqual(a, b) {
|
|
173
|
-
if (a.length !== b.length) return false;
|
|
174
|
-
let diff = 0;
|
|
175
|
-
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
176
|
-
return diff === 0;
|
|
177
|
-
}
|
|
178
|
-
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
179
|
-
var index_default = Plures;
|
|
180
|
-
|
|
181
|
-
export {
|
|
182
|
-
VERSION,
|
|
183
|
-
PluresError,
|
|
184
|
-
Plures,
|
|
185
|
-
index_default
|
|
186
|
-
};
|