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 +1 -0
- package/dist/{chunk-WPSUIAVZ.js → chunk-GDI4KDAH.js} +1 -1
- package/dist/cli.cjs +214 -7
- package/dist/cli.js +194 -7
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -49,6 +49,7 @@ if (event.type === "task.completed" && event.data.result?.agreement === 1) {
|
|
|
49
49
|
- `new Plures(key, { app: "support-agent" })` or `PLURES_APP`: send everything to one of your apps. `plures.apps.list()`, `.create({ name, id? })`, `.retrieve(id)`, `.update(id, { name?, id?, default? })`, `.del(id)`. Each app keeps its own tasks, ledger view, map and quality.
|
|
50
50
|
- `plures.maps.guide()`, `.retrieve(app)`, `.upload(app, map)`, `.setCheckpoint(app, id, status)`: the agent map, where your agent needs a person. Or run `npx plures map` in your repo: your own coding agent reads the code and only the map is sent.
|
|
51
51
|
- `plures.maps.createKey(app, { label? })`, `.keys(app)`, `.revokeKey(app, id)`: map keys for CI, which can only read and upload that app's map. `.workflow(app, branch?)` returns the GitHub Action that maps on every push; `npx plures map workflow` writes it to `.github/workflows/plures-map.yml`. Or use **Map with GitHub** on the app's map page and Plures opens the pull request for you, then **Turn on mapping** merges it. `plures.maps.startRun(app)` and `.runs(app)`: in CI with no Claude key, `npx plures map` starts a run where Plures runs Claude (first map free, then Claude's cost plus 30%, at most $5).
|
|
52
|
+
- 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.
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED