plures 1.11.0 → 1.13.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 CHANGED
@@ -49,8 +49,9 @@ 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
+ - In your terminal, `npx plures map` opens your map page so you can watch it build (`--no-open` to skip), keeps one live line of what it's reading, and ends by drawing the map's flows with the checks Plures suggests. On GitHub and other CI it prints a plain log.
52
53
  - `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.
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`.
54
+ - `plures.github.repos()`, `.connect(app, "owner/repo")`, `.turnOn(app)`, `.run(app)` (map now), `.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`; `npx plures map github run` maps again now.
54
55
  - `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.
55
56
  - `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.
56
57
  - `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.
@@ -1,5 +1,5 @@
1
1
  // src/index.ts
2
- var VERSION = "1.11.0";
2
+ var VERSION = "1.13.0";
3
3
  var PluresError = class extends Error {
4
4
  constructor(message, status, type) {
5
5
  super(message);
@@ -130,6 +130,8 @@ var Plures = class {
130
130
  /** Connects a repo ("owner/name"): sets its map key secret and opens the pull request. */
131
131
  connect: (app, repo) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app)}/github`, { repo }),
132
132
  /** Turns mapping on by merging the pull request. The repo's own rules (reviews, checks) still apply. */
133
+ /** Map now: runs the app's map workflow on GitHub. { update: true } first updates an older workflow. Watch it on the map page. */
134
+ run: (app, opts = {}) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app)}/github/run`, opts),
133
135
  turnOn: (app) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app)}/github/merge`, {}),
134
136
  /** Revokes the repo's map key. Delete the workflow file in the repo to stop runs. */
135
137
  disconnect: (app) => this.req("DELETE", `/api/v1/apps/${encodeURIComponent(app)}/github`)
package/dist/cli.cjs CHANGED
@@ -1,5 +1,27 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key2 of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key2) && key2 !== except)
13
+ __defProp(to, key2, { get: () => from[key2], enumerable: !(desc = __getOwnPropDesc(from, key2)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
3
25
 
4
26
  // src/cli.ts
5
27
  var import_node_child_process = require("child_process");
@@ -8,7 +30,7 @@ var import_node_path = require("path");
8
30
  var import_node_fs = require("fs");
9
31
 
10
32
  // src/index.ts
11
- var VERSION = "1.11.0";
33
+ var VERSION = "1.13.0";
12
34
  var PluresError = class extends Error {
13
35
  constructor(message, status, type) {
14
36
  super(message);
@@ -139,6 +161,8 @@ var Plures = class {
139
161
  /** Connects a repo ("owner/name"): sets its map key secret and opens the pull request. */
140
162
  connect: (app2, repo) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app2)}/github`, { repo }),
141
163
  /** Turns mapping on by merging the pull request. The repo's own rules (reviews, checks) still apply. */
164
+ /** Map now: runs the app's map workflow on GitHub. { update: true } first updates an older workflow. Watch it on the map page. */
165
+ run: (app2, opts = {}) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app2)}/github/run`, opts),
142
166
  turnOn: (app2) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app2)}/github/merge`, {}),
143
167
  /** Revokes the repo's map key. Delete the workflow file in the repo to stop runs. */
144
168
  disconnect: (app2) => this.req("DELETE", `/api/v1/apps/${encodeURIComponent(app2)}/github`)
@@ -205,6 +229,178 @@ function timingSafeEqual(a, b) {
205
229
  }
206
230
  var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
207
231
 
232
+ // src/term.ts
233
+ var out = process.stdout;
234
+ var fancy = Boolean(out.isTTY && !process.env.NO_COLOR && !process.env.CI && process.env.TERM !== "dumb");
235
+ var rgb = (r, g, b) => (s) => `\x1B[38;2;${r};${g};${b}m${s}\x1B[0m`;
236
+ var LANES = [rgb(91, 75, 255), rgb(74, 107, 250), rgb(50, 144, 238), rgb(25, 198, 230)];
237
+ var C = {
238
+ text: rgb(243, 244, 247),
239
+ dim: rgb(110, 116, 136),
240
+ mute: rgb(160, 166, 186),
241
+ coral: rgb(255, 142, 122),
242
+ violet: rgb(185, 180, 255),
243
+ cyan: rgb(127, 227, 244),
244
+ amber: rgb(245, 184, 74),
245
+ bold: (s) => `\x1B[1m${s}\x1B[22m`
246
+ };
247
+ var width = () => Math.max(40, Math.min(out.columns || 80, 110));
248
+ var visible = (s) => s.replace(/\x1b\[[0-9;]*m/g, "");
249
+ var cut = (s, n) => s.length > n ? `${s.slice(0, Math.max(1, n - 1))}\u2026` : s;
250
+ function banner(app2, where) {
251
+ const bar = LANES.map((c2, i) => c2("\u2501".repeat(8 - i))).join(" ");
252
+ out.write(`
253
+ ${bar}
254
+
255
+ ${C.bold(C.text("Plures"))} ${C.dim("map")} ${C.mute(app2)}
256
+ ${C.dim(where)}
257
+
258
+ `);
259
+ }
260
+ function liveLine() {
261
+ const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
262
+ let i = 0, count = 0, file = "", phase = "reading", timer = null;
263
+ const draw = () => {
264
+ const spin = LANES[Math.floor(i / 2) % 4](frames[i % frames.length]);
265
+ i++;
266
+ const left = phase === "drawing" ? ` ${spin} ${C.violet("Drawing the map")} ${C.dim(`from ${count} ${count === 1 ? "file" : "files"}`)}` : ` ${spin} ${C.bold(C.text(String(count).padStart(3)))} ${C.dim(count === 1 ? "file read " : "files read")} `;
267
+ const room = width() - visible(left).length - 2;
268
+ const f = phase === "reading" && file ? (() => {
269
+ const slash = file.lastIndexOf("/");
270
+ const s = cut(file, room);
271
+ return slash >= 0 && s === file ? `${C.dim(file.slice(0, slash + 1))}${C.text(file.slice(slash + 1))}` : C.text(s);
272
+ })() : "";
273
+ out.write(`\r\x1B[K${left}${f}`);
274
+ };
275
+ return {
276
+ start() {
277
+ out.write("\x1B[?25l");
278
+ draw();
279
+ timer = setInterval(draw, 80);
280
+ },
281
+ read(path) {
282
+ count++;
283
+ file = path;
284
+ },
285
+ drawing() {
286
+ phase = "drawing";
287
+ },
288
+ stop() {
289
+ if (timer) clearInterval(timer);
290
+ timer = null;
291
+ out.write("\r\x1B[K\x1B[?25h");
292
+ },
293
+ get count() {
294
+ return count;
295
+ }
296
+ };
297
+ }
298
+ if (fancy) for (const sig of ["SIGINT", "SIGTERM"]) process.once(sig, () => {
299
+ out.write("\x1B[?25h\n");
300
+ process.exit(130);
301
+ });
302
+ var ICON = { input: "\u25B8", model: "\u25CE", tool: "\u2315", decision: "\u25C7", action: "\u2197", data: "\u25A4", output: "\u21A6", human: "\u263A" };
303
+ function drawMap(m, url) {
304
+ const nodes = new Map(m.nodes.map((n) => [n.id, n]));
305
+ const cps = new Map(m.checkpoints.filter((c2) => c2.status !== "dismissed").map((c2) => [c2.at, c2]));
306
+ const W = width();
307
+ const nb = new Map(m.nodes.map((n) => [n.id, []]));
308
+ for (const e of m.edges) {
309
+ nb.get(e.from)?.push(e.to);
310
+ nb.get(e.to)?.push(e.from);
311
+ }
312
+ const seen = /* @__PURE__ */ new Set(), flows = [];
313
+ for (const n of m.nodes) {
314
+ if (seen.has(n.id)) continue;
315
+ const part = [], st = [n.id];
316
+ seen.add(n.id);
317
+ while (st.length) {
318
+ const v = st.pop();
319
+ part.push(v);
320
+ for (const w of nb.get(v) ?? []) if (!seen.has(w)) {
321
+ seen.add(w);
322
+ st.push(w);
323
+ }
324
+ }
325
+ flows.push(m.nodes.map((x) => x.id).filter((id) => part.includes(id)));
326
+ }
327
+ const chip = (id) => {
328
+ const n = nodes.get(id), c2 = cps.get(id);
329
+ const label = cut(n.label, 34);
330
+ const tag = !c2 ? "" : c2.status === "added" ? ` ${C.cyan("\u2713 checked")}` : ` ${C.violet("\u25CC check suggested")}`;
331
+ if (n.kind === "action") {
332
+ if (c2?.status === "added") return `${C.cyan(ICON.action)} ${C.text(label)}${tag}`;
333
+ const risky = n.risk === "high" || n.risk === "medium";
334
+ return `${risky ? C.coral(ICON.action) : C.mute(ICON.action)} ${risky ? C.coral(label) : C.mute(label)}${tag}`;
335
+ }
336
+ return `${C.dim(ICON[n.kind] ?? "\xB7")} ${C.mute(label)}${tag}`;
337
+ };
338
+ out.write("\n");
339
+ for (const ids of flows.filter((f) => f.length > 1)) {
340
+ const set = new Set(ids);
341
+ const es = m.edges.filter((e) => set.has(e.from) && set.has(e.to));
342
+ const layer = new Map(ids.map((id) => [id, 0]));
343
+ const order = [], state = /* @__PURE__ */ new Map(), back = /* @__PURE__ */ new Set();
344
+ const visit = (v) => {
345
+ state.set(v, 1);
346
+ for (const e of es.filter((x) => x.from === v)) {
347
+ if (state.get(e.to) === 1) back.add(`${v}>${e.to}`);
348
+ else if (!state.get(e.to)) visit(e.to);
349
+ }
350
+ state.set(v, 2);
351
+ order.push(v);
352
+ };
353
+ for (const id of ids) if (!state.get(id)) visit(id);
354
+ for (const v of order.reverse()) for (const e of es) if (e.from === v && !back.has(`${v}>${e.to}`)) layer.set(e.to, Math.max(layer.get(e.to), layer.get(v) + 1));
355
+ const rows = [];
356
+ for (const id of ids) (rows[layer.get(id)] ??= []).push(id);
357
+ const root = rows[0]?.length === 1 ? rows[0][0] : null;
358
+ out.write(` ${LANES[flows.indexOf(ids) % 4]("\u25CF")} ${C.bold(C.text(cut(root ? nodes.get(root).label : "Flow", W - 6)))}
359
+ `);
360
+ rows.forEach((row, ri) => {
361
+ if (!row || ri === 0 && root) return;
362
+ const lines = [[]];
363
+ let used = 6;
364
+ for (const id of row) {
365
+ const w = visible(chip(id)).length + 4;
366
+ if (used + w > W && lines[lines.length - 1].length) {
367
+ lines.push([]);
368
+ used = 6;
369
+ }
370
+ lines[lines.length - 1].push(id);
371
+ used += w;
372
+ }
373
+ for (const ln of lines) out.write(` ${C.dim("\u2502")} ${ln.map((id) => chip(id)).join(C.dim(" \xB7 "))}
374
+ `);
375
+ });
376
+ out.write("\n");
377
+ }
378
+ const actions = m.nodes.filter((n) => n.kind === "action");
379
+ const on = actions.filter((a) => cps.get(a.id)?.status === "added").length;
380
+ const sugg = m.checkpoints.filter((c2) => c2.status === "suggested").length;
381
+ out.write(` ${LANES.map((c2) => c2("\u2501")).join("")} ${C.bold(C.text(sugg ? `Plures found ${sugg} ${sugg === 1 ? "place" : "places"} a person should check.` : "Your map is ready."))}
382
+ `);
383
+ out.write(` ${C.mute(`${on} of ${actions.length} ${actions.length === 1 ? "action" : "actions"} with consequences ${on === 1 ? "has" : "have"} a human check.`)}
384
+
385
+ `);
386
+ out.write(` ${C.text("See it build:")} ${C.cyan(url)}
387
+
388
+ `);
389
+ }
390
+ async function openBrowser(url) {
391
+ const { spawn: spawn2 } = await import("child_process");
392
+ const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
393
+ try {
394
+ const p = spawn2(cmd, args, { stdio: "ignore", detached: true });
395
+ p.on("error", () => {
396
+ });
397
+ p.unref();
398
+ return true;
399
+ } catch {
400
+ return false;
401
+ }
402
+ }
403
+
208
404
  // src/cli.ts
209
405
  var argv = process.argv.slice(2);
210
406
  var flag = (name) => {
@@ -215,6 +411,7 @@ var flag = (name) => {
215
411
  return v;
216
412
  };
217
413
  var branch = flag("branch");
414
+ var noOpen = argv.includes("--no-open") ? (argv.splice(argv.indexOf("--no-open"), 1), true) : false;
218
415
  var app = flag("app") ?? process.env.PLURES_APP ?? "default";
219
416
  var key = flag("key") ?? process.env.PLURES_API_KEY;
220
417
  var base = flag("base-url") ?? process.env.PLURES_BASE_URL ?? "https://plures.si";
@@ -242,14 +439,15 @@ async function upload(file) {
242
439
  } catch {
243
440
  die(`${file} isn't valid JSON.`);
244
441
  }
245
- console.log(dim(`Sending the map to Plures. No source code is sent.`));
442
+ if (!fancy) console.log(dim(`Sending the map to Plures. No source code is sent.`));
246
443
  let m;
247
444
  try {
248
445
  m = await client().maps.upload(app, map2);
249
446
  } catch (e) {
250
447
  die(e instanceof PluresError ? `Plures said: ${e.message}` : String(e));
251
448
  }
252
- report(m);
449
+ if (fancy) drawMap(m, m.url ?? `${base}/dashboard/apps/${m.app}/map`);
450
+ else report(m);
253
451
  }
254
452
  function report(m) {
255
453
  const nodes = new Map(m.nodes.map((n) => [n.id, n]));
@@ -299,7 +497,12 @@ With plures-mcp connected, just ask: ${cyan("Map this app for Plures")}`);
299
497
  childEnv.ANTHROPIC_API_KEY = run.token;
300
498
  console.log(dim(run.free ? "Plures is mapping this repo. Your first map is free." : `Plures is mapping this repo, at most $${(run.max_cents / 100).toFixed(2)} from your credit.`));
301
499
  }
302
- console.log(`Reading this repo ${process.env.CI ? "on this runner" : "on your machine"} with Claude Code. ${dim("Only the map is sent to Plures.")}
500
+ const pageUrl = `${base}/dashboard/apps/${encodeURIComponent(app)}/map`;
501
+ if (fancy) {
502
+ banner(app, "Reading this repo on your machine with Claude Code. Only file names and the map go to Plures.");
503
+ if (key && !noOpen && await openBrowser(pageUrl)) console.log(` ${dim("Watch it live in your browser:")} ${cyan(pageUrl)}
504
+ `);
505
+ } else console.log(`Reading this repo ${process.env.CI ? "on this runner" : "on your machine"} with Claude Code. ${dim("Only the map is sent to Plures.")}
303
506
  `);
304
507
  const prompt = `${guide}
305
508
 
@@ -327,6 +530,8 @@ Write the map to ${FILE} in the current directory now. Read the code; don't run
327
530
  return r.startsWith("..") ? null : r.replace(/\\/g, "/");
328
531
  };
329
532
  let result = "";
533
+ const ticker = fancy ? liveLine() : null;
534
+ ticker?.start();
330
535
  const code = await new Promise((resolve) => {
331
536
  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
537
  let buf = "";
@@ -350,10 +555,12 @@ Write the map to ${FILE} in the current directory now. Read the code; don't run
350
555
  if (!f) continue;
351
556
  reads++;
352
557
  queue.push({ path: f, tool: "Read" });
353
- console.log(` ${dim(String(reads).padStart(3))} ${dim("reading")} ${f}`);
558
+ if (ticker) ticker.read(f);
559
+ else console.log(` ${dim(String(reads).padStart(3))} ${dim("reading")} ${f}`);
354
560
  } else if (b.name === "Write" && String(b.input?.file_path ?? "").endsWith(FILE)) {
355
561
  phase = "drawing";
356
- console.log(`
562
+ if (ticker) ticker.drawing();
563
+ else console.log(`
357
564
  ${violet("drawing")} the map`);
358
565
  }
359
566
  }
@@ -363,9 +570,11 @@ Write the map to ${FILE} in the current directory now. Read the code; don't run
363
570
  p.on("error", () => resolve(1));
364
571
  });
365
572
  clearInterval(tick);
366
- if (result) console.log(`
573
+ ticker?.stop();
574
+ if (result && !fancy) console.log(`
367
575
  ${result.trim()}
368
576
  `);
577
+ if (fancy) console.log(` ${cyan("\u2713")} ${reads} ${reads === 1 ? "file" : "files"} read. ${dim("Sending the map to Plures. No source code is sent.")}`);
369
578
  const ok = code === 0 && (0, import_node_fs.existsSync)(FILE) && (0, import_node_fs.statSync)(FILE).mtimeMs !== before;
370
579
  if (!ok) {
371
580
  phase = "failed";
@@ -410,6 +619,20 @@ Turn it on: ${cyan("npx plures map github on")}${app !== "default" ? ` --app ${a
410
619
  };
411
620
  try {
412
621
  if (!action || action === "status") return show(await p.github.status(app));
622
+ if (action === "run") {
623
+ let r;
624
+ try {
625
+ r = await p.github.run(app);
626
+ } catch (e) {
627
+ if (!(e instanceof PluresError && e.type === "workflow_outdated")) throw e;
628
+ console.log(dim("This repo's map workflow is from before Map now. Updating it, which merges a small change to the workflow file."));
629
+ r = await p.github.run(app, { update: true });
630
+ }
631
+ const url = `${base}/dashboard/apps/${encodeURIComponent(app)}/map`;
632
+ console.log(`${bold("Mapping now on GitHub.")} Watch it build: ${cyan(url)}`);
633
+ if (fancy && !noOpen) await openBrowser(url);
634
+ return void r;
635
+ }
413
636
  if (action === "on") {
414
637
  const r = await p.github.turnOn(app);
415
638
  console.log(r.already_merged ? "Mapping was already on." : `${bold("Mapping is on.")} The first run is starting on GitHub.`);
@@ -431,7 +654,7 @@ Turn it on: ${cyan("npx plures map github on")}${app !== "default" ? ` --app ${a
431
654
  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.`);
432
655
  return;
433
656
  }
434
- die("npx plures map github [connect [owner/repo] | on | off]");
657
+ die("npx plures map github [connect [owner/repo] | on | run | off]");
435
658
  } catch (e) {
436
659
  if (e instanceof PluresError) die(e.message);
437
660
  throw e;
@@ -448,6 +671,7 @@ async function main() {
448
671
  npx plures map workflow add a GitHub Action that maps on every push
449
672
  npx plures map github connect let Plures add that Action by pull request
450
673
  npx plures map github on merge it, which turns mapping on
674
+ npx plures map github run map now on GitHub, and watch it build
451
675
 
452
676
  Docs: https://plures.si/docs`);
453
677
  return;
package/dist/cli.js CHANGED
@@ -3,13 +3,187 @@ import {
3
3
  Plures,
4
4
  PluresError,
5
5
  VERSION
6
- } from "./chunk-WPSUIAVZ.js";
6
+ } from "./chunk-GIOZT6FB.js";
7
7
 
8
8
  // src/cli.ts
9
9
  import { spawnSync, spawn } from "child_process";
10
10
  import { randomBytes } from "crypto";
11
11
  import { relative, isAbsolute } from "path";
12
12
  import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "fs";
13
+
14
+ // src/term.ts
15
+ var out = process.stdout;
16
+ var fancy = Boolean(out.isTTY && !process.env.NO_COLOR && !process.env.CI && process.env.TERM !== "dumb");
17
+ var rgb = (r, g, b) => (s) => `\x1B[38;2;${r};${g};${b}m${s}\x1B[0m`;
18
+ var LANES = [rgb(91, 75, 255), rgb(74, 107, 250), rgb(50, 144, 238), rgb(25, 198, 230)];
19
+ var C = {
20
+ text: rgb(243, 244, 247),
21
+ dim: rgb(110, 116, 136),
22
+ mute: rgb(160, 166, 186),
23
+ coral: rgb(255, 142, 122),
24
+ violet: rgb(185, 180, 255),
25
+ cyan: rgb(127, 227, 244),
26
+ amber: rgb(245, 184, 74),
27
+ bold: (s) => `\x1B[1m${s}\x1B[22m`
28
+ };
29
+ var width = () => Math.max(40, Math.min(out.columns || 80, 110));
30
+ var visible = (s) => s.replace(/\x1b\[[0-9;]*m/g, "");
31
+ var cut = (s, n) => s.length > n ? `${s.slice(0, Math.max(1, n - 1))}\u2026` : s;
32
+ function banner(app2, where) {
33
+ const bar = LANES.map((c2, i) => c2("\u2501".repeat(8 - i))).join(" ");
34
+ out.write(`
35
+ ${bar}
36
+
37
+ ${C.bold(C.text("Plures"))} ${C.dim("map")} ${C.mute(app2)}
38
+ ${C.dim(where)}
39
+
40
+ `);
41
+ }
42
+ function liveLine() {
43
+ const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
44
+ let i = 0, count = 0, file = "", phase = "reading", timer = null;
45
+ const draw = () => {
46
+ const spin = LANES[Math.floor(i / 2) % 4](frames[i % frames.length]);
47
+ i++;
48
+ const left = phase === "drawing" ? ` ${spin} ${C.violet("Drawing the map")} ${C.dim(`from ${count} ${count === 1 ? "file" : "files"}`)}` : ` ${spin} ${C.bold(C.text(String(count).padStart(3)))} ${C.dim(count === 1 ? "file read " : "files read")} `;
49
+ const room = width() - visible(left).length - 2;
50
+ const f = phase === "reading" && file ? (() => {
51
+ const slash = file.lastIndexOf("/");
52
+ const s = cut(file, room);
53
+ return slash >= 0 && s === file ? `${C.dim(file.slice(0, slash + 1))}${C.text(file.slice(slash + 1))}` : C.text(s);
54
+ })() : "";
55
+ out.write(`\r\x1B[K${left}${f}`);
56
+ };
57
+ return {
58
+ start() {
59
+ out.write("\x1B[?25l");
60
+ draw();
61
+ timer = setInterval(draw, 80);
62
+ },
63
+ read(path) {
64
+ count++;
65
+ file = path;
66
+ },
67
+ drawing() {
68
+ phase = "drawing";
69
+ },
70
+ stop() {
71
+ if (timer) clearInterval(timer);
72
+ timer = null;
73
+ out.write("\r\x1B[K\x1B[?25h");
74
+ },
75
+ get count() {
76
+ return count;
77
+ }
78
+ };
79
+ }
80
+ if (fancy) for (const sig of ["SIGINT", "SIGTERM"]) process.once(sig, () => {
81
+ out.write("\x1B[?25h\n");
82
+ process.exit(130);
83
+ });
84
+ var ICON = { input: "\u25B8", model: "\u25CE", tool: "\u2315", decision: "\u25C7", action: "\u2197", data: "\u25A4", output: "\u21A6", human: "\u263A" };
85
+ function drawMap(m, url) {
86
+ const nodes = new Map(m.nodes.map((n) => [n.id, n]));
87
+ const cps = new Map(m.checkpoints.filter((c2) => c2.status !== "dismissed").map((c2) => [c2.at, c2]));
88
+ const W = width();
89
+ const nb = new Map(m.nodes.map((n) => [n.id, []]));
90
+ for (const e of m.edges) {
91
+ nb.get(e.from)?.push(e.to);
92
+ nb.get(e.to)?.push(e.from);
93
+ }
94
+ const seen = /* @__PURE__ */ new Set(), flows = [];
95
+ for (const n of m.nodes) {
96
+ if (seen.has(n.id)) continue;
97
+ const part = [], st = [n.id];
98
+ seen.add(n.id);
99
+ while (st.length) {
100
+ const v = st.pop();
101
+ part.push(v);
102
+ for (const w of nb.get(v) ?? []) if (!seen.has(w)) {
103
+ seen.add(w);
104
+ st.push(w);
105
+ }
106
+ }
107
+ flows.push(m.nodes.map((x) => x.id).filter((id) => part.includes(id)));
108
+ }
109
+ const chip = (id) => {
110
+ const n = nodes.get(id), c2 = cps.get(id);
111
+ const label = cut(n.label, 34);
112
+ const tag = !c2 ? "" : c2.status === "added" ? ` ${C.cyan("\u2713 checked")}` : ` ${C.violet("\u25CC check suggested")}`;
113
+ if (n.kind === "action") {
114
+ if (c2?.status === "added") return `${C.cyan(ICON.action)} ${C.text(label)}${tag}`;
115
+ const risky = n.risk === "high" || n.risk === "medium";
116
+ return `${risky ? C.coral(ICON.action) : C.mute(ICON.action)} ${risky ? C.coral(label) : C.mute(label)}${tag}`;
117
+ }
118
+ return `${C.dim(ICON[n.kind] ?? "\xB7")} ${C.mute(label)}${tag}`;
119
+ };
120
+ out.write("\n");
121
+ for (const ids of flows.filter((f) => f.length > 1)) {
122
+ const set = new Set(ids);
123
+ const es = m.edges.filter((e) => set.has(e.from) && set.has(e.to));
124
+ const layer = new Map(ids.map((id) => [id, 0]));
125
+ const order = [], state = /* @__PURE__ */ new Map(), back = /* @__PURE__ */ new Set();
126
+ const visit = (v) => {
127
+ state.set(v, 1);
128
+ for (const e of es.filter((x) => x.from === v)) {
129
+ if (state.get(e.to) === 1) back.add(`${v}>${e.to}`);
130
+ else if (!state.get(e.to)) visit(e.to);
131
+ }
132
+ state.set(v, 2);
133
+ order.push(v);
134
+ };
135
+ for (const id of ids) if (!state.get(id)) visit(id);
136
+ for (const v of order.reverse()) for (const e of es) if (e.from === v && !back.has(`${v}>${e.to}`)) layer.set(e.to, Math.max(layer.get(e.to), layer.get(v) + 1));
137
+ const rows = [];
138
+ for (const id of ids) (rows[layer.get(id)] ??= []).push(id);
139
+ const root = rows[0]?.length === 1 ? rows[0][0] : null;
140
+ out.write(` ${LANES[flows.indexOf(ids) % 4]("\u25CF")} ${C.bold(C.text(cut(root ? nodes.get(root).label : "Flow", W - 6)))}
141
+ `);
142
+ rows.forEach((row, ri) => {
143
+ if (!row || ri === 0 && root) return;
144
+ const lines = [[]];
145
+ let used = 6;
146
+ for (const id of row) {
147
+ const w = visible(chip(id)).length + 4;
148
+ if (used + w > W && lines[lines.length - 1].length) {
149
+ lines.push([]);
150
+ used = 6;
151
+ }
152
+ lines[lines.length - 1].push(id);
153
+ used += w;
154
+ }
155
+ for (const ln of lines) out.write(` ${C.dim("\u2502")} ${ln.map((id) => chip(id)).join(C.dim(" \xB7 "))}
156
+ `);
157
+ });
158
+ out.write("\n");
159
+ }
160
+ const actions = m.nodes.filter((n) => n.kind === "action");
161
+ const on = actions.filter((a) => cps.get(a.id)?.status === "added").length;
162
+ const sugg = m.checkpoints.filter((c2) => c2.status === "suggested").length;
163
+ out.write(` ${LANES.map((c2) => c2("\u2501")).join("")} ${C.bold(C.text(sugg ? `Plures found ${sugg} ${sugg === 1 ? "place" : "places"} a person should check.` : "Your map is ready."))}
164
+ `);
165
+ out.write(` ${C.mute(`${on} of ${actions.length} ${actions.length === 1 ? "action" : "actions"} with consequences ${on === 1 ? "has" : "have"} a human check.`)}
166
+
167
+ `);
168
+ out.write(` ${C.text("See it build:")} ${C.cyan(url)}
169
+
170
+ `);
171
+ }
172
+ async function openBrowser(url) {
173
+ const { spawn: spawn2 } = await import("child_process");
174
+ const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
175
+ try {
176
+ const p = spawn2(cmd, args, { stdio: "ignore", detached: true });
177
+ p.on("error", () => {
178
+ });
179
+ p.unref();
180
+ return true;
181
+ } catch {
182
+ return false;
183
+ }
184
+ }
185
+
186
+ // src/cli.ts
13
187
  var argv = process.argv.slice(2);
14
188
  var flag = (name) => {
15
189
  const i = argv.indexOf(`--${name}`);
@@ -19,6 +193,7 @@ var flag = (name) => {
19
193
  return v;
20
194
  };
21
195
  var branch = flag("branch");
196
+ var noOpen = argv.includes("--no-open") ? (argv.splice(argv.indexOf("--no-open"), 1), true) : false;
22
197
  var app = flag("app") ?? process.env.PLURES_APP ?? "default";
23
198
  var key = flag("key") ?? process.env.PLURES_API_KEY;
24
199
  var base = flag("base-url") ?? process.env.PLURES_BASE_URL ?? "https://plures.si";
@@ -46,14 +221,15 @@ async function upload(file) {
46
221
  } catch {
47
222
  die(`${file} isn't valid JSON.`);
48
223
  }
49
- console.log(dim(`Sending the map to Plures. No source code is sent.`));
224
+ if (!fancy) console.log(dim(`Sending the map to Plures. No source code is sent.`));
50
225
  let m;
51
226
  try {
52
227
  m = await client().maps.upload(app, map2);
53
228
  } catch (e) {
54
229
  die(e instanceof PluresError ? `Plures said: ${e.message}` : String(e));
55
230
  }
56
- report(m);
231
+ if (fancy) drawMap(m, m.url ?? `${base}/dashboard/apps/${m.app}/map`);
232
+ else report(m);
57
233
  }
58
234
  function report(m) {
59
235
  const nodes = new Map(m.nodes.map((n) => [n.id, n]));
@@ -103,7 +279,12 @@ With plures-mcp connected, just ask: ${cyan("Map this app for Plures")}`);
103
279
  childEnv.ANTHROPIC_API_KEY = run.token;
104
280
  console.log(dim(run.free ? "Plures is mapping this repo. Your first map is free." : `Plures is mapping this repo, at most $${(run.max_cents / 100).toFixed(2)} from your credit.`));
105
281
  }
106
- console.log(`Reading this repo ${process.env.CI ? "on this runner" : "on your machine"} with Claude Code. ${dim("Only the map is sent to Plures.")}
282
+ const pageUrl = `${base}/dashboard/apps/${encodeURIComponent(app)}/map`;
283
+ if (fancy) {
284
+ banner(app, "Reading this repo on your machine with Claude Code. Only file names and the map go to Plures.");
285
+ if (key && !noOpen && await openBrowser(pageUrl)) console.log(` ${dim("Watch it live in your browser:")} ${cyan(pageUrl)}
286
+ `);
287
+ } else console.log(`Reading this repo ${process.env.CI ? "on this runner" : "on your machine"} with Claude Code. ${dim("Only the map is sent to Plures.")}
107
288
  `);
108
289
  const prompt = `${guide}
109
290
 
@@ -131,6 +312,8 @@ Write the map to ${FILE} in the current directory now. Read the code; don't run
131
312
  return r.startsWith("..") ? null : r.replace(/\\/g, "/");
132
313
  };
133
314
  let result = "";
315
+ const ticker = fancy ? liveLine() : null;
316
+ ticker?.start();
134
317
  const code = await new Promise((resolve) => {
135
318
  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
319
  let buf = "";
@@ -154,10 +337,12 @@ Write the map to ${FILE} in the current directory now. Read the code; don't run
154
337
  if (!f) continue;
155
338
  reads++;
156
339
  queue.push({ path: f, tool: "Read" });
157
- console.log(` ${dim(String(reads).padStart(3))} ${dim("reading")} ${f}`);
340
+ if (ticker) ticker.read(f);
341
+ else console.log(` ${dim(String(reads).padStart(3))} ${dim("reading")} ${f}`);
158
342
  } else if (b.name === "Write" && String(b.input?.file_path ?? "").endsWith(FILE)) {
159
343
  phase = "drawing";
160
- console.log(`
344
+ if (ticker) ticker.drawing();
345
+ else console.log(`
161
346
  ${violet("drawing")} the map`);
162
347
  }
163
348
  }
@@ -167,9 +352,11 @@ Write the map to ${FILE} in the current directory now. Read the code; don't run
167
352
  p.on("error", () => resolve(1));
168
353
  });
169
354
  clearInterval(tick);
170
- if (result) console.log(`
355
+ ticker?.stop();
356
+ if (result && !fancy) console.log(`
171
357
  ${result.trim()}
172
358
  `);
359
+ if (fancy) console.log(` ${cyan("\u2713")} ${reads} ${reads === 1 ? "file" : "files"} read. ${dim("Sending the map to Plures. No source code is sent.")}`);
173
360
  const ok = code === 0 && existsSync(FILE) && statSync(FILE).mtimeMs !== before;
174
361
  if (!ok) {
175
362
  phase = "failed";
@@ -214,6 +401,20 @@ Turn it on: ${cyan("npx plures map github on")}${app !== "default" ? ` --app ${a
214
401
  };
215
402
  try {
216
403
  if (!action || action === "status") return show(await p.github.status(app));
404
+ if (action === "run") {
405
+ let r;
406
+ try {
407
+ r = await p.github.run(app);
408
+ } catch (e) {
409
+ if (!(e instanceof PluresError && e.type === "workflow_outdated")) throw e;
410
+ console.log(dim("This repo's map workflow is from before Map now. Updating it, which merges a small change to the workflow file."));
411
+ r = await p.github.run(app, { update: true });
412
+ }
413
+ const url = `${base}/dashboard/apps/${encodeURIComponent(app)}/map`;
414
+ console.log(`${bold("Mapping now on GitHub.")} Watch it build: ${cyan(url)}`);
415
+ if (fancy && !noOpen) await openBrowser(url);
416
+ return void r;
417
+ }
217
418
  if (action === "on") {
218
419
  const r = await p.github.turnOn(app);
219
420
  console.log(r.already_merged ? "Mapping was already on." : `${bold("Mapping is on.")} The first run is starting on GitHub.`);
@@ -235,7 +436,7 @@ Turn it on: ${cyan("npx plures map github on")}${app !== "default" ? ` --app ${a
235
436
  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.`);
236
437
  return;
237
438
  }
238
- die("npx plures map github [connect [owner/repo] | on | off]");
439
+ die("npx plures map github [connect [owner/repo] | on | run | off]");
239
440
  } catch (e) {
240
441
  if (e instanceof PluresError) die(e.message);
241
442
  throw e;
@@ -252,6 +453,7 @@ async function main() {
252
453
  npx plures map workflow add a GitHub Action that maps on every push
253
454
  npx plures map github connect let Plures add that Action by pull request
254
455
  npx plures map github on merge it, which turns mapping on
456
+ npx plures map github run map now on GitHub, and watch it build
255
457
 
256
458
  Docs: https://plures.si/docs`);
257
459
  return;
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.11.0";
29
+ var VERSION = "1.13.0";
30
30
  var PluresError = class extends Error {
31
31
  constructor(message, status, type) {
32
32
  super(message);
@@ -157,6 +157,8 @@ var Plures = class {
157
157
  /** Connects a repo ("owner/name"): sets its map key secret and opens the pull request. */
158
158
  connect: (app, repo) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app)}/github`, { repo }),
159
159
  /** Turns mapping on by merging the pull request. The repo's own rules (reviews, checks) still apply. */
160
+ /** Map now: runs the app's map workflow on GitHub. { update: true } first updates an older workflow. Watch it on the map page. */
161
+ run: (app, opts = {}) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app)}/github/run`, opts),
160
162
  turnOn: (app) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app)}/github/merge`, {}),
161
163
  /** Revokes the repo's map key. Delete the workflow file in the repo to stop runs. */
162
164
  disconnect: (app) => this.req("DELETE", `/api/v1/apps/${encodeURIComponent(app)}/github`)
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.11.0";
353
+ declare const VERSION = "1.13.0";
354
354
  type GitHubLink = {
355
355
  object: "github_link";
356
356
  app: string;
@@ -550,6 +550,15 @@ declare class Plures {
550
550
  /** Connects a repo ("owner/name"): sets its map key secret and opens the pull request. */
551
551
  connect: (app: string, repo: string) => Promise<GitHubLink>;
552
552
  /** Turns mapping on by merging the pull request. The repo's own rules (reviews, checks) still apply. */
553
+ /** Map now: runs the app's map workflow on GitHub. { update: true } first updates an older workflow. Watch it on the map page. */
554
+ run: (app: string, opts?: {
555
+ update?: boolean;
556
+ }) => Promise<{
557
+ object: "map_run_request";
558
+ app: string;
559
+ started: true;
560
+ updated_workflow: boolean;
561
+ }>;
553
562
  turnOn: (app: string) => Promise<{
554
563
  object: "github_merge";
555
564
  app: string;
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.11.0";
353
+ declare const VERSION = "1.13.0";
354
354
  type GitHubLink = {
355
355
  object: "github_link";
356
356
  app: string;
@@ -550,6 +550,15 @@ declare class Plures {
550
550
  /** Connects a repo ("owner/name"): sets its map key secret and opens the pull request. */
551
551
  connect: (app: string, repo: string) => Promise<GitHubLink>;
552
552
  /** Turns mapping on by merging the pull request. The repo's own rules (reviews, checks) still apply. */
553
+ /** Map now: runs the app's map workflow on GitHub. { update: true } first updates an older workflow. Watch it on the map page. */
554
+ run: (app: string, opts?: {
555
+ update?: boolean;
556
+ }) => Promise<{
557
+ object: "map_run_request";
558
+ app: string;
559
+ started: true;
560
+ updated_workflow: boolean;
561
+ }>;
553
562
  turnOn: (app: string) => Promise<{
554
563
  object: "github_merge";
555
564
  app: string;
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  PluresError,
4
4
  VERSION,
5
5
  index_default
6
- } from "./chunk-WPSUIAVZ.js";
6
+ } from "./chunk-GIOZT6FB.js";
7
7
  export {
8
8
  Plures,
9
9
  PluresError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plures",
3
- "version": "1.11.0",
3
+ "version": "1.13.0",
4
4
  "description": "Human judgment as an API for AI agents. Send a decision to ID verified people and get their consensus back.",
5
5
  "keywords": [
6
6
  "human-in-the-loop",