@letterstory/cli 0.98.2 → 0.98.4
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/lib/cli.mjs +22 -1
- package/lib/commands/repo.mjs +305 -0
- package/lib/commands/targets.mjs +27 -6
- package/lib/commands.mjs +1 -0
- package/lib/documents.mjs +2 -0
- package/package.json +1 -1
package/lib/cli.mjs
CHANGED
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
cmdMailers,
|
|
52
52
|
cmdAdmin,
|
|
53
53
|
cmdFeedback,
|
|
54
|
+
cmdRepo,
|
|
54
55
|
cmdEvals,
|
|
55
56
|
cmdOfferings,
|
|
56
57
|
cmdTargets,
|
|
@@ -58,7 +59,7 @@ import {
|
|
|
58
59
|
} from "./commands.mjs";
|
|
59
60
|
|
|
60
61
|
// Keep in sync with cli/package.json.
|
|
61
|
-
export const VERSION = "0.98.
|
|
62
|
+
export const VERSION = "0.98.4";
|
|
62
63
|
|
|
63
64
|
// Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
|
|
64
65
|
// can't accidentally swallow the id as --json's value. Every `type: "bool"` flag (and its
|
|
@@ -144,6 +145,8 @@ export const BOOLEAN_FLAGS = new Set([
|
|
|
144
145
|
"no-enrich",
|
|
145
146
|
// `offerings set-collection <id> --none` (the org itself is the subject again).
|
|
146
147
|
"none",
|
|
148
|
+
// `repo set-site --repo <owner/name> --site <host> --not` (the repo does not build that site).
|
|
149
|
+
"not",
|
|
147
150
|
// `prospects import|source --one-per-company`, `prospects source --enrich`, `prospects import --find-emails`.
|
|
148
151
|
"one-per-company",
|
|
149
152
|
"enrich",
|
|
@@ -1097,6 +1100,23 @@ Insights:
|
|
|
1097
1100
|
insights access [--period 14d|30d|90d] [--collection <uuid>]
|
|
1098
1101
|
Page access: who's reading your phantom sites
|
|
1099
1102
|
|
|
1103
|
+
Repo changes (Claude Code edits your repo in a sandbox; the Letterstory GitHub App opens a PR):
|
|
1104
|
+
repo repos Repos the Letterstory GitHub App can reach
|
|
1105
|
+
repo start --repo <owner/name> --instructions <text>|--instructions-file <path|->
|
|
1106
|
+
[--branch <base>] [--attach <path>]... [--attach-url <name>=<https url>]... [--wait]
|
|
1107
|
+
Start a change (attached files go to the sandbox);
|
|
1108
|
+
--wait follows it to the PR
|
|
1109
|
+
repo fix-audit --issue <issue-type> [--audit <id>] [--branch <base>] [--wait]
|
|
1110
|
+
One PR fixing a site-audit issue on every affected page,
|
|
1111
|
+
in the repo verified to build the site
|
|
1112
|
+
repo sites [--detect [--force]] Which website each repo builds, and the evidence
|
|
1113
|
+
repo set-site --repo <owner/name> --site <host> [--not]
|
|
1114
|
+
Say which site a repo builds (--not: correct a wrong label)
|
|
1115
|
+
repo reset-site --repo <owner/name> --site <host> Back to automatic detection
|
|
1116
|
+
repo status <run-id> Phase, spend, PR link and Claude's summary
|
|
1117
|
+
repo list [--status running|succeeded|no_changes|failed|cancelled] [--limit n] [--audit <id>]
|
|
1118
|
+
repo cancel <run-id> Stop it and delete the sandbox; no PR
|
|
1119
|
+
|
|
1100
1120
|
Feedback (tell us what's broken, missing, unclear, or wanted):
|
|
1101
1121
|
feedback submit --category bug|missing_feature|unclear|feature_request|praise|other
|
|
1102
1122
|
[--message <text>|--body <text>|--file <path>|--file -] [--rating 1-5]
|
|
@@ -1201,6 +1221,7 @@ const CLIENT_COMMANDS = {
|
|
|
1201
1221
|
mail: cmdMail,
|
|
1202
1222
|
mailers: cmdMailers,
|
|
1203
1223
|
feedback: cmdFeedback,
|
|
1224
|
+
repo: cmdRepo,
|
|
1204
1225
|
evals: cmdEvals,
|
|
1205
1226
|
visits: cmdVisits,
|
|
1206
1227
|
// Platform-scoped (staff key) — the only commands that act BEFORE an org exists.
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
// `repo` — changes to the org's own repos, made by Claude Code in a sandbox and opened as a
|
|
2
|
+
// pull request by the Letterstory GitHub App (src/lib/repo-changes). One subcommand per tool:
|
|
3
|
+
// repo start → start_repo_change repo status → get_repo_change
|
|
4
|
+
// repo list → list_repo_changes repo cancel → cancel_repo_change
|
|
5
|
+
// repo repos → list_github_app_repos
|
|
6
|
+
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
import { basename } from "node:path";
|
|
9
|
+
import { CliError } from "../client.mjs";
|
|
10
|
+
import {
|
|
11
|
+
argsFromFlags,
|
|
12
|
+
flagList,
|
|
13
|
+
flagStr,
|
|
14
|
+
followJob,
|
|
15
|
+
ok,
|
|
16
|
+
printResult,
|
|
17
|
+
readTextInput,
|
|
18
|
+
refuseUnknownFlags,
|
|
19
|
+
} from "./shared.mjs";
|
|
20
|
+
|
|
21
|
+
/** `--repo owner/name` → the two schema keys; --owner/--name also work. */
|
|
22
|
+
function splitRepo(flags) {
|
|
23
|
+
const full = flagStr(flags.repo);
|
|
24
|
+
if (full === undefined) return { owner: flagStr(flags.owner), name: flagStr(flags.name) };
|
|
25
|
+
const [owner, name, ...extra] = full.split("/");
|
|
26
|
+
if (!owner || !name || extra.length) throw new CliError("--repo must be owner/name");
|
|
27
|
+
return { owner, name };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const ARGS = {
|
|
31
|
+
start_repo_change: {
|
|
32
|
+
repo_owner: { value: (flags) => splitRepo(flags).owner },
|
|
33
|
+
repo_name: { value: (flags) => splitRepo(flags).name },
|
|
34
|
+
base_branch: "branch",
|
|
35
|
+
// --instructions, or a longer brief from --instructions-file <path> / - (stdin).
|
|
36
|
+
instructions: { value: (flags) => readTextInput(flags, "instructions", "instructions-file") },
|
|
37
|
+
// --attach <path> (uploaded first) and --attach-url <name>=<https url>, both repeatable.
|
|
38
|
+
files: { value: (_flags, _positionals, extra) => (extra.files?.length ? extra.files : undefined) },
|
|
39
|
+
},
|
|
40
|
+
create_repo_change_upload: { file_names: { internal: "the basenames of the --attach files" } },
|
|
41
|
+
list_repo_sites: {},
|
|
42
|
+
detect_repo_sites: { force: { flag: "force", type: "bool" } },
|
|
43
|
+
set_repo_site: {
|
|
44
|
+
repo_owner: { value: (flags) => splitRepo(flags).owner },
|
|
45
|
+
repo_name: { value: (flags) => splitRepo(flags).name },
|
|
46
|
+
site_host: { flag: "site", required: true },
|
|
47
|
+
builds: { flag: "not", type: "bool", invert: true },
|
|
48
|
+
},
|
|
49
|
+
reset_repo_site: {
|
|
50
|
+
repo_owner: { value: (flags) => splitRepo(flags).owner },
|
|
51
|
+
repo_name: { value: (flags) => splitRepo(flags).name },
|
|
52
|
+
site_host: { flag: "site", required: true },
|
|
53
|
+
},
|
|
54
|
+
fix_audit_issue: {
|
|
55
|
+
issue_type: { flag: "issue", required: true },
|
|
56
|
+
audit_id: "audit",
|
|
57
|
+
base_branch: "branch",
|
|
58
|
+
},
|
|
59
|
+
get_repo_change: { run_id: { positional: 0, required: true, label: "run-id" } },
|
|
60
|
+
cancel_repo_change: { run_id: { positional: 0, required: true, label: "run-id" } },
|
|
61
|
+
list_repo_changes: {
|
|
62
|
+
status: { flag: "status", enum: ["running", "succeeded", "no_changes", "failed", "cancelled"] },
|
|
63
|
+
limit: { flag: "limit", type: "num" },
|
|
64
|
+
audit_id: "audit",
|
|
65
|
+
},
|
|
66
|
+
list_github_app_repos: {},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const TERMINAL = new Set(["succeeded", "no_changes", "failed", "cancelled"]);
|
|
70
|
+
|
|
71
|
+
function describe(run) {
|
|
72
|
+
const lines = [
|
|
73
|
+
`${run.id} ${run.repo}@${run.baseBranch} ${run.status} (${run.phase}) $${run.costUsd.toFixed(2)}`,
|
|
74
|
+
];
|
|
75
|
+
if (run.prUrl) lines.push(` PR: ${run.prUrl}`);
|
|
76
|
+
if (run.errorCode) lines.push(` Error (${run.errorCode}): ${run.errorDetail ?? ""}`);
|
|
77
|
+
if (run.summary) lines.push(...run.summary.split("\n").map((l) => ` ${l}`));
|
|
78
|
+
return lines.join("\n");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function cmdRepo(ctx) {
|
|
82
|
+
const sub = ctx.positionals[0];
|
|
83
|
+
const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
|
|
84
|
+
switch (sub) {
|
|
85
|
+
case "start":
|
|
86
|
+
return repoStart(rest);
|
|
87
|
+
case "status":
|
|
88
|
+
return repoStatus(rest);
|
|
89
|
+
case "fix-audit":
|
|
90
|
+
return repoFixAudit(rest);
|
|
91
|
+
case "sites":
|
|
92
|
+
return repoSites(rest);
|
|
93
|
+
case "set-site":
|
|
94
|
+
return repoSetSite(rest);
|
|
95
|
+
case "reset-site":
|
|
96
|
+
return repoResetSite(rest);
|
|
97
|
+
case "list":
|
|
98
|
+
return repoList(rest);
|
|
99
|
+
case "cancel":
|
|
100
|
+
return repoCancel(rest);
|
|
101
|
+
case "repos":
|
|
102
|
+
return repoRepos(rest);
|
|
103
|
+
default:
|
|
104
|
+
throw new CliError(
|
|
105
|
+
`Unknown repo subcommand: ${sub ?? "(none)"}. Try: start, fix-audit, sites, set-site, reset-site, status, list, cancel, repos`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** `--attach-url name=https://…` → `{ name, url }`. */
|
|
111
|
+
function attachmentUrls(specs) {
|
|
112
|
+
return specs.map((spec) => {
|
|
113
|
+
const at = spec.indexOf("=");
|
|
114
|
+
if (at < 1) throw new CliError(`--attach-url must be name=https://… (got ${spec})`);
|
|
115
|
+
return { name: spec.slice(0, at), url: spec.slice(at + 1) };
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Reserve an upload per --attach file, PUT the bytes, and return `{ name, upload_path }` for each. */
|
|
120
|
+
async function uploadAttachments(ctx, paths) {
|
|
121
|
+
if (!paths.length) return [];
|
|
122
|
+
const names = paths.map((p) =>
|
|
123
|
+
basename(p)
|
|
124
|
+
.replace(/[^A-Za-z0-9._ -]/g, "-")
|
|
125
|
+
.replace(/^[^A-Za-z0-9]+/, "")
|
|
126
|
+
);
|
|
127
|
+
if (new Set(names).size !== names.length) throw new CliError("Two --attach files share a name; rename one.");
|
|
128
|
+
const bytes = paths.map((p) => {
|
|
129
|
+
try {
|
|
130
|
+
return readFileSync(p);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
throw new CliError(`Could not read --attach ${p}: ${err.message}`);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
const { uploads } = await ctx.client.callTool("create_repo_change_upload", { file_names: names });
|
|
136
|
+
for (const [i, slot] of uploads.entries()) {
|
|
137
|
+
const res = await fetch(slot.upload_url, {
|
|
138
|
+
method: "PUT",
|
|
139
|
+
headers: { "content-type": "application/octet-stream" },
|
|
140
|
+
body: bytes[i],
|
|
141
|
+
});
|
|
142
|
+
if (!res.ok) throw new CliError(`Uploading ${slot.name} failed (${res.status}).`);
|
|
143
|
+
ok(ctx, `Uploaded ${slot.name}`);
|
|
144
|
+
}
|
|
145
|
+
return uploads.map((slot) => ({ name: slot.name, upload_path: slot.path }));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function repoStart(ctx) {
|
|
149
|
+
const { client, flags, io } = ctx;
|
|
150
|
+
refuseUnknownFlags(ctx, {}, [
|
|
151
|
+
"repo",
|
|
152
|
+
"owner",
|
|
153
|
+
"name",
|
|
154
|
+
"branch",
|
|
155
|
+
"instructions",
|
|
156
|
+
"instructions-file",
|
|
157
|
+
"attach",
|
|
158
|
+
"attach-url",
|
|
159
|
+
"wait",
|
|
160
|
+
]);
|
|
161
|
+
const files = [
|
|
162
|
+
...(await uploadAttachments(ctx, flagList(flags.attach))),
|
|
163
|
+
...attachmentUrls(flagList(flags["attach-url"])),
|
|
164
|
+
];
|
|
165
|
+
const args = argsFromFlags(ARGS.start_repo_change, flags, ctx.positionals, { files });
|
|
166
|
+
if (!args.repo_owner || !args.repo_name) throw new CliError("Missing --repo owner/name");
|
|
167
|
+
if (!args.instructions) throw new CliError("Missing --instructions (or --instructions-file <path|->)");
|
|
168
|
+
|
|
169
|
+
const started = await client.callTool("start_repo_change", args);
|
|
170
|
+
ok(ctx, started.message);
|
|
171
|
+
if (!flags.wait) {
|
|
172
|
+
printResult(io, flags, started.run, describe);
|
|
173
|
+
return 0;
|
|
174
|
+
}
|
|
175
|
+
const { settled, value } = await followJob(
|
|
176
|
+
io,
|
|
177
|
+
async () => {
|
|
178
|
+
const polled = await client.callTool("get_repo_change", { run_id: started.run.id });
|
|
179
|
+
ok(ctx, `… ${polled.run.status} (${polled.run.phase})`);
|
|
180
|
+
return { done: TERMINAL.has(polled.run.status), value: polled.run };
|
|
181
|
+
},
|
|
182
|
+
{ intervalMs: 30000, ceilingMs: 60 * 60 * 1000 }
|
|
183
|
+
);
|
|
184
|
+
if (!settled) ok(ctx, `Still running; check later with: letterstory repo status ${started.run.id}`);
|
|
185
|
+
printResult(io, flags, value, describe);
|
|
186
|
+
return value.status === "failed" ? 1 : 0;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function followRun(ctx, run) {
|
|
190
|
+
const { client, flags, io } = ctx;
|
|
191
|
+
const { settled, value } = await followJob(
|
|
192
|
+
io,
|
|
193
|
+
async () => {
|
|
194
|
+
const polled = await client.callTool("get_repo_change", { run_id: run.id });
|
|
195
|
+
ok(ctx, `… ${polled.run.status} (${polled.run.phase})`);
|
|
196
|
+
return { done: TERMINAL.has(polled.run.status), value: polled.run };
|
|
197
|
+
},
|
|
198
|
+
{ intervalMs: 30000, ceilingMs: 60 * 60 * 1000 }
|
|
199
|
+
);
|
|
200
|
+
if (!settled) ok(ctx, `Still running; check later with: letterstory repo status ${run.id}`);
|
|
201
|
+
printResult(io, flags, value, describe);
|
|
202
|
+
return value.status === "failed" ? 1 : 0;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** `repo fix-audit --issue <type> [--audit <id>] [--wait]`: one audit issue → one PR, in the repo verified to build the site. */
|
|
206
|
+
async function repoFixAudit(ctx) {
|
|
207
|
+
const { client, flags, io } = ctx;
|
|
208
|
+
refuseUnknownFlags(ctx, {}, ["issue", "audit", "branch", "wait"]);
|
|
209
|
+
const result = await client.callTool(
|
|
210
|
+
"fix_audit_issue",
|
|
211
|
+
argsFromFlags(ARGS.fix_audit_issue, flags, ctx.positionals)
|
|
212
|
+
);
|
|
213
|
+
ok(ctx, result.message);
|
|
214
|
+
if (!flags.wait) {
|
|
215
|
+
printResult(io, flags, result.run, describe);
|
|
216
|
+
return 0;
|
|
217
|
+
}
|
|
218
|
+
return followRun(ctx, result.run);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** `repo sites [--detect] [--force]`: which website each repo builds, and why. */
|
|
222
|
+
async function repoSites(ctx) {
|
|
223
|
+
const { client, flags, io } = ctx;
|
|
224
|
+
const result = flags.detect
|
|
225
|
+
? await client.callTool("detect_repo_sites", argsFromFlags(ARGS.detect_repo_sites, flags))
|
|
226
|
+
: await client.callTool("list_repo_sites", argsFromFlags(ARGS.list_repo_sites, flags));
|
|
227
|
+
printResult(io, flags, result, (r) => {
|
|
228
|
+
const serving = r.sites.filter((s) => s.verdict === "serves");
|
|
229
|
+
const lines = serving.map((s) =>
|
|
230
|
+
s.decidedBy === "member"
|
|
231
|
+
? `${s.repo} builds ${s.siteHost} (set by a member)`
|
|
232
|
+
: `${s.repo} builds ${s.siteHost} (${Math.round(s.score * 100)}%: ${s.evidence.signals.join(", ")})`
|
|
233
|
+
);
|
|
234
|
+
if (r.failed?.length) lines.push(...r.failed.map((f) => `! ${f.repo}: ${f.error}`));
|
|
235
|
+
return lines.length ? lines.join("\n") : "No repo is verified to build any of this organization's sites yet.";
|
|
236
|
+
});
|
|
237
|
+
return 0;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** `repo set-site --repo owner/name --site acme.com [--not]`: say which site a repo builds (or doesn't). */
|
|
241
|
+
async function repoSetSite(ctx) {
|
|
242
|
+
const { client, flags, io } = ctx;
|
|
243
|
+
refuseUnknownFlags(ctx, {}, ["repo", "owner", "name", "site", "not"]);
|
|
244
|
+
const result = await client.callTool("set_repo_site", argsFromFlags(ARGS.set_repo_site, flags));
|
|
245
|
+
ok(ctx, result.message);
|
|
246
|
+
printResult(io, flags, result.site);
|
|
247
|
+
return 0;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** `repo reset-site --repo owner/name --site acme.com`: back to automatic detection. */
|
|
251
|
+
async function repoResetSite(ctx) {
|
|
252
|
+
const { client, flags, io } = ctx;
|
|
253
|
+
refuseUnknownFlags(ctx, {}, ["repo", "owner", "name", "site"]);
|
|
254
|
+
const result = await client.callTool("reset_repo_site", argsFromFlags(ARGS.reset_repo_site, flags));
|
|
255
|
+
ok(ctx, result.message);
|
|
256
|
+
printResult(io, flags, result);
|
|
257
|
+
return 0;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function repoStatus(ctx) {
|
|
261
|
+
const { client, flags, io } = ctx;
|
|
262
|
+
const result = await client.callTool(
|
|
263
|
+
"get_repo_change",
|
|
264
|
+
argsFromFlags(ARGS.get_repo_change, flags, ctx.positionals)
|
|
265
|
+
);
|
|
266
|
+
ok(ctx, result.message);
|
|
267
|
+
printResult(io, flags, result.run, describe);
|
|
268
|
+
return 0;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async function repoList(ctx) {
|
|
272
|
+
const { client, flags, io } = ctx;
|
|
273
|
+
const result = await client.callTool(
|
|
274
|
+
"list_repo_changes",
|
|
275
|
+
argsFromFlags(ARGS.list_repo_changes, flags, ctx.positionals)
|
|
276
|
+
);
|
|
277
|
+
printResult(io, flags, result.runs, (runs) =>
|
|
278
|
+
runs.length ? runs.map(describe).join("\n") : "No repo changes yet."
|
|
279
|
+
);
|
|
280
|
+
return 0;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function repoCancel(ctx) {
|
|
284
|
+
const { client, flags, io } = ctx;
|
|
285
|
+
const result = await client.callTool(
|
|
286
|
+
"cancel_repo_change",
|
|
287
|
+
argsFromFlags(ARGS.cancel_repo_change, flags, ctx.positionals)
|
|
288
|
+
);
|
|
289
|
+
ok(ctx, result.message);
|
|
290
|
+
printResult(io, flags, result.run, describe);
|
|
291
|
+
return 0;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function repoRepos(ctx) {
|
|
295
|
+
const { client, flags, io } = ctx;
|
|
296
|
+
const result = await client.callTool("list_github_app_repos", argsFromFlags(ARGS.list_github_app_repos, flags));
|
|
297
|
+
printResult(io, flags, result, (r) =>
|
|
298
|
+
r.repos.length
|
|
299
|
+
? r.repos.map((repo) => `${repo.fullName} (default ${repo.defaultBranch})`).join("\n")
|
|
300
|
+
: r.message
|
|
301
|
+
);
|
|
302
|
+
return 0;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export { ARGS };
|
package/lib/commands/targets.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// routes and staff Fleet do.
|
|
9
9
|
|
|
10
10
|
import { CliError } from "../client.mjs";
|
|
11
|
-
import { flagStr, flagNum, printResult, argsFromFlags, refuseUnknownFlags, ok } from "./shared.mjs";
|
|
11
|
+
import { flagStr, flagNum, flagList, printResult, argsFromFlags, refuseUnknownFlags, ok } from "./shared.mjs";
|
|
12
12
|
|
|
13
13
|
const STATUSES = ["active", "inactive", "achieved", "retired"];
|
|
14
14
|
const PRIORITIES = ["high", "normal", "low"];
|
|
@@ -25,8 +25,9 @@ function readOffering(flags) {
|
|
|
25
25
|
}
|
|
26
26
|
const offering = { value: readOffering };
|
|
27
27
|
|
|
28
|
-
// `--priority high|normal|low|default`: how
|
|
29
|
-
// voice (high = 3
|
|
28
|
+
// `--priority high|normal|low|default`: how often the target's ground is written across target
|
|
29
|
+
// slots, and how much its questions count in share of voice (high = 3, normal = 2, low = 1);
|
|
30
|
+
// default = back to high, a target's default.
|
|
30
31
|
function readPriority(flags) {
|
|
31
32
|
const v = flagStr(flags.priority);
|
|
32
33
|
if (v === undefined) {
|
|
@@ -55,6 +56,8 @@ export const ARGS = {
|
|
|
55
56
|
mention_framing: "framing",
|
|
56
57
|
offering_id: offering,
|
|
57
58
|
priority,
|
|
59
|
+
// `--stance <text>`, repeatable: one point of view per flag.
|
|
60
|
+
stances: { flag: "stance", type: "list" },
|
|
58
61
|
},
|
|
59
62
|
update_ranking_target: {
|
|
60
63
|
target_id: { positional: 0, label: "target-id", required: true },
|
|
@@ -67,6 +70,14 @@ export const ARGS = {
|
|
|
67
70
|
offering_id: offering,
|
|
68
71
|
position: { flag: "position", type: "num" },
|
|
69
72
|
priority,
|
|
73
|
+
// `--stance <text>` (repeatable) REPLACES the list; `--clear-stances` empties it.
|
|
74
|
+
stances: {
|
|
75
|
+
value: (flags) => {
|
|
76
|
+
if (flags["clear-stances"] === true) return [];
|
|
77
|
+
const list = flagList(flags.stance);
|
|
78
|
+
return list.length ? list : undefined;
|
|
79
|
+
},
|
|
80
|
+
},
|
|
70
81
|
},
|
|
71
82
|
set_ranking_target_cap: {
|
|
72
83
|
offering_id: {
|
|
@@ -117,8 +128,10 @@ export const ARGS = {
|
|
|
117
128
|
const USAGE = `Usage: targets <list|add|update|activate|deactivate|cap|suggest|subqueries|subquery>
|
|
118
129
|
targets list [--offering <offering-id|company>]
|
|
119
130
|
targets add <phrase> [--framing <f>] [--offering <offering-id|company>] [--priority high|normal|low]
|
|
131
|
+
[--stance <s>]... points of view held on this target's ground only
|
|
120
132
|
targets update <target-id> [--phrase <p>] [--framing <f> | --clear-framing] [--offering <offering-id|company>]
|
|
121
133
|
[--position <n>] [--state active|inactive|achieved|retired] [--priority high|normal|low|default]
|
|
134
|
+
[--stance <s>]... | --clear-stances replaces the target's stances
|
|
122
135
|
targets activate <target-id>
|
|
123
136
|
targets deactivate <target-id>
|
|
124
137
|
targets cap <offering-id|company> <n|default>
|
|
@@ -165,7 +178,8 @@ function formatScope(s) {
|
|
|
165
178
|
function formatTarget(t) {
|
|
166
179
|
const framing = t.mention_framing ? ` — as ${t.mention_framing}` : "";
|
|
167
180
|
const prio = t.priority ? ` (${t.priority} priority)` : "";
|
|
168
|
-
|
|
181
|
+
const stances = (t.stances ?? []).map((s) => `\n · ${s}`).join("");
|
|
182
|
+
return ` ${t.id} [${t.status}] ${t.phrase}${framing}${prio}${stances}`;
|
|
169
183
|
}
|
|
170
184
|
|
|
171
185
|
// Targets grouped under their scope, each headed by its cap and usage. A target on an
|
|
@@ -223,7 +237,7 @@ async function update(ctx, flags) {
|
|
|
223
237
|
const args = argsFromFlags(ARGS.update_ranking_target, flags, positionals);
|
|
224
238
|
if (Object.keys(args).length === 1) {
|
|
225
239
|
throw new CliError(
|
|
226
|
-
"Nothing to update — pass --phrase, --framing, --clear-framing, --offering, --position, --state or --priority."
|
|
240
|
+
"Nothing to update — pass --phrase, --framing, --clear-framing, --stance, --clear-stances, --offering, --position, --state or --priority."
|
|
227
241
|
);
|
|
228
242
|
}
|
|
229
243
|
const result = await client.callTool("update_ranking_target", args);
|
|
@@ -233,7 +247,14 @@ async function update(ctx, flags) {
|
|
|
233
247
|
}
|
|
234
248
|
|
|
235
249
|
async function targetsUpdate(ctx) {
|
|
236
|
-
refuseUnknownFlags(ctx, ARGS.update_ranking_target, [
|
|
250
|
+
refuseUnknownFlags(ctx, ARGS.update_ranking_target, [
|
|
251
|
+
"framing",
|
|
252
|
+
"clear-framing",
|
|
253
|
+
"stance",
|
|
254
|
+
"clear-stances",
|
|
255
|
+
"offering",
|
|
256
|
+
"priority",
|
|
257
|
+
]);
|
|
237
258
|
return update(ctx, ctx.flags);
|
|
238
259
|
}
|
|
239
260
|
|
package/lib/commands.mjs
CHANGED
|
@@ -40,5 +40,6 @@ export * from "./commands/offerings.mjs";
|
|
|
40
40
|
export * from "./commands/targets.mjs";
|
|
41
41
|
export * from "./commands/admin.mjs";
|
|
42
42
|
export * from "./commands/feedback.mjs";
|
|
43
|
+
export * from "./commands/repo.mjs";
|
|
43
44
|
export * from "./commands/evals.mjs";
|
|
44
45
|
export * from "./commands/visits.mjs";
|
package/lib/documents.mjs
CHANGED
|
@@ -61,6 +61,8 @@ export const DOCUMENT_FLAG_PAIRS = [
|
|
|
61
61
|
["text", "text-file"],
|
|
62
62
|
// An offering's (or the company's) manifesto is prose — a PDF or a .docx is fair game.
|
|
63
63
|
["manifesto", "manifesto-file"],
|
|
64
|
+
// A repo change's brief (repo.mjs `repo start`) — prose, so a .docx or PDF spec is fine.
|
|
65
|
+
["instructions", "instructions-file"],
|
|
64
66
|
];
|
|
65
67
|
|
|
66
68
|
/**
|