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/README.md +2 -0
- package/dist/chunk-GDI4KDAH.js +205 -0
- package/dist/cli.cjs +290 -26
- package/dist/cli.js +246 -197
- package/dist/index.cjs +3 -1
- package/dist/index.d.cts +17 -1
- package/dist/index.d.ts +17 -1
- package/dist/index.js +6 -196
- package/package.json +1 -1
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/
|
|
8
|
-
var
|
|
9
|
-
var
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
203
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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", "
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
};
|