plures 1.10.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/dist/cli.js CHANGED
@@ -1,213 +1,187 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ Plures,
4
+ PluresError,
5
+ VERSION
6
+ } from "./chunk-GDI4KDAH.js";
2
7
 
3
8
  // src/cli.ts
4
9
  import { spawnSync, spawn } from "child_process";
10
+ import { randomBytes } from "crypto";
11
+ import { relative, isAbsolute } from "path";
5
12
  import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "fs";
6
13
 
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
- }
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`
17
28
  };
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)}`)
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}`);
101
56
  };
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();
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");
110
74
  },
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();
75
+ get count() {
76
+ return count;
126
77
  }
127
78
  };
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");
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);
171
105
  }
172
- return (await r.text()).split("\n").filter(Boolean).map((l) => JSON.parse(l));
173
106
  }
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;
107
+ flows.push(m.nodes.map((x) => x.id).filter((id) => part.includes(id)));
181
108
  }
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);
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");
200
159
  }
201
- };
202
- function timingSafeEqual(a, b) {
203
- if (a.length !== b.length)
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 {
204
182
  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;
183
+ }
209
184
  }
210
- var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
211
185
 
212
186
  // src/cli.ts
213
187
  var argv = process.argv.slice(2);
@@ -219,6 +193,7 @@ var flag = (name) => {
219
193
  return v;
220
194
  };
221
195
  var branch = flag("branch");
196
+ var noOpen = argv.includes("--no-open") ? (argv.splice(argv.indexOf("--no-open"), 1), true) : false;
222
197
  var app = flag("app") ?? process.env.PLURES_APP ?? "default";
223
198
  var key = flag("key") ?? process.env.PLURES_API_KEY;
224
199
  var base = flag("base-url") ?? process.env.PLURES_BASE_URL ?? "https://plures.si";
@@ -246,14 +221,15 @@ async function upload(file) {
246
221
  } catch {
247
222
  die(`${file} isn't valid JSON.`);
248
223
  }
249
- 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.`));
250
225
  let m;
251
226
  try {
252
227
  m = await client().maps.upload(app, map2);
253
228
  } catch (e) {
254
229
  die(e instanceof PluresError ? `Plures said: ${e.message}` : String(e));
255
230
  }
256
- report(m);
231
+ if (fancy) drawMap(m, m.url ?? `${base}/dashboard/apps/${m.app}/map`);
232
+ else report(m);
257
233
  }
258
234
  function report(m) {
259
235
  const nodes = new Map(m.nodes.map((n) => [n.id, n]));
@@ -303,16 +279,89 @@ With plures-mcp connected, just ask: ${cyan("Map this app for Plures")}`);
303
279
  childEnv.ANTHROPIC_API_KEY = run.token;
304
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.`));
305
281
  }
306
- 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.")}
307
288
  `);
308
289
  const prompt = `${guide}
309
290
 
310
291
  Write the map to ${FILE} in the current directory now. Read the code; don't run it or change anything else.`;
292
+ const live = key ? client() : null;
293
+ const session = randomBytes(12).toString("base64url");
294
+ const source = process.env.GITHUB_ACTIONS ? "github" : "cli";
295
+ let queue = [], phase = "reading", sent = "", reads = 0;
296
+ const flush = async () => {
297
+ if (!live) return;
298
+ const events = queue;
299
+ queue = [];
300
+ if (!events.length && sent === phase) return;
301
+ sent = phase;
302
+ await live.maps.progress(app, { session, phase, source, events }).catch(() => {
303
+ });
304
+ };
305
+ const tick = setInterval(() => {
306
+ void flush();
307
+ }, 900);
308
+ void flush();
309
+ const rel = (p) => {
310
+ if (typeof p !== "string" || !p) return null;
311
+ const r = isAbsolute(p) ? relative(process.cwd(), p) : p;
312
+ return r.startsWith("..") ? null : r.replace(/\\/g, "/");
313
+ };
314
+ let result = "";
315
+ const ticker = fancy ? liveLine() : null;
316
+ ticker?.start();
311
317
  const code = await new Promise((resolve) => {
312
- const p = spawn("claude", ["-p", prompt, "--allowedTools", "Read,Grep,Glob,Write,Bash(git rev-parse:*),Bash(git remote:*)"], { stdio: ["ignore", "inherit", "inherit"], env: childEnv });
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 });
319
+ let buf = "";
320
+ p.stdout.on("data", (chunk) => {
321
+ buf += chunk.toString();
322
+ let i;
323
+ while ((i = buf.indexOf("\n")) >= 0) {
324
+ const line = buf.slice(0, i);
325
+ buf = buf.slice(i + 1);
326
+ let ev;
327
+ try {
328
+ ev = JSON.parse(line);
329
+ } catch {
330
+ continue;
331
+ }
332
+ if (ev.type === "result" && typeof ev.result === "string") result = ev.result;
333
+ for (const b of ev.type === "assistant" ? ev.message?.content ?? [] : []) {
334
+ if (b.type !== "tool_use") continue;
335
+ if (b.name === "Read") {
336
+ const f = rel(b.input?.file_path);
337
+ if (!f) continue;
338
+ reads++;
339
+ queue.push({ path: f, tool: "Read" });
340
+ if (ticker) ticker.read(f);
341
+ else console.log(` ${dim(String(reads).padStart(3))} ${dim("reading")} ${f}`);
342
+ } else if (b.name === "Write" && String(b.input?.file_path ?? "").endsWith(FILE)) {
343
+ phase = "drawing";
344
+ if (ticker) ticker.drawing();
345
+ else console.log(`
346
+ ${violet("drawing")} the map`);
347
+ }
348
+ }
349
+ }
350
+ });
313
351
  p.on("close", (n) => resolve(n ?? 1));
314
352
  p.on("error", () => resolve(1));
315
353
  });
354
+ clearInterval(tick);
355
+ ticker?.stop();
356
+ if (result && !fancy) console.log(`
357
+ ${result.trim()}
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.")}`);
360
+ const ok = code === 0 && existsSync(FILE) && statSync(FILE).mtimeMs !== before;
361
+ if (!ok) {
362
+ phase = "failed";
363
+ await flush();
364
+ } else await flush();
316
365
  if (code !== 0 || !existsSync(FILE) || statSync(FILE).mtimeMs === before) die(`Claude Code didn't write ${FILE}. Ask it directly, then run npx plures map upload.`);
317
366
  await upload(FILE);
318
367
  }
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.10.0";
29
+ var VERSION = "1.12.0";
30
30
  var PluresError = class extends Error {
31
31
  constructor(message, status, type) {
32
32
  super(message);
@@ -136,6 +136,8 @@ var Plures = class {
136
136
  /** Starts a map run where Plures runs Claude (CI with no Anthropic key). npx plures map does this. */
137
137
  startRun: (app) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app)}/map_runs`, {}),
138
138
  runs: (app) => this.req("GET", `/api/v1/apps/${encodeURIComponent(app)}/map_runs`),
139
+ /** While mapping, which files the coding agent is reading, shown live on the map page. File names only. npx plures map does this. */
140
+ progress: (app, p) => this.req("POST", `/api/v1/apps/${encodeURIComponent(app)}/map/progress`, p),
139
141
  /** The GitHub Actions workflow that maps a repo on every push to its main branch. */
140
142
  workflow: async (app, branch = "main") => {
141
143
  const r = await fetch(`${this.base}/api/v1/map_workflow?app=${encodeURIComponent(app)}&branch=${encodeURIComponent(branch)}`);
package/dist/index.d.cts CHANGED
@@ -350,7 +350,7 @@ type WebhookEvent = {
350
350
  created_at: string;
351
351
  data: Task;
352
352
  };
353
- declare const VERSION = "1.10.0";
353
+ declare const VERSION = "1.12.0";
354
354
  type GitHubLink = {
355
355
  object: "github_link";
356
356
  app: string;
@@ -507,6 +507,22 @@ declare class Plures {
507
507
  created_at: string;
508
508
  }[];
509
509
  }>;
510
+ /** While mapping, which files the coding agent is reading, shown live on the map page. File names only. npx plures map does this. */
511
+ progress: (app: string, p: {
512
+ session: string;
513
+ phase?: "reading" | "drawing" | "done" | "failed";
514
+ source?: "cli" | "github";
515
+ events?: {
516
+ path: string;
517
+ tool?: string;
518
+ }[];
519
+ }) => Promise<{
520
+ object: "map_progress";
521
+ app: string;
522
+ session: string;
523
+ phase: string;
524
+ received: number;
525
+ }>;
510
526
  /** The GitHub Actions workflow that maps a repo on every push to its main branch. */
511
527
  workflow: (app: string, branch?: string) => Promise<string>;
512
528
  };
package/dist/index.d.ts CHANGED
@@ -350,7 +350,7 @@ type WebhookEvent = {
350
350
  created_at: string;
351
351
  data: Task;
352
352
  };
353
- declare const VERSION = "1.10.0";
353
+ declare const VERSION = "1.12.0";
354
354
  type GitHubLink = {
355
355
  object: "github_link";
356
356
  app: string;
@@ -507,6 +507,22 @@ declare class Plures {
507
507
  created_at: string;
508
508
  }[];
509
509
  }>;
510
+ /** While mapping, which files the coding agent is reading, shown live on the map page. File names only. npx plures map does this. */
511
+ progress: (app: string, p: {
512
+ session: string;
513
+ phase?: "reading" | "drawing" | "done" | "failed";
514
+ source?: "cli" | "github";
515
+ events?: {
516
+ path: string;
517
+ tool?: string;
518
+ }[];
519
+ }) => Promise<{
520
+ object: "map_progress";
521
+ app: string;
522
+ session: string;
523
+ phase: string;
524
+ received: number;
525
+ }>;
510
526
  /** The GitHub Actions workflow that maps a repo on every push to its main branch. */
511
527
  workflow: (app: string, branch?: string) => Promise<string>;
512
528
  };