@letterstory/cli 0.31.1 → 0.33.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 +19 -0
- package/lib/cli.mjs +12 -1
- package/lib/commands/admin.mjs +46 -1
- package/lib/commands/directives.mjs +147 -0
- package/lib/commands.mjs +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -276,6 +276,25 @@ LETTERSTORY_STAFF_API_KEY=<your staff key with org:create> \
|
|
|
276
276
|
Everything the parent job does is also its own tool for existing orgs:
|
|
277
277
|
`ensure_collection_enhance_flow`, `provision_org_visuals`, `propose_seers` (`--args '{"apply":true}'`).
|
|
278
278
|
|
|
279
|
+
### Staff: ask the fleet a question
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
# Needs a STAFF key with the fleet:query capability. The answer is derived from
|
|
283
|
+
# read-only SQL across every org -- nothing here can write, and credential columns
|
|
284
|
+
# are unreadable, so this is safe to point at production because it cannot be
|
|
285
|
+
# anything else.
|
|
286
|
+
LETTERSTORY_STAFF_API_KEY=<your staff key with fleet:query> \
|
|
287
|
+
letterstory admin ask "which writing kernels are hooked up to the most clients?"
|
|
288
|
+
|
|
289
|
+
# --sql prints every query behind the answer, which is what makes a scripted answer
|
|
290
|
+
# checkable; --json emits the whole result (answer + steps) for a caller that wants
|
|
291
|
+
# the queries as data.
|
|
292
|
+
letterstory admin ask "which clients published nothing in the last 30 days?" --sql
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
This is the headless half of `/superadmin/ask` — same function, same schema — so a
|
|
296
|
+
long-running agent can watch the fleet on a schedule without a browser.
|
|
297
|
+
|
|
279
298
|
### Propose, then onboard a phantom
|
|
280
299
|
|
|
281
300
|
The Fleet wizard's Plan and Name screens as two commands: pick the beat, then the name,
|
package/lib/cli.mjs
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
cmdFlows,
|
|
25
25
|
cmdConnectors,
|
|
26
26
|
cmdStrategy,
|
|
27
|
+
cmdDirectives,
|
|
27
28
|
cmdProbes,
|
|
28
29
|
cmdSuggestions,
|
|
29
30
|
cmdOnboarding,
|
|
@@ -44,7 +45,7 @@ import {
|
|
|
44
45
|
} from "./commands.mjs";
|
|
45
46
|
|
|
46
47
|
// Keep in sync with cli/package.json.
|
|
47
|
-
export const VERSION = "0.
|
|
48
|
+
export const VERSION = "0.33.0";
|
|
48
49
|
|
|
49
50
|
// Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
|
|
50
51
|
// can't accidentally swallow the id as --json's value.
|
|
@@ -286,6 +287,10 @@ Strategy & onboarding:
|
|
|
286
287
|
strategy topics set --collection <uuid> (--topic <topic-id> | --suggestion <suggestion-id>)
|
|
287
288
|
suggestions list <collection_id> List pending strategy suggestions + their auto-apply countdown
|
|
288
289
|
suggestions approve | dismiss | snooze <verdict|proposal> <id> [--days <n>]
|
|
290
|
+
directives list <collection_id> What is steering this collection right now
|
|
291
|
+
directives propose <collection_id> <what you want> Read one instruction back as concrete levers (applies nothing)
|
|
292
|
+
directives apply <directive_id> --effect <id>... Commit the levers you kept (or --reason to decline them all)
|
|
293
|
+
directives off <directive_id> --reason <why> Stop a live instruction; the reason is read back by the next proposal
|
|
289
294
|
probes list [--collection <uuid>] [--active-only] [--kind mention|citation|page]
|
|
290
295
|
probes keep | pause | resume <probe_id> Keep = protect an AI probe from rotation (no pause)
|
|
291
296
|
probes edit <probe_id> <new question text> Fix/sharpen a generated probe
|
|
@@ -463,6 +468,11 @@ Domain onboarding (ONE SHOT: a bare domain in, a fully-configured organization o
|
|
|
463
468
|
${bin === "phantom" ? "onboard" : "phantom-onboard"} domain --status Latest domain onboarding on this org.
|
|
464
469
|
|
|
465
470
|
Admin (STAFF key — --staff-key / LETTERSTORY_STAFF_API_KEY; acts before an org exists):
|
|
471
|
+
admin ask "<question>" [--sql] [--json] Ask the whole fleet a question in English.
|
|
472
|
+
Answered from the live database with
|
|
473
|
+
read-only SQL across every org; --sql prints
|
|
474
|
+
the queries behind the answer. Needs the
|
|
475
|
+
fleet:query capability on the staff key.
|
|
466
476
|
admin create-org --domain <bare-domain> [--name "Acme"] [--test] [--no-key]
|
|
467
477
|
[--key-name N] [--capabilities a,b] [--onboard] [--watch] [+ the onboard domain flags]
|
|
468
478
|
The key exchange: create the client org from
|
|
@@ -640,6 +650,7 @@ const CLIENT_COMMANDS = {
|
|
|
640
650
|
flows: cmdFlows,
|
|
641
651
|
connectors: cmdConnectors,
|
|
642
652
|
strategy: cmdStrategy,
|
|
653
|
+
directives: cmdDirectives,
|
|
643
654
|
probes: cmdProbes,
|
|
644
655
|
suggestions: cmdSuggestions,
|
|
645
656
|
onboarding: cmdOnboarding,
|
package/lib/commands/admin.mjs
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// (`x-staff-api-key`, minted at Settings → Staff keys) against the superadmin REST
|
|
4
4
|
// routes, because they exist precisely to act BEFORE an org exists.
|
|
5
5
|
//
|
|
6
|
+
// admin ask "which kernels are hooked up to the most clients?" [--sql] [--json]
|
|
6
7
|
// admin create-org --domain acme.com [--name "Acme"] [--test] [--no-key] [--key-name N]
|
|
7
8
|
// [--capabilities a,b] [--onboard] [--about "…"] [--sitemap <url>]
|
|
8
9
|
// [--cms <provider>] [--posts N] [--seers suggest|skip] [--no-visuals]
|
|
@@ -17,6 +18,11 @@
|
|
|
17
18
|
// `phantom-onboard domain --watch` uses. Without --watch, watch it afterwards with
|
|
18
19
|
// LETTERSTORY_API_KEY=<printed key> letterstory phantom-onboard domain --watch --attach <job-id>
|
|
19
20
|
//
|
|
21
|
+
// `admin ask` is the headless half of /superadmin/ask: the same question, the same
|
|
22
|
+
// function, the same read-only SQL across every org — no browser. That projection is the
|
|
23
|
+
// point rather than a convenience, because a background agent that watches the fleet
|
|
24
|
+
// cannot open a console (AGENTS.md principle 5). Needs a staff key with `fleet:query`.
|
|
25
|
+
//
|
|
20
26
|
// The staff key comes from --staff-key or LETTERSTORY_STAFF_API_KEY; the base URL from the
|
|
21
27
|
// usual --url / LETTERSTORY_API_URL / config resolution.
|
|
22
28
|
|
|
@@ -178,8 +184,47 @@ async function cmdAdminCreateOrg(ctx) {
|
|
|
178
184
|
return watchDomainJob({ ...ctx, client: orgClient }, result.job_id);
|
|
179
185
|
}
|
|
180
186
|
|
|
187
|
+
/**
|
|
188
|
+
* `admin ask "<question>"` — one fleet question, one answer.
|
|
189
|
+
*
|
|
190
|
+
* Prints the prose by default. `--sql` also prints every query behind it, which is
|
|
191
|
+
* what makes a scripted answer checkable; `--json` emits the whole result for a
|
|
192
|
+
* caller that wants the steps as data.
|
|
193
|
+
*/
|
|
194
|
+
async function cmdAdminAsk(ctx) {
|
|
195
|
+
const question = ctx.positionals.join(" ").trim();
|
|
196
|
+
if (question.length < 3) {
|
|
197
|
+
throw new CliError('Usage: admin ask "which clients published nothing in the last 30 days?"');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const result = await staffPost(ctx, "/api/superadmin/ask", { question });
|
|
201
|
+
|
|
202
|
+
if (flagBool(ctx.flags.json)) {
|
|
203
|
+
printResult(ctx.io, ctx.flags, result);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// The partial-answer warning goes to stderr, so `admin ask ... > answer.md` still
|
|
208
|
+
// captures only the answer while a human at a terminal still sees the caveat.
|
|
209
|
+
if (result.stopped_by && result.stopped_by !== "end_turn") {
|
|
210
|
+
ctx.io.error(`! Run ended on ${result.stopped_by} — the answer below is partial.`);
|
|
211
|
+
}
|
|
212
|
+
ctx.io.log(result.answer || "(no answer)");
|
|
213
|
+
|
|
214
|
+
if (flagBool(ctx.flags.sql)) {
|
|
215
|
+
for (const step of result.steps ?? []) {
|
|
216
|
+
if (!step.sql) continue;
|
|
217
|
+
ctx.io.log(`\n-- ${step.tool}${step.ok ? "" : " (failed)"}${step.truncated ? " (truncated)" : ""}`);
|
|
218
|
+
ctx.io.log(step.sql);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
181
223
|
export async function cmdAdmin(ctx) {
|
|
182
224
|
const sub = ctx.positionals[0];
|
|
183
225
|
if (sub === "create-org") return cmdAdminCreateOrg({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
184
|
-
|
|
226
|
+
if (sub === "ask") return cmdAdminAsk({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
227
|
+
throw new CliError(
|
|
228
|
+
'Usage: admin create-org --domain <bare-domain> [--onboard] … | admin ask "<question>" (see --help)'
|
|
229
|
+
);
|
|
185
230
|
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// `directives` — tell one collection what to do, and see what you've already told it.
|
|
2
|
+
//
|
|
3
|
+
// Thin wrappers over the four MCP tools (propose / apply / list / revoke), which
|
|
4
|
+
// call the same lib functions the app's routes do — so a script or an agent gets
|
|
5
|
+
// exactly the sentence-to-levers interpretation a person gets, and reads back the
|
|
6
|
+
// same "what is steering this collection" list.
|
|
7
|
+
//
|
|
8
|
+
// `propose` PERSISTS NOTHING you'd notice: it writes one proposal row nothing
|
|
9
|
+
// consumes and returns the levers, so `--json | jq` is a legitimate way to ask
|
|
10
|
+
// "what would you do?" without doing it. Applying is a second, explicit call.
|
|
11
|
+
|
|
12
|
+
import { CliError } from "../client.mjs";
|
|
13
|
+
import { flagStr, flagList, requirePositional, printResult, argsFromFlags, ok } from "./shared.mjs";
|
|
14
|
+
|
|
15
|
+
// How every tool this module calls gets its arguments — see `argsFromFlags` in shared.mjs.
|
|
16
|
+
// `scripts/parity-check.ts` compares these keys against each tool's published input schema.
|
|
17
|
+
export const ARGS = {
|
|
18
|
+
propose_collection_directive: {
|
|
19
|
+
collection_id: { positional: 0, label: "collection-id", required: true },
|
|
20
|
+
// The instruction is the REST of the line, joined — quoting a sentence on a
|
|
21
|
+
// shell is exactly the friction that makes people type one word instead.
|
|
22
|
+
text: { value: (_flags, positionals) => positionals.slice(1).join(" ").trim() || undefined },
|
|
23
|
+
},
|
|
24
|
+
apply_collection_directive: {
|
|
25
|
+
directive_id: { positional: 0, label: "directive-id", required: true },
|
|
26
|
+
accepted_effect_ids: { flag: "effect", type: "list" },
|
|
27
|
+
rejection_reason: "reason",
|
|
28
|
+
},
|
|
29
|
+
list_collection_directives: {
|
|
30
|
+
collection_id: { positional: 0, label: "collection-id", required: true },
|
|
31
|
+
},
|
|
32
|
+
revoke_collection_directive: {
|
|
33
|
+
directive_id: { positional: 0, label: "directive-id", required: true },
|
|
34
|
+
reason: { flag: "reason", required: true },
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export async function cmdDirectives(ctx) {
|
|
39
|
+
const sub = ctx.positionals[0];
|
|
40
|
+
const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
|
|
41
|
+
switch (sub) {
|
|
42
|
+
case "list":
|
|
43
|
+
case "ls":
|
|
44
|
+
return directivesList(rest);
|
|
45
|
+
case "propose":
|
|
46
|
+
return directivesPropose(rest);
|
|
47
|
+
case "apply":
|
|
48
|
+
return directivesApply(rest);
|
|
49
|
+
case "off":
|
|
50
|
+
case "revoke":
|
|
51
|
+
return directivesRevoke(rest);
|
|
52
|
+
default:
|
|
53
|
+
throw new CliError("Usage: directives <list|propose|apply|off>");
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function formatEffect(e) {
|
|
58
|
+
const where = e.clusterName ? ` (${e.clusterName})` : "";
|
|
59
|
+
const life = e.ongoing ? "keeps applying" : "once";
|
|
60
|
+
return ` ${e.id} ${e.label}${where} [${life}]\n why: ${e.rationale}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function directivesList(ctx) {
|
|
64
|
+
const { client, flags, positionals, io } = ctx;
|
|
65
|
+
const result = await client.callTool(
|
|
66
|
+
"list_collection_directives",
|
|
67
|
+
argsFromFlags(ARGS.list_collection_directives, flags, positionals)
|
|
68
|
+
);
|
|
69
|
+
printResult(io, flags, result, (r) => {
|
|
70
|
+
const list = r.directives ?? [];
|
|
71
|
+
if (!list.length) return "Nothing is steering this collection.";
|
|
72
|
+
return list
|
|
73
|
+
.map((d) => {
|
|
74
|
+
const until = d.expiresAt ? ` stops ${d.expiresAt.slice(0, 10)}` : "";
|
|
75
|
+
const who = d.decidedBy ? "set by a person" : "set without a person in the loop";
|
|
76
|
+
const head = `${d.id} ${d.body}${until}\n (${who}, ${d.createdAt.slice(0, 10)})`;
|
|
77
|
+
return [head, ...(d.effects ?? []).map(formatEffect)].join("\n");
|
|
78
|
+
})
|
|
79
|
+
.join("\n\n");
|
|
80
|
+
});
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function directivesPropose(ctx) {
|
|
85
|
+
const { client, flags, positionals, io } = ctx;
|
|
86
|
+
if (!positionals.slice(1).join(" ").trim()) {
|
|
87
|
+
throw new CliError(
|
|
88
|
+
"Usage: directives propose <collection-id> <what you want>\n e.g. directives propose abc-123 focus on the pricing area"
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
const result = await client.callTool(
|
|
92
|
+
"propose_collection_directive",
|
|
93
|
+
argsFromFlags(ARGS.propose_collection_directive, flags, positionals)
|
|
94
|
+
);
|
|
95
|
+
printResult(io, flags, result, (r) => {
|
|
96
|
+
const p = r.proposal ?? {};
|
|
97
|
+
const lines = [p.interpretation || p.body, "", `directive: ${p.directiveId}`];
|
|
98
|
+
for (const e of p.effects ?? []) lines.push(formatEffect(e));
|
|
99
|
+
if (!(p.effects ?? []).length) lines.push(" (no levers — nothing here we can currently do)");
|
|
100
|
+
if (p.unsupported) lines.push("", `Can't express: ${p.unsupported}`);
|
|
101
|
+
for (const s of p.supersedes ?? []) lines.push("", `Accepting turns off: "${s.body}"`);
|
|
102
|
+
lines.push(
|
|
103
|
+
"",
|
|
104
|
+
`Then: directives apply ${p.directiveId} --effect <id> [--effect <id>]`,
|
|
105
|
+
` or: directives apply ${p.directiveId} --reason "why none of these"`
|
|
106
|
+
);
|
|
107
|
+
return lines.join("\n");
|
|
108
|
+
});
|
|
109
|
+
return 0;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function directivesApply(ctx) {
|
|
113
|
+
const { client, flags, positionals } = ctx;
|
|
114
|
+
const args = argsFromFlags(ARGS.apply_collection_directive, flags, positionals);
|
|
115
|
+
const effects = flagList(flags.effect);
|
|
116
|
+
// Declining everything is a legitimate answer, but it has to say why — the next
|
|
117
|
+
// proposal reads the reason back, and one that doesn't is a dismissal.
|
|
118
|
+
if (!effects.length && !flagStr(flags.reason)) {
|
|
119
|
+
throw new CliError('Pass --effect <id> to apply one, or --reason "why" to decline the lot.');
|
|
120
|
+
}
|
|
121
|
+
args.accepted_effect_ids = effects;
|
|
122
|
+
const result = await client.callTool("apply_collection_directive", args);
|
|
123
|
+
const applied = result.applied ?? {};
|
|
124
|
+
const failed = (applied.results ?? []).filter((r) => !r.ok);
|
|
125
|
+
if (!effects.length) {
|
|
126
|
+
ok(ctx, "Noted — we won't suggest that again.");
|
|
127
|
+
} else if (failed.length) {
|
|
128
|
+
ok(
|
|
129
|
+
ctx,
|
|
130
|
+
`Applied ${effects.length - failed.length}/${effects.length}. ${failed.map((f) => f.detail).join(" ")}`
|
|
131
|
+
);
|
|
132
|
+
} else {
|
|
133
|
+
ok(ctx, `Applied ${effects.length} change${effects.length === 1 ? "" : "s"}.`);
|
|
134
|
+
}
|
|
135
|
+
return 0;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function directivesRevoke(ctx) {
|
|
139
|
+
const { client, flags, positionals } = ctx;
|
|
140
|
+
requirePositional(positionals, 0, "directive-id");
|
|
141
|
+
await client.callTool(
|
|
142
|
+
"revoke_collection_directive",
|
|
143
|
+
argsFromFlags(ARGS.revoke_collection_directive, flags, positionals)
|
|
144
|
+
);
|
|
145
|
+
ok(ctx, "Turned off.");
|
|
146
|
+
return 0;
|
|
147
|
+
}
|
package/lib/commands.mjs
CHANGED
|
@@ -16,6 +16,7 @@ export * from "./commands/authors.mjs";
|
|
|
16
16
|
export * from "./commands/flows.mjs";
|
|
17
17
|
export * from "./commands/connectors.mjs";
|
|
18
18
|
export * from "./commands/strategy.mjs";
|
|
19
|
+
export * from "./commands/directives.mjs";
|
|
19
20
|
export * from "./commands/probes.mjs";
|
|
20
21
|
export * from "./commands/series.mjs";
|
|
21
22
|
export * from "./commands/insights.mjs";
|