plures 1.11.0 → 1.12.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,6 +49,7 @@ if (event.type === "task.completed" && event.data.result?.agreement === 1) {
49
49
  - `new Plures(key, { app: "support-agent" })` or `PLURES_APP`: send everything to one of your apps. `plures.apps.list()`, `.create({ name, id? })`, `.retrieve(id)`, `.update(id, { name?, id?, default? })`, `.del(id)`. Each app keeps its own tasks, ledger view, map and quality.
50
50
  - `plures.maps.guide()`, `.retrieve(app)`, `.upload(app, map)`, `.setCheckpoint(app, id, status)`: the agent map, where your agent needs a person. Or run `npx plures map` in your repo: your own coding agent reads the code and only the map is sent.
51
51
  - `plures.maps.createKey(app, { label? })`, `.keys(app)`, `.revokeKey(app, id)`: map keys for CI, which can only read and upload that app's map. `.workflow(app, branch?)` returns the GitHub Action that maps on every push; `npx plures map workflow` writes it to `.github/workflows/plures-map.yml`. Or use **Map with GitHub** on the app's map page and Plures opens the pull request for you, then **Turn on mapping** merges it. `plures.maps.startRun(app)` and `.runs(app)`: in CI with no Claude key, `npx plures map` starts a run where Plures runs Claude (first map free, then Claude's cost plus 30%, at most $5).
52
+ - 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
54
  - `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
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.
@@ -1,5 +1,5 @@
1
1
  // src/index.ts
2
- var VERSION = "1.11.0";
2
+ var VERSION = "1.12.0";
3
3
  var PluresError = class extends Error {
4
4
  constructor(message, status, type) {
5
5
  super(message);
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.12.0";
12
34
  var PluresError = class extends Error {
13
35
  constructor(message, status, type) {
14
36
  super(message);
@@ -205,6 +227,178 @@ function timingSafeEqual(a, b) {
205
227
  }
206
228
  var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
207
229
 
230
+ // src/term.ts
231
+ var out = process.stdout;
232
+ var fancy = Boolean(out.isTTY && !process.env.NO_COLOR && !process.env.CI && process.env.TERM !== "dumb");
233
+ var rgb = (r, g, b) => (s) => `\x1B[38;2;${r};${g};${b}m${s}\x1B[0m`;
234
+ var LANES = [rgb(91, 75, 255), rgb(74, 107, 250), rgb(50, 144, 238), rgb(25, 198, 230)];
235
+ var C = {
236
+ text: rgb(243, 244, 247),
237
+ dim: rgb(110, 116, 136),
238
+ mute: rgb(160, 166, 186),
239
+ coral: rgb(255, 142, 122),
240
+ violet: rgb(185, 180, 255),
241
+ cyan: rgb(127, 227, 244),
242
+ amber: rgb(245, 184, 74),
243
+ bold: (s) => `\x1B[1m${s}\x1B[22m`
244
+ };
245
+ var width = () => Math.max(40, Math.min(out.columns || 80, 110));
246
+ var visible = (s) => s.replace(/\x1b\[[0-9;]*m/g, "");
247
+ var cut = (s, n) => s.length > n ? `${s.slice(0, Math.max(1, n - 1))}\u2026` : s;
248
+ function banner(app2, where) {
249
+ const bar = LANES.map((c2, i) => c2("\u2501".repeat(8 - i))).join(" ");
250
+ out.write(`
251
+ ${bar}
252
+
253
+ ${C.bold(C.text("Plures"))} ${C.dim("map")} ${C.mute(app2)}
254
+ ${C.dim(where)}
255
+
256
+ `);
257
+ }
258
+ function liveLine() {
259
+ const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
260
+ let i = 0, count = 0, file = "", phase = "reading", timer = null;
261
+ const draw = () => {
262
+ const spin = LANES[Math.floor(i / 2) % 4](frames[i % frames.length]);
263
+ i++;
264
+ 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")} `;
265
+ const room = width() - visible(left).length - 2;
266
+ const f = phase === "reading" && file ? (() => {
267
+ const slash = file.lastIndexOf("/");
268
+ const s = cut(file, room);
269
+ return slash >= 0 && s === file ? `${C.dim(file.slice(0, slash + 1))}${C.text(file.slice(slash + 1))}` : C.text(s);
270
+ })() : "";
271
+ out.write(`\r\x1B[K${left}${f}`);
272
+ };
273
+ return {
274
+ start() {
275
+ out.write("\x1B[?25l");
276
+ draw();
277
+ timer = setInterval(draw, 80);
278
+ },
279
+ read(path) {
280
+ count++;
281
+ file = path;
282
+ },
283
+ drawing() {
284
+ phase = "drawing";
285
+ },
286
+ stop() {
287
+ if (timer) clearInterval(timer);
288
+ timer = null;
289
+ out.write("\r\x1B[K\x1B[?25h");
290
+ },
291
+ get count() {
292
+ return count;
293
+ }
294
+ };
295
+ }
296
+ if (fancy) for (const sig of ["SIGINT", "SIGTERM"]) process.once(sig, () => {
297
+ out.write("\x1B[?25h\n");
298
+ process.exit(130);
299
+ });
300
+ var ICON = { input: "\u25B8", model: "\u25CE", tool: "\u2315", decision: "\u25C7", action: "\u2197", data: "\u25A4", output: "\u21A6", human: "\u263A" };
301
+ function drawMap(m, url) {
302
+ const nodes = new Map(m.nodes.map((n) => [n.id, n]));
303
+ const cps = new Map(m.checkpoints.filter((c2) => c2.status !== "dismissed").map((c2) => [c2.at, c2]));
304
+ const W = width();
305
+ const nb = new Map(m.nodes.map((n) => [n.id, []]));
306
+ for (const e of m.edges) {
307
+ nb.get(e.from)?.push(e.to);
308
+ nb.get(e.to)?.push(e.from);
309
+ }
310
+ const seen = /* @__PURE__ */ new Set(), flows = [];
311
+ for (const n of m.nodes) {
312
+ if (seen.has(n.id)) continue;
313
+ const part = [], st = [n.id];
314
+ seen.add(n.id);
315
+ while (st.length) {
316
+ const v = st.pop();
317
+ part.push(v);
318
+ for (const w of nb.get(v) ?? []) if (!seen.has(w)) {
319
+ seen.add(w);
320
+ st.push(w);
321
+ }
322
+ }
323
+ flows.push(m.nodes.map((x) => x.id).filter((id) => part.includes(id)));
324
+ }
325
+ const chip = (id) => {
326
+ const n = nodes.get(id), c2 = cps.get(id);
327
+ const label = cut(n.label, 34);
328
+ const tag = !c2 ? "" : c2.status === "added" ? ` ${C.cyan("\u2713 checked")}` : ` ${C.violet("\u25CC check suggested")}`;
329
+ if (n.kind === "action") {
330
+ if (c2?.status === "added") return `${C.cyan(ICON.action)} ${C.text(label)}${tag}`;
331
+ const risky = n.risk === "high" || n.risk === "medium";
332
+ return `${risky ? C.coral(ICON.action) : C.mute(ICON.action)} ${risky ? C.coral(label) : C.mute(label)}${tag}`;
333
+ }
334
+ return `${C.dim(ICON[n.kind] ?? "\xB7")} ${C.mute(label)}${tag}`;
335
+ };
336
+ out.write("\n");
337
+ for (const ids of flows.filter((f) => f.length > 1)) {
338
+ const set = new Set(ids);
339
+ const es = m.edges.filter((e) => set.has(e.from) && set.has(e.to));
340
+ const layer = new Map(ids.map((id) => [id, 0]));
341
+ const order = [], state = /* @__PURE__ */ new Map(), back = /* @__PURE__ */ new Set();
342
+ const visit = (v) => {
343
+ state.set(v, 1);
344
+ for (const e of es.filter((x) => x.from === v)) {
345
+ if (state.get(e.to) === 1) back.add(`${v}>${e.to}`);
346
+ else if (!state.get(e.to)) visit(e.to);
347
+ }
348
+ state.set(v, 2);
349
+ order.push(v);
350
+ };
351
+ for (const id of ids) if (!state.get(id)) visit(id);
352
+ 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));
353
+ const rows = [];
354
+ for (const id of ids) (rows[layer.get(id)] ??= []).push(id);
355
+ const root = rows[0]?.length === 1 ? rows[0][0] : null;
356
+ out.write(` ${LANES[flows.indexOf(ids) % 4]("\u25CF")} ${C.bold(C.text(cut(root ? nodes.get(root).label : "Flow", W - 6)))}
357
+ `);
358
+ rows.forEach((row, ri) => {
359
+ if (!row || ri === 0 && root) return;
360
+ const lines = [[]];
361
+ let used = 6;
362
+ for (const id of row) {
363
+ const w = visible(chip(id)).length + 4;
364
+ if (used + w > W && lines[lines.length - 1].length) {
365
+ lines.push([]);
366
+ used = 6;
367
+ }
368
+ lines[lines.length - 1].push(id);
369
+ used += w;
370
+ }
371
+ for (const ln of lines) out.write(` ${C.dim("\u2502")} ${ln.map((id) => chip(id)).join(C.dim(" \xB7 "))}
372
+ `);
373
+ });
374
+ out.write("\n");
375
+ }
376
+ const actions = m.nodes.filter((n) => n.kind === "action");
377
+ const on = actions.filter((a) => cps.get(a.id)?.status === "added").length;
378
+ const sugg = m.checkpoints.filter((c2) => c2.status === "suggested").length;
379
+ 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."))}
380
+ `);
381
+ out.write(` ${C.mute(`${on} of ${actions.length} ${actions.length === 1 ? "action" : "actions"} with consequences ${on === 1 ? "has" : "have"} a human check.`)}
382
+
383
+ `);
384
+ out.write(` ${C.text("See it build:")} ${C.cyan(url)}
385
+
386
+ `);
387
+ }
388
+ async function openBrowser(url) {
389
+ const { spawn: spawn2 } = await import("child_process");
390
+ const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
391
+ try {
392
+ const p = spawn2(cmd, args, { stdio: "ignore", detached: true });
393
+ p.on("error", () => {
394
+ });
395
+ p.unref();
396
+ return true;
397
+ } catch {
398
+ return false;
399
+ }
400
+ }
401
+
208
402
  // src/cli.ts
209
403
  var argv = process.argv.slice(2);
210
404
  var flag = (name) => {
@@ -215,6 +409,7 @@ var flag = (name) => {
215
409
  return v;
216
410
  };
217
411
  var branch = flag("branch");
412
+ var noOpen = argv.includes("--no-open") ? (argv.splice(argv.indexOf("--no-open"), 1), true) : false;
218
413
  var app = flag("app") ?? process.env.PLURES_APP ?? "default";
219
414
  var key = flag("key") ?? process.env.PLURES_API_KEY;
220
415
  var base = flag("base-url") ?? process.env.PLURES_BASE_URL ?? "https://plures.si";
@@ -242,14 +437,15 @@ async function upload(file) {
242
437
  } catch {
243
438
  die(`${file} isn't valid JSON.`);
244
439
  }
245
- console.log(dim(`Sending the map to Plures. No source code is sent.`));
440
+ if (!fancy) console.log(dim(`Sending the map to Plures. No source code is sent.`));
246
441
  let m;
247
442
  try {
248
443
  m = await client().maps.upload(app, map2);
249
444
  } catch (e) {
250
445
  die(e instanceof PluresError ? `Plures said: ${e.message}` : String(e));
251
446
  }
252
- report(m);
447
+ if (fancy) drawMap(m, m.url ?? `${base}/dashboard/apps/${m.app}/map`);
448
+ else report(m);
253
449
  }
254
450
  function report(m) {
255
451
  const nodes = new Map(m.nodes.map((n) => [n.id, n]));
@@ -299,7 +495,12 @@ With plures-mcp connected, just ask: ${cyan("Map this app for Plures")}`);
299
495
  childEnv.ANTHROPIC_API_KEY = run.token;
300
496
  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
497
  }
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.")}
498
+ const pageUrl = `${base}/dashboard/apps/${encodeURIComponent(app)}/map`;
499
+ if (fancy) {
500
+ banner(app, "Reading this repo on your machine with Claude Code. Only file names and the map go to Plures.");
501
+ if (key && !noOpen && await openBrowser(pageUrl)) console.log(` ${dim("Watch it live in your browser:")} ${cyan(pageUrl)}
502
+ `);
503
+ } 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
504
  `);
304
505
  const prompt = `${guide}
305
506
 
@@ -327,6 +528,8 @@ Write the map to ${FILE} in the current directory now. Read the code; don't run
327
528
  return r.startsWith("..") ? null : r.replace(/\\/g, "/");
328
529
  };
329
530
  let result = "";
531
+ const ticker = fancy ? liveLine() : null;
532
+ ticker?.start();
330
533
  const code = await new Promise((resolve) => {
331
534
  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
535
  let buf = "";
@@ -350,10 +553,12 @@ Write the map to ${FILE} in the current directory now. Read the code; don't run
350
553
  if (!f) continue;
351
554
  reads++;
352
555
  queue.push({ path: f, tool: "Read" });
353
- console.log(` ${dim(String(reads).padStart(3))} ${dim("reading")} ${f}`);
556
+ if (ticker) ticker.read(f);
557
+ else console.log(` ${dim(String(reads).padStart(3))} ${dim("reading")} ${f}`);
354
558
  } else if (b.name === "Write" && String(b.input?.file_path ?? "").endsWith(FILE)) {
355
559
  phase = "drawing";
356
- console.log(`
560
+ if (ticker) ticker.drawing();
561
+ else console.log(`
357
562
  ${violet("drawing")} the map`);
358
563
  }
359
564
  }
@@ -363,9 +568,11 @@ Write the map to ${FILE} in the current directory now. Read the code; don't run
363
568
  p.on("error", () => resolve(1));
364
569
  });
365
570
  clearInterval(tick);
366
- if (result) console.log(`
571
+ ticker?.stop();
572
+ if (result && !fancy) console.log(`
367
573
  ${result.trim()}
368
574
  `);
575
+ if (fancy) console.log(` ${cyan("\u2713")} ${reads} ${reads === 1 ? "file" : "files"} read. ${dim("Sending the map to Plures. No source code is sent.")}`);
369
576
  const ok = code === 0 && (0, import_node_fs.existsSync)(FILE) && (0, import_node_fs.statSync)(FILE).mtimeMs !== before;
370
577
  if (!ok) {
371
578
  phase = "failed";
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-GDI4KDAH.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";
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.12.0";
30
30
  var PluresError = class extends Error {
31
31
  constructor(message, status, type) {
32
32
  super(message);
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.12.0";
354
354
  type GitHubLink = {
355
355
  object: "github_link";
356
356
  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.12.0";
354
354
  type GitHubLink = {
355
355
  object: "github_link";
356
356
  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-GDI4KDAH.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.12.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",