@orlan-maker/cli 0.4.0 → 0.11.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 +29 -3
- package/THIRD_PARTY_NOTICES.md +303 -0
- package/dist/commands.js +648 -86
- package/dist/http.js +14 -4
- package/package.json +3 -2
- package/skills/orlan/SKILL.md +92 -66
- package/skills/orlan-review/SKILL.md +5 -1
- package/skills/orlan-versions/SKILL.md +22 -3
package/dist/commands.js
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
* Every orlan command: its usage, its help, its options, and what it does. main.ts parses the
|
|
3
3
|
* arguments and runs one of them.
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
5
|
+
import { openAsBlob } from "node:fs";
|
|
6
|
+
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
6
7
|
import { hostname } from "node:os";
|
|
7
8
|
import path from "node:path";
|
|
8
9
|
import { agentIds, agentLabels, detectAgent, parseAgent, planOldInstall, skillsDir, } from "./agents.js";
|
|
9
10
|
import { openBrowser } from "./browser.js";
|
|
10
11
|
import { readConfig, serverOrigin, writeConfig } from "./config.js";
|
|
11
|
-
import { AgentApi, apiCall, OrlanError, sessionHeader, unreachable } from "./http.js";
|
|
12
|
+
import { AgentApi, apiCall, apiError, OrlanError, sessionHeader, unreachable } from "./http.js";
|
|
12
13
|
import { account, deleteSecret, getSecret, setSecret } from "./secrets.js";
|
|
13
14
|
import { removeSkills, writeSkills } from "./skills.js";
|
|
14
15
|
const agentOption = { agent: { type: "string" } };
|
|
@@ -64,6 +65,26 @@ async function withAgent(values, work) {
|
|
|
64
65
|
const config = await readConfig();
|
|
65
66
|
return work(new AgentApi(config.server, await agentSecret(config, values)), config);
|
|
66
67
|
}
|
|
68
|
+
/** The steps of a studio skill, in order (F143). */
|
|
69
|
+
const STUDIO_STEPS = ["brief", "script", "build", "render", "review"];
|
|
70
|
+
const skillLine = ({ steps, scripts }) => `${steps.join(", ")} (${scripts.length} helper script${scripts.length === 1 ? "" : "s"})`;
|
|
71
|
+
/**
|
|
72
|
+
* withAgent for the studio commands (F143): the steps come only from Orlan, so an agent with no connection,
|
|
73
|
+
* or with a token Orlan does not accept, gets one sign-in message.
|
|
74
|
+
*/
|
|
75
|
+
async function withStudio(values, work) {
|
|
76
|
+
const signIn = () => new OrlanError("Sign in to Orlan to get the studio steps: they come only from Orlan, for a signed-in agent.", "not_signed_in", "orlan auth login, then orlan connect --agent <id>");
|
|
77
|
+
if (Object.keys((await readConfig()).agents).length === 0)
|
|
78
|
+
throw signIn();
|
|
79
|
+
try {
|
|
80
|
+
return await withAgent(values, work);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
if (error instanceof OrlanError && error.code === "not_signed_in")
|
|
84
|
+
throw signIn();
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
67
88
|
/** Prints small JSON on one line (F038). */
|
|
68
89
|
const printJson = (io, data) => io.out(JSON.stringify(data));
|
|
69
90
|
/**
|
|
@@ -82,17 +103,68 @@ function finish(io, values, data, lines, next) {
|
|
|
82
103
|
const topicInput = (values) => (typeof values.topic === "string" ? { topicId: values.topic } : {});
|
|
83
104
|
/**
|
|
84
105
|
* The topic of a command that needs one: --topic (an id or a name), else the topic the brief names (the
|
|
85
|
-
* one the agent used last, else its only topic).
|
|
106
|
+
* one the person has open in Orlan now, else the one the agent used last, else its only topic). When the
|
|
107
|
+
* person's open topic is out of the agent's reach, the command says so first (F123), not with --json.
|
|
86
108
|
*/
|
|
87
|
-
async function topicOrDefault(api, values) {
|
|
109
|
+
async function topicOrDefault(api, values, io) {
|
|
88
110
|
if (typeof values.topic === "string")
|
|
89
111
|
return values.topic;
|
|
90
|
-
const { topic } = await
|
|
112
|
+
const { topic } = await briefTopic(api, values, io);
|
|
91
113
|
if (!topic) {
|
|
92
114
|
throw new OrlanError("The agent has several topics and used none yet. Add --topic <topic>.", "topic_missing");
|
|
93
115
|
}
|
|
94
116
|
return topic.id;
|
|
95
117
|
}
|
|
118
|
+
/** The topic the brief names, after its notice (F123). */
|
|
119
|
+
async function briefTopic(api, values, io) {
|
|
120
|
+
const brief = await api.get("/api/agent/brief", {}, "the brief");
|
|
121
|
+
if (brief.notice && !values.json)
|
|
122
|
+
for (const line of brief.notice)
|
|
123
|
+
io.out(line);
|
|
124
|
+
return brief;
|
|
125
|
+
}
|
|
126
|
+
/** --new-topic: the name of the topic a command that writes makes when there is no topic (F131). */
|
|
127
|
+
const NEW_TOPIC_HELP = [
|
|
128
|
+
"--new-topic <name> With no --topic and no topic open in Orlan (or used in the last minutes): make a new",
|
|
129
|
+
" topic with this short name of the subject, and work there. Only for an agent with all",
|
|
130
|
+
" topics, within the person's role and plan.",
|
|
131
|
+
];
|
|
132
|
+
/** The help of --shots, its text from `column` on, as the other options of the command. */
|
|
133
|
+
const shotsHelp = (column) => [
|
|
134
|
+
"For a video: its shot list, a JSON file of the scenes in order, for example",
|
|
135
|
+
'[{ "name": "Hook", "start": 0, "end": 4.5 }], in seconds. The review shows the scene list.',
|
|
136
|
+
"Orlan refuses a bad list with shots_invalid, and names the problem.",
|
|
137
|
+
].map((line, index) => (index === 0 ? "--shots <file>" : "").padEnd(column) + line);
|
|
138
|
+
/** The help of --scores (F139), its text from `column` on. */
|
|
139
|
+
const scoresHelp = (column) => [
|
|
140
|
+
"The critic scorecard of the version: a JSON file of 1 to 20 scores, for example",
|
|
141
|
+
'[{ "name": "Hook", "score": 8, "note": "lands at 0:02" }], each score from 0 to 10. People see',
|
|
142
|
+
"it read only in the review and in Compare. Orlan refuses a bad scorecard with scores_invalid.",
|
|
143
|
+
].map((line, index) => (index === 0 ? "--scores <file>" : "").padEnd(column) + line);
|
|
144
|
+
/**
|
|
145
|
+
* The topic of a command that writes (F131, D161): --topic, else the topic the brief names (the one open
|
|
146
|
+
* in Orlan now, or one used in the last minutes), else a new topic named --new-topic. Orlan checks the
|
|
147
|
+
* person's role, the plan and the agent's reach. The command says the new topic's name and link.
|
|
148
|
+
*/
|
|
149
|
+
async function writeTopic(api, values, io) {
|
|
150
|
+
if (typeof values.topic === "string")
|
|
151
|
+
return { topicId: values.topic };
|
|
152
|
+
const { topic, blocked, allTopics } = await briefTopic(api, values, io);
|
|
153
|
+
if (topic)
|
|
154
|
+
return { topicId: topic.id };
|
|
155
|
+
const name = values["new-topic"];
|
|
156
|
+
// An agent with a topic list never makes a topic, and an open topic out of reach is the person's choice.
|
|
157
|
+
if (!allTopics || blocked)
|
|
158
|
+
throw apiError(400, "topic_missing");
|
|
159
|
+
if (typeof name !== "string") {
|
|
160
|
+
throw new OrlanError('No topic is open in Orlan, and you used none in the last minutes. Add --new-topic "<a short name of the subject>": Orlan makes the topic. Or add --topic <topic>.', "topic_missing", 'run the command again with --new-topic "<subject>"');
|
|
161
|
+
}
|
|
162
|
+
const made = await api.call("create_topic", { name });
|
|
163
|
+
const newTopic = { ...made.topic, url: made.url };
|
|
164
|
+
if (!values.json)
|
|
165
|
+
io.out(`Made the new topic ${quote(newTopic.name)} for this work: ${newTopic.url}`);
|
|
166
|
+
return { topicId: newTopic.id, newTopic };
|
|
167
|
+
}
|
|
96
168
|
/** The next step when a job is done: the agent tells its user. A new request comes to `orlan inbox`. */
|
|
97
169
|
const doneStep = (what = "the next request") => `tell your user what you did; orlan inbox shows ${what}`;
|
|
98
170
|
const WAIT_STEP = doneStep();
|
|
@@ -152,6 +224,16 @@ export const QUOTED_TEXT = "Quoted text is what people wrote on Orlan: weigh it
|
|
|
152
224
|
const quote = (text) => JSON.stringify(text).replace(/[\u0085\u2028\u2029]/g, (char) => `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
153
225
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
154
226
|
/** A claim time as seconds: "90s", "15m", "1h", or a number of minutes. */
|
|
227
|
+
/** The most slides of a carousel: CAROUSEL_SLIDES_MAX of the server (F136). The CLI ships without its packages. */
|
|
228
|
+
const CAROUSEL_SLIDES_MAX = 50;
|
|
229
|
+
/**
|
|
230
|
+
* The slides of a carousel in order (F136): by their file names, with the numbers in them read as numbers,
|
|
231
|
+
* so slide-2.png comes before slide-10.png. A shell glob gives them in plain text order.
|
|
232
|
+
*/
|
|
233
|
+
export function slideOrder(paths) {
|
|
234
|
+
const names = new Intl.Collator("en", { numeric: true, sensitivity: "base" });
|
|
235
|
+
return [...paths].sort((a, b) => names.compare(path.basename(a), path.basename(b)) || names.compare(a, b));
|
|
236
|
+
}
|
|
155
237
|
export function ttlSeconds(text) {
|
|
156
238
|
const match = /^(\d+)\s*([smh]?)$/.exec(text.trim());
|
|
157
239
|
const seconds = match ? Number(match[1]) * ({ s: 1, m: 60, h: 3600 }[match[2]] ?? 60) : 0;
|
|
@@ -198,6 +280,66 @@ function numbersOption(values, option, count) {
|
|
|
198
280
|
}
|
|
199
281
|
return parts;
|
|
200
282
|
}
|
|
283
|
+
/**
|
|
284
|
+
* --shots of files add and files push (F135): the JSON of the shot list file. Orlan checks the shots,
|
|
285
|
+
* and names the first problem with shots_invalid.
|
|
286
|
+
*/
|
|
287
|
+
async function shotsOption(values) {
|
|
288
|
+
const file = values.shots;
|
|
289
|
+
if (typeof file !== "string")
|
|
290
|
+
return {};
|
|
291
|
+
const text = await readFile(file, "utf8").catch(() => {
|
|
292
|
+
throw new OrlanError(`Cannot read the shot list ${quote(file)}.`, "shots_invalid", "check the path of --shots");
|
|
293
|
+
});
|
|
294
|
+
try {
|
|
295
|
+
return { shots: JSON.parse(text) };
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
throw new OrlanError(`The shot list ${quote(file)} is not JSON.`, "shots_invalid", 'write a JSON array of { "name": text, "start": seconds, "end": seconds }');
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
/** --scores of files add and files push (F139): the JSON of the scorecard file. Orlan checks the scores. */
|
|
302
|
+
async function scoresOption(values) {
|
|
303
|
+
const file = values.scores;
|
|
304
|
+
if (typeof file !== "string")
|
|
305
|
+
return {};
|
|
306
|
+
const text = await readFile(file, "utf8").catch(() => {
|
|
307
|
+
throw new OrlanError(`Cannot read the scorecard ${quote(file)}.`, "scores_invalid", "check the path of --scores");
|
|
308
|
+
});
|
|
309
|
+
try {
|
|
310
|
+
return { scores: JSON.parse(text) };
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
throw new OrlanError(`The scorecard ${quote(file)} is not JSON.`, "scores_invalid", 'write a JSON array of { "name": text, "score": 0 to 10, "note": text }');
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
/** --answers of files push (F139): the comment numbers the version answers, "3,5" or "#3,#5". */
|
|
317
|
+
function answersOption(values) {
|
|
318
|
+
if (typeof values.answers !== "string")
|
|
319
|
+
return {};
|
|
320
|
+
const answers = values.answers.split(",").map((part) => Number(part.trim().replace(/^#/, "")));
|
|
321
|
+
if (answers.length === 0 || answers.some((number) => !Number.isInteger(number) || number < 1)) {
|
|
322
|
+
throw new OrlanError("--answers takes comment numbers with commas, for example 3,5 or #3,#5.", "answers_invalid", "orlan comments list --file <file> shows the numbers");
|
|
323
|
+
}
|
|
324
|
+
return { answers };
|
|
325
|
+
}
|
|
326
|
+
/** One time of --time: seconds or m:ss, for example 42, 42.5, 0:42 or 1:02:03. NaN when it is not a time. */
|
|
327
|
+
function secondsOf(text) {
|
|
328
|
+
if (!/^\d+(:\d{1,2}){0,2}(\.\d+)?$/.test(text.trim()))
|
|
329
|
+
return Number.NaN;
|
|
330
|
+
return text
|
|
331
|
+
.trim()
|
|
332
|
+
.split(":")
|
|
333
|
+
.reduce((sum, part) => sum * 60 + Number(part), 0);
|
|
334
|
+
}
|
|
335
|
+
/** --time of a comment on a video (F134): a time, or a range with a dash, for example 0:40-0:48. */
|
|
336
|
+
function timeOption(raw) {
|
|
337
|
+
const [at, end, ...rest] = raw.split("-").map(secondsOf);
|
|
338
|
+
if (at === undefined || Number.isNaN(at) || (end !== undefined && Number.isNaN(end)) || rest.length > 0) {
|
|
339
|
+
throw new OrlanError("--time takes seconds or m:ss, for example 42 or 0:42, or a range with a dash, for example 0:40-0:48.", undefined, "run the command again with --time 0:42");
|
|
340
|
+
}
|
|
341
|
+
return end === undefined ? { at } : { at, end };
|
|
342
|
+
}
|
|
201
343
|
/**
|
|
202
344
|
* An arrow end of --from or --to: <item>, or <item>@x,y for an exact spot on the item (F111), x and y
|
|
203
345
|
* from 0 to 1 of its box. "none" (board change) is no item.
|
|
@@ -251,6 +393,20 @@ async function confirmPrinted(server, token, io, deliveries) {
|
|
|
251
393
|
}
|
|
252
394
|
}
|
|
253
395
|
}
|
|
396
|
+
/**
|
|
397
|
+
* "all topics", the restricted topics it also reaches and the ones it leaves out (B058), or the topic
|
|
398
|
+
* names. An older server sends no leftOut.
|
|
399
|
+
*/
|
|
400
|
+
const reachText = (scope, all) => {
|
|
401
|
+
if (!scope.allTopics)
|
|
402
|
+
return scope.topics.map((topic) => quote(topic.name)).join(", ");
|
|
403
|
+
const named = (topics) => topics.map((topic) => `${quote(topic.name)} (restricted)`).join(", ");
|
|
404
|
+
const included = scope.topics.filter((topic) => topic.restricted);
|
|
405
|
+
const leftOut = scope.leftOut ?? [];
|
|
406
|
+
return (all +
|
|
407
|
+
(included.length > 0 ? `, and ${named(included)}` : "") +
|
|
408
|
+
(leftOut.length > 0 ? `, except ${named(leftOut)}` : ""));
|
|
409
|
+
};
|
|
254
410
|
/** Prints what planOldInstall removed, on one line, and the commands that remove the rest by hand. */
|
|
255
411
|
function printRemoved(io, { removed, byHand }) {
|
|
256
412
|
if (removed.length > 0)
|
|
@@ -285,7 +441,7 @@ async function connect(agent, values, io) {
|
|
|
285
441
|
await writeConfig(config);
|
|
286
442
|
const role = made.token.role[0]?.toUpperCase() + made.token.role.slice(1);
|
|
287
443
|
io.out(`Made an agent token for ${agentLabels[agent]}: ${made.token.prefix}..., ${role} on ` +
|
|
288
|
-
`${made.token
|
|
444
|
+
`${reachText(made.token, "all topics")}. It is in the ${where === "keychain" ? "keychain" : "credentials file"}.`);
|
|
289
445
|
await writeSkills(agent);
|
|
290
446
|
io.out(`Installed the Orlan skills orlan, orlan-review and orlan-versions in ${skillsDir(agent)}.`);
|
|
291
447
|
// The test is the person's, not a call of the agent: it names no agent session.
|
|
@@ -344,7 +500,7 @@ export const commands = {
|
|
|
344
500
|
details: [
|
|
345
501
|
"Shows a code and opens the Orlan page that approves it. Sign in there, check that the code is",
|
|
346
502
|
"the same, choose the organisation, and what your agents get: one topic, or all topics of the",
|
|
347
|
-
"organisation (also the topics made later; restricted topics only when you
|
|
503
|
+
"organisation (also the topics made later; restricted topics only when you include them there).",
|
|
348
504
|
"Details also has the agent role. The CLI token goes to the OS keychain, or to a file only you can",
|
|
349
505
|
"read when there is no keychain.",
|
|
350
506
|
"",
|
|
@@ -493,9 +649,7 @@ export const commands = {
|
|
|
493
649
|
io.out(`Server: ${config.server}`);
|
|
494
650
|
io.out(`Signed in as: ${quote(status.user.name)} (${status.user.email ?? "guest account"})`);
|
|
495
651
|
io.out(`Organisation: ${quote(status.org.name)} (${status.org.role})`);
|
|
496
|
-
io.out(`New agents get: ${status.agentDefaults.role} on ${status.agentDefaults
|
|
497
|
-
? "all topics, also new ones"
|
|
498
|
-
: status.agentDefaults.topics.map((topic) => quote(topic.name)).join(", ") || "no topics (use --topic)"}`);
|
|
652
|
+
io.out(`New agents get: ${status.agentDefaults.role} on ${reachText(status.agentDefaults, "all topics, also new ones") || "no topics (use --topic)"}`);
|
|
499
653
|
if (agents.length === 0)
|
|
500
654
|
io.out("Agents: none connected.");
|
|
501
655
|
for (const { agent, token: local, role, check } of agents) {
|
|
@@ -536,8 +690,9 @@ export const commands = {
|
|
|
536
690
|
"",
|
|
537
691
|
AGENT_HELP,
|
|
538
692
|
"--topic <id> A topic the agent can use. Repeat it for more. The agent then gets only these topics.",
|
|
539
|
-
"
|
|
540
|
-
"
|
|
693
|
+
" Each must be in what you approved at sign-in: a restricted topic only when you",
|
|
694
|
+
" included it. Default: what you chose at sign-in (one topic, the ticked topics, or all topics).",
|
|
695
|
+
"--role <role> editor, commenter or viewer, not above the role you approved. Default: that role.",
|
|
541
696
|
],
|
|
542
697
|
options: CONNECT_OPTIONS,
|
|
543
698
|
positionals: [0, 0],
|
|
@@ -574,56 +729,140 @@ export const commands = {
|
|
|
574
729
|
usage: "orlan files list [--topic <topic>] [--agent <id>] [--json]",
|
|
575
730
|
summary: "List the files of a topic with their current versions.",
|
|
576
731
|
details: [
|
|
577
|
-
"--topic <topic> The topic id or its name. Default: the topic
|
|
732
|
+
"--topic <topic> The topic id or its name. Default: the topic open in Orlan now, else the one used last.",
|
|
578
733
|
AGENT_TOKEN_HELP,
|
|
579
734
|
JSON_HELP,
|
|
580
735
|
],
|
|
581
736
|
options: { ...agentOption, ...jsonOption, topic: { type: "string" } },
|
|
582
737
|
positionals: [0, 0],
|
|
583
738
|
run: (values, _positionals, io) => withAgent(values, async (api) => {
|
|
584
|
-
const topicId = await topicOrDefault(api, values);
|
|
739
|
+
const topicId = await topicOrDefault(api, values, io);
|
|
585
740
|
const { files } = await api.call("read_board", { topicId, view: "files" });
|
|
586
741
|
return finish(io, values, { files }, files.length === 0
|
|
587
742
|
? ["No files."]
|
|
588
|
-
: files.map((file) => `${file.id} ${quote(file.name)} ${file.kind}
|
|
743
|
+
: files.map((file) => `${file.id} ${quote(file.name)} ${file.kind}` +
|
|
744
|
+
// A video says its length (F133).
|
|
745
|
+
(file.current.durationMs !== undefined ? ` ${Math.round(file.current.durationMs / 1000)} s` : "") +
|
|
746
|
+
// The scenes of its shot list (F135).
|
|
747
|
+
(file.current.scenes ? ` ${file.current.scenes} scene${file.current.scenes === 1 ? "" : "s"}` : "") +
|
|
748
|
+
// The slides of a carousel (F136).
|
|
749
|
+
(file.current.slides ? ` ${file.current.slides} slide${file.current.slides === 1 ? "" : "s"}` : "") +
|
|
750
|
+
` v${file.current.number} current (${file.current.status}), ` +
|
|
589
751
|
`${file.versions} version${file.versions === 1 ? "" : "s"}` +
|
|
590
752
|
(file.current.changed
|
|
591
753
|
? `, ${changedText(file.current.changed.pages)} from v${file.current.changed.from}`
|
|
592
|
-
: "")
|
|
754
|
+
: "") +
|
|
755
|
+
// The final version a person picked, with its formats (F137).
|
|
756
|
+
(file.final ? `, final v${file.final.version} (${file.final.formats.join(", ")})` : "") +
|
|
757
|
+
// The critic scorecard of the newest scored version (F139).
|
|
758
|
+
(file.scores ? `, critic v${file.scores.version} ${scoreTotal(file.scores.scores)}` : "")), files.length === 0 ? "orlan files add <path> (a new file)" : "orlan files pull <file> --edit --out <path>");
|
|
593
759
|
}),
|
|
594
760
|
},
|
|
595
761
|
"files add": {
|
|
596
|
-
usage: "orlan files add <path> [--at <x,y>] [--topic <topic>] [--agent <id>] [--json]",
|
|
762
|
+
usage: "orlan files add <path>... [--carousel [--name <name>]] [--at <x,y>] [--shots <shots.json>] [--scores <scores.json>] [--topic <topic> | --new-topic <name>] [--agent <id>] [--json]",
|
|
597
763
|
summary: "Post a local file as a new file on a topic, and put it on the board.",
|
|
598
764
|
details: [
|
|
599
765
|
"One request sends the file and places it on the board, right of the other items. The server",
|
|
600
|
-
"renders its pages. A PowerPoint, PDF, PNG, JPG or HTML file,
|
|
766
|
+
"renders its pages. A PowerPoint, Word, Excel, PDF, PNG, JPG or HTML file, or an MP4 or WebM video,",
|
|
767
|
+
"with the right extension. The file streams from the disk, so a large video works too.",
|
|
601
768
|
"",
|
|
769
|
+
"--carousel The paths are the PNG or JPG slides of one carousel: one file on the board.",
|
|
770
|
+
" The slides go in the order of the numbers in their names: slide-2 before slide-10.",
|
|
771
|
+
"--name <name> With --carousel: the name of the carousel. Default: the name of its first slide.",
|
|
602
772
|
"--at <x,y> Put it at this point of the board, in board coordinates, or at the nearest free",
|
|
603
773
|
" place. `orlan board` shows where the items are.",
|
|
604
|
-
|
|
774
|
+
...shotsHelp(17),
|
|
775
|
+
...scoresHelp(17),
|
|
776
|
+
"--topic <topic> The topic id or its name. Default: the topic open in Orlan now, else the one used last.",
|
|
777
|
+
...NEW_TOPIC_HELP,
|
|
605
778
|
AGENT_TOKEN_HELP,
|
|
606
779
|
JSON_HELP,
|
|
607
780
|
],
|
|
608
|
-
options: {
|
|
609
|
-
|
|
610
|
-
|
|
781
|
+
options: {
|
|
782
|
+
...agentOption,
|
|
783
|
+
...jsonOption,
|
|
784
|
+
topic: { type: "string" },
|
|
785
|
+
"new-topic": { type: "string" },
|
|
786
|
+
at: { type: "string" },
|
|
787
|
+
shots: { type: "string" },
|
|
788
|
+
scores: { type: "string" },
|
|
789
|
+
carousel: { type: "boolean" },
|
|
790
|
+
name: { type: "string" },
|
|
791
|
+
},
|
|
792
|
+
positionals: [1, CAROUSEL_SLIDES_MAX],
|
|
793
|
+
run: (values, paths, io) => withAgent(values, async (api) => {
|
|
794
|
+
const [filePath] = paths;
|
|
795
|
+
if (!values.carousel && (paths.length > 1 || values.name !== undefined)) {
|
|
796
|
+
throw new OrlanError(paths.length > 1
|
|
797
|
+
? "Give one file, or add --carousel for the slides of one carousel."
|
|
798
|
+
: "--name goes with --carousel.", undefined, `orlan files add ${paths.join(" ")} --carousel`);
|
|
799
|
+
}
|
|
611
800
|
const at = numbersOption(values, "at", 2);
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
801
|
+
const shots = await shotsOption(values);
|
|
802
|
+
const scores = await scoresOption(values);
|
|
803
|
+
if (values.carousel && shots.shots !== undefined) {
|
|
804
|
+
throw new OrlanError("--shots goes with a video, not with --carousel.", "shots_invalid", "leave out --shots");
|
|
805
|
+
}
|
|
806
|
+
const { topicId, newTopic } = await writeTopic(api, values, io);
|
|
807
|
+
const madeTopic = newTopic ? { newTopic } : {};
|
|
808
|
+
if (values.carousel) {
|
|
809
|
+
// Each slide goes up first, in order; the post makes them one carousel (F136).
|
|
810
|
+
const slides = slideOrder(paths);
|
|
811
|
+
const uploadIds = [];
|
|
812
|
+
for (const slide of slides) {
|
|
813
|
+
uploadIds.push(await api.upload(await openAsBlob(slide), { topic: topicId, name: path.basename(slide) }));
|
|
814
|
+
}
|
|
815
|
+
const name = typeof values.name === "string" && values.name.trim()
|
|
816
|
+
? values.name.trim()
|
|
817
|
+
: path.parse(slides[0]).name;
|
|
818
|
+
const posted = await api.call("post", {
|
|
819
|
+
topicId,
|
|
820
|
+
name,
|
|
821
|
+
slides: uploadIds,
|
|
822
|
+
...(at ? { position: { x: at[0], y: at[1] } } : {}),
|
|
823
|
+
...scores,
|
|
824
|
+
});
|
|
825
|
+
return finish(io, values, {
|
|
826
|
+
file: {
|
|
827
|
+
id: posted.file.id,
|
|
828
|
+
name: posted.file.name,
|
|
829
|
+
version: posted.posted.number,
|
|
830
|
+
slides: slides.length,
|
|
831
|
+
},
|
|
832
|
+
boardItemId: posted.boardItemId,
|
|
833
|
+
...madeTopic,
|
|
834
|
+
}, [
|
|
835
|
+
`Added the carousel ${quote(posted.file.name)} as file ${posted.file.id}, v${posted.posted.number}, ` +
|
|
836
|
+
`${slides.length} slide${slides.length === 1 ? "" : "s"} in this order: ` +
|
|
837
|
+
`${slides.map((slide) => path.basename(slide)).join(", ")}. It is on the board.`,
|
|
838
|
+
], doneStep("comments on the file"));
|
|
839
|
+
}
|
|
840
|
+
if (at || shots.shots !== undefined || scores.scores !== undefined) {
|
|
841
|
+
// A place on the board, a shot list or a scorecard: the file goes up first, then the post places it.
|
|
842
|
+
const uploadId = await api.upload(await openAsBlob(filePath), {
|
|
843
|
+
topic: topicId,
|
|
844
|
+
name: path.basename(filePath),
|
|
845
|
+
});
|
|
846
|
+
const posted = await api.call("post", {
|
|
847
|
+
topicId,
|
|
848
|
+
name: path.basename(filePath),
|
|
849
|
+
uploadId,
|
|
850
|
+
...(at ? { position: { x: at[0], y: at[1] } } : {}),
|
|
851
|
+
...shots,
|
|
852
|
+
...scores,
|
|
853
|
+
});
|
|
617
854
|
return finish(io, values, {
|
|
618
855
|
file: { id: posted.file.id, name: posted.file.name, version: posted.posted.number },
|
|
619
856
|
boardItemId: posted.boardItemId,
|
|
857
|
+
...madeTopic,
|
|
620
858
|
}, [
|
|
621
859
|
`Added ${quote(posted.file.name)} as file ${posted.file.id}, v${posted.posted.number}. It is on the ` +
|
|
622
|
-
`board at ${at.join(",")}, or at the nearest free place; its pages render now.`,
|
|
860
|
+
`board${at ? ` at ${at.join(",")}, or at the nearest free place` : ""}; its pages render now.`,
|
|
861
|
+
...(shots.shots !== undefined ? ["Its shot list shows as the scene list of the video."] : []),
|
|
623
862
|
], doneStep("comments on the file"));
|
|
624
863
|
}
|
|
625
|
-
const { nextStep, ...answer } = await api.postFile("/api/agent/files", { name: path.basename(filePath), topic:
|
|
626
|
-
return finish(io, values, answer, [
|
|
864
|
+
const { nextStep, ...answer } = await api.postFile("/api/agent/files", { name: path.basename(filePath), topic: topicId }, await openAsBlob(filePath), "the new file");
|
|
865
|
+
return finish(io, values, { ...answer, ...madeTopic }, [
|
|
627
866
|
`Added ${quote(answer.file.name)} to topic ${quote(answer.topic.name)} as file ` +
|
|
628
867
|
`${answer.file.id}, v${answer.file.version}. It is on the board; its pages render now.`,
|
|
629
868
|
], nextStep);
|
|
@@ -703,7 +942,10 @@ export const commands = {
|
|
|
703
942
|
if (!response.ok) {
|
|
704
943
|
throw new OrlanError(`The download from ${config.server} answered ${response.status}.`, undefined, "run the command again");
|
|
705
944
|
}
|
|
706
|
-
|
|
945
|
+
// A carousel's original is the zip of its slides (F136).
|
|
946
|
+
const target = typeof values.out === "string"
|
|
947
|
+
? values.out
|
|
948
|
+
: `${path.basename(answer.file.name)}${answer.file.kind === "carousel" ? ".zip" : ""}`;
|
|
707
949
|
await writeFile(target, Buffer.from(await response.arrayBuffer()));
|
|
708
950
|
const lines = [
|
|
709
951
|
`Saved version ${answer.version.number} of ${quote(answer.file.name)} to ${quote(target)}.`,
|
|
@@ -720,16 +962,22 @@ export const commands = {
|
|
|
720
962
|
}),
|
|
721
963
|
},
|
|
722
964
|
"files push": {
|
|
723
|
-
usage: 'orlan files push <file> <path
|
|
965
|
+
usage: 'orlan files push <file> <path>... --changelog "<what changed>" [--answers <n,n>] [--base <n>] [--shots <shots.json>] [--scores <scores.json>] [--topic <topic>] [--agent <id>] [--json]',
|
|
724
966
|
summary: "Post a local file as a new version of a file.",
|
|
725
967
|
details: [
|
|
726
968
|
"Two requests: one sends the file, one posts it with the changelog. It must be the same type of",
|
|
727
969
|
"file. The version waits for a person to make it current. It ends your claim. When a version",
|
|
728
970
|
"landed after your base, Orlan refuses the push with stale_base and its changelog.",
|
|
971
|
+
"For a carousel, give each PNG or JPG slide. They go in the order of the numbers in their names.",
|
|
729
972
|
"",
|
|
730
973
|
"<file> The file id, or its name where it is unique on the topic.",
|
|
731
974
|
"--changelog <text> What changed, for the people who review it. Required.",
|
|
975
|
+
"--answers <n,n> The comments this version answers, by number: 3,5 or #3,#5. Each shows",
|
|
976
|
+
' "answered in v<n>". A "#3" in the changelog counts too. A number that is no',
|
|
977
|
+
" comment on the file is refused with answers_invalid.",
|
|
732
978
|
"--base <n> The version you edited, for example 4 or v4. Default: the version your `pull --edit` got.",
|
|
979
|
+
...shotsHelp(20),
|
|
980
|
+
...scoresHelp(20),
|
|
733
981
|
"--topic <topic> The topic of a file name, when two topics have a file with that name.",
|
|
734
982
|
AGENT_TOKEN_HELP,
|
|
735
983
|
JSON_HELP,
|
|
@@ -740,24 +988,37 @@ export const commands = {
|
|
|
740
988
|
changelog: { type: "string" },
|
|
741
989
|
base: { type: "string" },
|
|
742
990
|
topic: { type: "string" },
|
|
991
|
+
shots: { type: "string" },
|
|
992
|
+
scores: { type: "string" },
|
|
993
|
+
answers: { type: "string" },
|
|
743
994
|
},
|
|
744
|
-
positionals: [2,
|
|
745
|
-
run: (values, [fileRef,
|
|
995
|
+
positionals: [2, CAROUSEL_SLIDES_MAX + 1],
|
|
996
|
+
run: (values, [fileRef, ...filePaths], io) => withAgent(values, async (api) => {
|
|
746
997
|
const changelog = typeof values.changelog === "string" ? values.changelog.trim() : "";
|
|
747
998
|
if (!changelog) {
|
|
748
|
-
throw new OrlanError("Add --changelog.", undefined, `orlan files push ${fileRef} ${
|
|
999
|
+
throw new OrlanError("Add --changelog.", undefined, `orlan files push ${fileRef} ${filePaths.join(" ")} --changelog "<what changed>"`);
|
|
749
1000
|
}
|
|
750
1001
|
const base = baseOption(values);
|
|
751
|
-
const
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
1002
|
+
const shots = await shotsOption(values);
|
|
1003
|
+
const scores = await scoresOption(values);
|
|
1004
|
+
const answers = answersOption(values);
|
|
1005
|
+
// Several paths are the slides of a new carousel version (F136), in the order of their names.
|
|
1006
|
+
const uploadIds = [];
|
|
1007
|
+
for (const filePath of filePaths.length > 1 ? slideOrder(filePaths) : filePaths) {
|
|
1008
|
+
uploadIds.push(await api.upload(await openAsBlob(filePath), {
|
|
1009
|
+
file: fileRef,
|
|
1010
|
+
topic: values.topic,
|
|
1011
|
+
}));
|
|
1012
|
+
}
|
|
755
1013
|
const posted = await api.call("post", {
|
|
756
1014
|
artifactId: fileRef,
|
|
757
1015
|
...topicInput(values),
|
|
758
|
-
uploadId,
|
|
1016
|
+
...(uploadIds.length > 1 ? { slides: uploadIds } : { uploadId: uploadIds[0] }),
|
|
759
1017
|
changelog,
|
|
760
1018
|
...base,
|
|
1019
|
+
...shots,
|
|
1020
|
+
...scores,
|
|
1021
|
+
...answers,
|
|
761
1022
|
});
|
|
762
1023
|
return finish(io, values, { file: { id: posted.file.id, name: posted.file.name }, version: posted.posted.number }, [
|
|
763
1024
|
`Posted version ${posted.posted.number} of ${quote(posted.file.name)}. It waits for a person to make it current.`,
|
|
@@ -859,7 +1120,10 @@ export const commands = {
|
|
|
859
1120
|
lines.push(`file ${quote(file.name)}:`);
|
|
860
1121
|
for (const thread of file.threads) {
|
|
861
1122
|
const place = `${thread.at}${thread.region ? `, region ${thread.region.join(",")}` : ""}` +
|
|
862
|
-
(thread.element ? `, element ${quote(thread.element)}` : "")
|
|
1123
|
+
(thread.element ? `, element ${quote(thread.element)}` : "") +
|
|
1124
|
+
(thread.box ? `, box ${thread.box.join(",")}` : "") +
|
|
1125
|
+
(thread.scene ? `, scene ${thread.scene.index} ${quote(thread.scene.name)}` : "") +
|
|
1126
|
+
(thread.answeredIn ? `, answered in ${thread.answeredIn}` : "");
|
|
863
1127
|
lines.push(...threadLines(thread, place));
|
|
864
1128
|
}
|
|
865
1129
|
}
|
|
@@ -877,19 +1141,23 @@ export const commands = {
|
|
|
877
1141
|
}),
|
|
878
1142
|
},
|
|
879
1143
|
comment: {
|
|
880
|
-
usage: 'orlan comment <thread> "<text>" | --file <file> --page <n> [--region <x,y,w,h>] "<text>" | --at <x,y> "<text>" [--topic <topic>] [--agent <id>] [--json]',
|
|
881
|
-
summary: "Reply in a thread, or open a new thread on a page or on the board.",
|
|
1144
|
+
usage: 'orlan comment <thread> "<text>" | --file <file> --page <n> [--region <x,y,w,h>] "<text>" | --file <video> --time <t> [--box <x,y,w,h>] "<text>" | --file <video> --scene <n> "<text>" | --at <x,y> "<text>" [--topic <topic>] [--agent <id>] [--json]',
|
|
1145
|
+
summary: "Reply in a thread, or open a new thread on a page, on a video or on the board.",
|
|
882
1146
|
details: [
|
|
883
|
-
"With a thread (#12 or its id), it replies in that thread. With --file and --page, or
|
|
884
|
-
"opens a new thread there: to ask a person, or to point at a problem. People see it
|
|
1147
|
+
"With a thread (#12 or its id), it replies in that thread. With --file and --page, --time or --scene, or",
|
|
1148
|
+
"with --at, it opens a new thread there: to ask a person, or to point at a problem. People see it at once.",
|
|
885
1149
|
"Write @ and a name to mention a person or an agent of the topic: they are told like any mention.",
|
|
886
1150
|
"A question waits for the answer: leave the thread open, the answer comes to your inbox.",
|
|
887
1151
|
"",
|
|
888
1152
|
"--file <file> The file of a new thread on a page: its id, or its name.",
|
|
889
1153
|
"--page <n> The page, from 1.",
|
|
890
1154
|
"--region <x,y,w,h> The rectangle on the page, as fractions (0 to 1). Default: the whole page.",
|
|
1155
|
+
"--time <t> A time of the video: seconds or m:ss, for example 42 or 0:42. A range has a",
|
|
1156
|
+
" dash, for example 0:40-0:48.",
|
|
1157
|
+
"--box <x,y,w,h> With one --time: a rectangle on the frame at that time, as fractions (0 to 1).",
|
|
1158
|
+
"--scene <n> A scene of the video's shot list, from 1: the thread takes the scene's range.",
|
|
891
1159
|
"--at <x,y> A new thread on this point of the board, in board coordinates.",
|
|
892
|
-
"--topic <topic> The topic of --at, of a #12 or of a file name. Default: the topic
|
|
1160
|
+
"--topic <topic> The topic of --at, of a #12 or of a file name. Default: the topic open in Orlan now, else the one used last.",
|
|
893
1161
|
AGENT_TOKEN_HELP,
|
|
894
1162
|
JSON_HELP,
|
|
895
1163
|
],
|
|
@@ -899,6 +1167,9 @@ export const commands = {
|
|
|
899
1167
|
file: { type: "string" },
|
|
900
1168
|
page: { type: "string" },
|
|
901
1169
|
region: { type: "string" },
|
|
1170
|
+
time: { type: "string" },
|
|
1171
|
+
box: { type: "string" },
|
|
1172
|
+
scene: { type: "string" },
|
|
902
1173
|
at: { type: "string" },
|
|
903
1174
|
topic: { type: "string" },
|
|
904
1175
|
},
|
|
@@ -907,7 +1178,7 @@ export const commands = {
|
|
|
907
1178
|
const numbers = (option, count) => numbersOption(values, option, count);
|
|
908
1179
|
const [first, second] = positionals;
|
|
909
1180
|
const body = (second ?? first);
|
|
910
|
-
const usage = "orlan comment --help (the
|
|
1181
|
+
const usage = "orlan comment --help (the forms)";
|
|
911
1182
|
let target;
|
|
912
1183
|
if (second !== undefined) {
|
|
913
1184
|
if (values.file !== undefined || values.at !== undefined) {
|
|
@@ -915,6 +1186,27 @@ export const commands = {
|
|
|
915
1186
|
}
|
|
916
1187
|
target = { threadId: first };
|
|
917
1188
|
}
|
|
1189
|
+
else if (typeof values.file === "string" && (values.time !== undefined || values.scene !== undefined)) {
|
|
1190
|
+
// A time, a range, a box on a frame or a scene of a video (F134, F135).
|
|
1191
|
+
if ((values.time === undefined) === (values.scene === undefined) || values.page !== undefined) {
|
|
1192
|
+
throw new OrlanError("A comment on a video takes --time or --scene: one of them.", undefined, usage);
|
|
1193
|
+
}
|
|
1194
|
+
const box = numbers("box", 4);
|
|
1195
|
+
const scene = Number(values.scene);
|
|
1196
|
+
if (values.scene !== undefined && (!Number.isInteger(scene) || scene < 1)) {
|
|
1197
|
+
throw new OrlanError("--scene takes the number of a scene, from 1.", undefined, "add --scene 1");
|
|
1198
|
+
}
|
|
1199
|
+
if (box && (typeof values.time !== "string" || values.time.includes("-"))) {
|
|
1200
|
+
throw new OrlanError("--box goes with one --time, not a range.", undefined, "add --time 0:42");
|
|
1201
|
+
}
|
|
1202
|
+
target = {
|
|
1203
|
+
video: {
|
|
1204
|
+
artifactId: values.file,
|
|
1205
|
+
...(typeof values.time === "string" ? timeOption(values.time) : { scene }),
|
|
1206
|
+
...(box ? { box: { x: box[0], y: box[1], w: box[2], h: box[3] } } : {}),
|
|
1207
|
+
},
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
918
1210
|
else if (typeof values.file === "string") {
|
|
919
1211
|
const page = Number(values.page);
|
|
920
1212
|
if (!Number.isInteger(page) || page < 1) {
|
|
@@ -985,14 +1277,14 @@ export const commands = {
|
|
|
985
1277
|
'"in frame <id>". Each box is the box after it grew to fit its words. People vote; an agent reads',
|
|
986
1278
|
"the vote and its result, it does not vote.",
|
|
987
1279
|
"",
|
|
988
|
-
"--topic <topic> The topic id or its name. Default: the topic
|
|
1280
|
+
"--topic <topic> The topic id or its name. Default: the topic open in Orlan now, else the one used last.",
|
|
989
1281
|
AGENT_TOKEN_HELP,
|
|
990
1282
|
JSON_HELP,
|
|
991
1283
|
],
|
|
992
1284
|
options: { ...agentOption, ...jsonOption, topic: { type: "string" } },
|
|
993
1285
|
positionals: [0, 0],
|
|
994
1286
|
run: (values, _positionals, io) => withAgent(values, async (api) => {
|
|
995
|
-
const topicId = await topicOrDefault(api, values);
|
|
1287
|
+
const topicId = await topicOrDefault(api, values, io);
|
|
996
1288
|
// Two requests at the same time: the board, and the names of its files.
|
|
997
1289
|
const [board, { files }] = await Promise.all([
|
|
998
1290
|
api.call("read_board", { topicId, view: "board" }),
|
|
@@ -1054,7 +1346,7 @@ export const commands = {
|
|
|
1054
1346
|
"",
|
|
1055
1347
|
"--out <file.png> Where to save the picture. A file that is there is replaced.",
|
|
1056
1348
|
"--frame <frame> Only this frame: its id, from `orlan board`.",
|
|
1057
|
-
"--topic <topic> The topic id or its name. Default: the topic
|
|
1349
|
+
"--topic <topic> The topic id or its name. Default: the topic open in Orlan now, else the one used last.",
|
|
1058
1350
|
AGENT_TOKEN_HELP,
|
|
1059
1351
|
JSON_HELP,
|
|
1060
1352
|
],
|
|
@@ -1070,7 +1362,7 @@ export const commands = {
|
|
|
1070
1362
|
if (typeof values.out !== "string" || !values.out) {
|
|
1071
1363
|
throw new OrlanError("Add --out <file.png>: where to save the picture.", undefined, "orlan board view --out board.png");
|
|
1072
1364
|
}
|
|
1073
|
-
const topicId = await topicOrDefault(api, values);
|
|
1365
|
+
const topicId = await topicOrDefault(api, values, io);
|
|
1074
1366
|
const answer = await api.call("view_board", { topicId, ...(typeof values.frame === "string" ? { frameId: values.frame } : {}) });
|
|
1075
1367
|
const saved = path.resolve(values.out);
|
|
1076
1368
|
const png = Buffer.from(answer.png, "base64");
|
|
@@ -1099,7 +1391,7 @@ export const commands = {
|
|
|
1099
1391
|
}),
|
|
1100
1392
|
},
|
|
1101
1393
|
"board add": {
|
|
1102
|
-
usage: 'orlan board add note|text|frame|shape ["<text>"] --at <x,y> [--size <S|M|L|XL|w,h>] [--text-color <color>] [--kind <kind>] [--color <color>] [--frame <frame>|none] | arrow --from <item>[@x,y] --to <item>[@x,y] [--text "<label>"] [--id <id>] [--topic <topic>] [--agent <id>] [--json]',
|
|
1394
|
+
usage: 'orlan board add note|text|frame|shape ["<text>"] --at <x,y> [--size <S|M|L|XL|w,h>] [--text-color <color>] [--kind <kind>] [--color <color>] [--frame <frame>|none] | arrow --from <item>[@x,y] --to <item>[@x,y] [--text "<label>"] [--id <id>] [--topic <topic> | --new-topic <name>] [--agent <id>] [--json]',
|
|
1103
1395
|
summary: "Add a note, a text, a shape, a frame or an arrow to the board.",
|
|
1104
1396
|
details: [
|
|
1105
1397
|
"A note, a text, a shape or a frame goes at --at, with its text (a frame's text is its title, a",
|
|
@@ -1130,7 +1422,8 @@ export const commands = {
|
|
|
1130
1422
|
"--frame <frame> The frame the item goes in, by its id. none: in no frame. Default: the frame that",
|
|
1131
1423
|
" holds more than half of it.",
|
|
1132
1424
|
'--text <text> The text, as the "<text>" word gives it. For an arrow: its label, for example "yes".',
|
|
1133
|
-
"--topic <topic> The topic id or its name. Default: the topic
|
|
1425
|
+
"--topic <topic> The topic id or its name. Default: the topic open in Orlan now, else the one used last.",
|
|
1426
|
+
...NEW_TOPIC_HELP,
|
|
1134
1427
|
AGENT_TOKEN_HELP,
|
|
1135
1428
|
JSON_HELP,
|
|
1136
1429
|
],
|
|
@@ -1148,6 +1441,7 @@ export const commands = {
|
|
|
1148
1441
|
id: { type: "string" },
|
|
1149
1442
|
frame: { type: "string" },
|
|
1150
1443
|
topic: { type: "string" },
|
|
1444
|
+
"new-topic": { type: "string" },
|
|
1151
1445
|
},
|
|
1152
1446
|
positionals: [1, 2],
|
|
1153
1447
|
run: (values, [type, word], io) => {
|
|
@@ -1198,10 +1492,10 @@ export const commands = {
|
|
|
1198
1492
|
// The server refuses a frame on an arrow, and says why.
|
|
1199
1493
|
Object.assign(edit, frameOption(values));
|
|
1200
1494
|
return withAgent(values, async (api) => {
|
|
1201
|
-
const { items, warnings } = await editBoard(api, values, [edit]);
|
|
1495
|
+
const { items, warnings, newTopic } = await editBoard(api, values, io, [edit]);
|
|
1202
1496
|
const item = items[0];
|
|
1203
1497
|
const where = type === "arrow" ? `, from ${values.from} to ${values.to}` : "";
|
|
1204
|
-
return finish(io, values, { ...item, warnings }, [`Added ${boxText(item)}${where}.`, ...warningLines(warnings)], afterWarnings(warnings, type === "arrow"
|
|
1498
|
+
return finish(io, values, { ...item, warnings, ...(newTopic ? { newTopic } : {}) }, [`Added ${boxText(item)}${where}.`, ...warningLines(warnings)], afterWarnings(warnings, type === "arrow"
|
|
1205
1499
|
? "orlan board (the board with the arrow)"
|
|
1206
1500
|
: `orlan board add arrow --from ${item.id} --to <item> (to link it), or orlan board`));
|
|
1207
1501
|
});
|
|
@@ -1233,7 +1527,7 @@ export const commands = {
|
|
|
1233
1527
|
"--to <item> The item the arrow points to, as --from.",
|
|
1234
1528
|
"--frame <frame> Put the item in this frame, by its id. none: take it out of its frame. The item",
|
|
1235
1529
|
" stays where it is. With --at and no --frame, the item goes in the frame it lands in.",
|
|
1236
|
-
"--topic <topic> The topic id or its name. Default: the topic
|
|
1530
|
+
"--topic <topic> The topic id or its name. Default: the topic open in Orlan now, else the one used last.",
|
|
1237
1531
|
AGENT_TOKEN_HELP,
|
|
1238
1532
|
JSON_HELP,
|
|
1239
1533
|
],
|
|
@@ -1272,7 +1566,7 @@ export const commands = {
|
|
|
1272
1566
|
throw new OrlanError("Give --text, --at, --size, --text-color, --kind, --color, --from, --to or --frame.", undefined, `orlan board change ${id} --text "<text>"`);
|
|
1273
1567
|
}
|
|
1274
1568
|
return withAgent(values, async (api) => {
|
|
1275
|
-
const { items, warnings } = await editBoard(api, values, edits);
|
|
1569
|
+
const { items, warnings } = await editBoard(api, values, io, edits);
|
|
1276
1570
|
const item = items.find((each) => each.id === id);
|
|
1277
1571
|
return finish(io, values, { ...(item ?? { id }), warnings }, [item ? `Changed ${boxText(item)}.` : `Changed ${id}.`, ...warningLines(warnings)], afterWarnings(warnings, "orlan board (the board with the change)"));
|
|
1278
1572
|
});
|
|
@@ -1287,14 +1581,14 @@ export const commands = {
|
|
|
1287
1581
|
"of a deleted frame stay on the board, in the frame that frame was in.",
|
|
1288
1582
|
"",
|
|
1289
1583
|
"<item> The item id, from `orlan board`. Give several to delete several.",
|
|
1290
|
-
"--topic <topic> The topic id or its name. Default: the topic
|
|
1584
|
+
"--topic <topic> The topic id or its name. Default: the topic open in Orlan now, else the one used last.",
|
|
1291
1585
|
AGENT_TOKEN_HELP,
|
|
1292
1586
|
JSON_HELP,
|
|
1293
1587
|
],
|
|
1294
1588
|
options: { ...agentOption, ...jsonOption, topic: { type: "string" } },
|
|
1295
1589
|
positionals: [1, 100],
|
|
1296
1590
|
run: (values, ids, io) => withAgent(values, async (api) => {
|
|
1297
|
-
const { warnings } = await editBoard(api, values, ids.map((id) => ({ op: "delete", id })));
|
|
1591
|
+
const { warnings } = await editBoard(api, values, io, ids.map((id) => ({ op: "delete", id })));
|
|
1298
1592
|
return finish(io, values, { deleted: ids, warnings }, [
|
|
1299
1593
|
`Deleted ${ids.length === 1 ? "1 item" : `${ids.length} items`}: ${ids.join(", ")}. A person can undo it.`,
|
|
1300
1594
|
...warningLines(warnings),
|
|
@@ -1302,7 +1596,7 @@ export const commands = {
|
|
|
1302
1596
|
}),
|
|
1303
1597
|
},
|
|
1304
1598
|
"board edit": {
|
|
1305
|
-
usage: "orlan board edit '<edits as JSON>' [--topic <topic>] [--agent <id>] [--json]",
|
|
1599
|
+
usage: "orlan board edit '<edits as JSON>' [--topic <topic> | --new-topic <name>] [--agent <id>] [--json]",
|
|
1306
1600
|
summary: "Make several changes to the board in one step: all of them, or none.",
|
|
1307
1601
|
details: [
|
|
1308
1602
|
"The edits are a JSON list of at most 100. A person can undo the step. A create can give its item",
|
|
@@ -1335,11 +1629,12 @@ export const commands = {
|
|
|
1335
1629
|
"the error says what to change. The answer gives each item's id and its final place and size.",
|
|
1336
1630
|
...WARNINGS_HELP,
|
|
1337
1631
|
"",
|
|
1338
|
-
"--topic <topic> The topic id or its name. Default: the topic
|
|
1632
|
+
"--topic <topic> The topic id or its name. Default: the topic open in Orlan now, else the one used last.",
|
|
1633
|
+
...NEW_TOPIC_HELP,
|
|
1339
1634
|
AGENT_TOKEN_HELP,
|
|
1340
1635
|
JSON_HELP,
|
|
1341
1636
|
],
|
|
1342
|
-
options: { ...agentOption, ...jsonOption, topic: { type: "string" } },
|
|
1637
|
+
options: { ...agentOption, ...jsonOption, topic: { type: "string" }, "new-topic": { type: "string" } },
|
|
1343
1638
|
positionals: [1, 1],
|
|
1344
1639
|
run: (values, [text], io) => {
|
|
1345
1640
|
let edits;
|
|
@@ -1353,8 +1648,8 @@ export const commands = {
|
|
|
1353
1648
|
throw new OrlanError('The edits are a JSON list, for example [{"op":"delete","id":"<item>"}].', undefined, "orlan board edit --help");
|
|
1354
1649
|
}
|
|
1355
1650
|
return withAgent(values, async (api) => {
|
|
1356
|
-
const { ids, items, warnings } = await editBoard(api, values, edits);
|
|
1357
|
-
return finish(io, values, { ids, items, warnings }, [
|
|
1651
|
+
const { ids, items, warnings, newTopic } = await editBoard(api, values, io, edits);
|
|
1652
|
+
return finish(io, values, { ids, items, warnings, ...(newTopic ? { newTopic } : {}) }, [
|
|
1358
1653
|
`Made ${ids.length} change${ids.length === 1 ? "" : "s"} in one step.`,
|
|
1359
1654
|
...items.map((item) => `- ${boxText(item)}`),
|
|
1360
1655
|
...warningLines(warnings),
|
|
@@ -1363,7 +1658,7 @@ export const commands = {
|
|
|
1363
1658
|
},
|
|
1364
1659
|
},
|
|
1365
1660
|
"board mermaid": {
|
|
1366
|
-
usage: "orlan board mermaid <file.mmd> [--frame <frame>] [--topic <topic>] [--agent <id>] [--json]",
|
|
1661
|
+
usage: "orlan board mermaid <file.mmd> [--frame <frame>] [--topic <topic> | --new-topic <name>] [--agent <id>] [--json]",
|
|
1367
1662
|
summary: "Draw a Mermaid flowchart on the board as real shapes, labeled arrows and frames.",
|
|
1368
1663
|
details: [
|
|
1369
1664
|
"Orlan reads the flowchart and draws it in one step, which a person can undo: a shape for each",
|
|
@@ -1401,17 +1696,24 @@ export const commands = {
|
|
|
1401
1696
|
"",
|
|
1402
1697
|
"<file.mmd> The Mermaid file.",
|
|
1403
1698
|
"--frame <frame> Draw the diagram in this frame, by its id, from `orlan board`.",
|
|
1404
|
-
"--topic <topic> The topic id or its name. Default: the topic
|
|
1699
|
+
"--topic <topic> The topic id or its name. Default: the topic open in Orlan now, else the one used last.",
|
|
1700
|
+
...NEW_TOPIC_HELP,
|
|
1405
1701
|
AGENT_TOKEN_HELP,
|
|
1406
1702
|
JSON_HELP,
|
|
1407
1703
|
],
|
|
1408
|
-
options: {
|
|
1704
|
+
options: {
|
|
1705
|
+
...agentOption,
|
|
1706
|
+
...jsonOption,
|
|
1707
|
+
topic: { type: "string" },
|
|
1708
|
+
"new-topic": { type: "string" },
|
|
1709
|
+
frame: { type: "string" },
|
|
1710
|
+
},
|
|
1409
1711
|
positionals: [1, 1],
|
|
1410
1712
|
run: (values, [file], io) => withAgent(values, async (api) => {
|
|
1411
1713
|
const source = await readFile(file, "utf8").catch((error) => {
|
|
1412
1714
|
throw new OrlanError(`Orlan cannot read ${quote(file)}: ${error.code ?? error.message}.`, undefined, "give the path of a Mermaid file, for example orlan board mermaid flow.mmd");
|
|
1413
1715
|
});
|
|
1414
|
-
const topicId = await
|
|
1716
|
+
const { topicId, newTopic } = await writeTopic(api, values, io);
|
|
1415
1717
|
const answer = await api.call("import_mermaid", {
|
|
1416
1718
|
topicId,
|
|
1417
1719
|
mermaid: source,
|
|
@@ -1427,7 +1729,7 @@ export const commands = {
|
|
|
1427
1729
|
const made = [...count("shape", "shape"), ...count("arrow", "arrow"), ...count("frame", "frame")].join(", ");
|
|
1428
1730
|
const prefixed = Object.entries(ids).filter(([from, to]) => from !== to);
|
|
1429
1731
|
const where = typeof values.frame === "string" ? ` in frame ${values.frame}` : "";
|
|
1430
|
-
return finish(io, values, { ids, items, warnings, replaced, ignored }, [
|
|
1732
|
+
return finish(io, values, { ids, items, warnings, replaced, ignored, ...(newTopic ? { newTopic } : {}) }, [
|
|
1431
1733
|
`Drew ${quote(path.basename(file))}${where} in one step: ${made || "no items"}. A person can undo it.`,
|
|
1432
1734
|
...(replaced.length > 0
|
|
1433
1735
|
? [
|
|
@@ -1454,7 +1756,7 @@ export const commands = {
|
|
|
1454
1756
|
"--assignee <who> me (this agent), user:<id> or agent:<id>. Default: nobody.",
|
|
1455
1757
|
"--thread <t> The comment thread the task comes from: #12 or the thread id.",
|
|
1456
1758
|
"--done The task is done already.",
|
|
1457
|
-
"--topic <topic> The topic id or its name. Default: the topic
|
|
1759
|
+
"--topic <topic> The topic id or its name. Default: the topic open in Orlan now, else the one used last.",
|
|
1458
1760
|
AGENT_TOKEN_HELP,
|
|
1459
1761
|
JSON_HELP,
|
|
1460
1762
|
],
|
|
@@ -1471,7 +1773,7 @@ export const commands = {
|
|
|
1471
1773
|
const assignee = assigneeOption(config, values);
|
|
1472
1774
|
const task = await api.call("record", {
|
|
1473
1775
|
kind: "task",
|
|
1474
|
-
topicId: await topicOrDefault(api, values),
|
|
1776
|
+
topicId: await topicOrDefault(api, values, io),
|
|
1475
1777
|
text,
|
|
1476
1778
|
...(assignee ? { assignee } : {}),
|
|
1477
1779
|
...(typeof values.thread === "string" ? { sourceThreadId: values.thread } : {}),
|
|
@@ -1525,7 +1827,7 @@ export const commands = {
|
|
|
1525
1827
|
"",
|
|
1526
1828
|
"--body <text> The decision in full: what, and why.",
|
|
1527
1829
|
"--thread <t> The comment thread the decision comes from: #12 or the thread id.",
|
|
1528
|
-
"--topic <topic> The topic id or its name. Default: the topic
|
|
1830
|
+
"--topic <topic> The topic id or its name. Default: the topic open in Orlan now, else the one used last.",
|
|
1529
1831
|
AGENT_TOKEN_HELP,
|
|
1530
1832
|
JSON_HELP,
|
|
1531
1833
|
],
|
|
@@ -1540,7 +1842,7 @@ export const commands = {
|
|
|
1540
1842
|
run: (values, [title], io) => withAgent(values, async (api) => {
|
|
1541
1843
|
const decision = await api.call("record", {
|
|
1542
1844
|
kind: "decision",
|
|
1543
|
-
topicId: await topicOrDefault(api, values),
|
|
1845
|
+
topicId: await topicOrDefault(api, values, io),
|
|
1544
1846
|
title,
|
|
1545
1847
|
...(typeof values.body === "string" ? { body: values.body } : {}),
|
|
1546
1848
|
...(typeof values.thread === "string" ? { sourceThreadId: values.thread } : {}),
|
|
@@ -1598,12 +1900,249 @@ export const commands = {
|
|
|
1598
1900
|
: "orlan brief (where the work stands)");
|
|
1599
1901
|
}),
|
|
1600
1902
|
},
|
|
1903
|
+
brand: {
|
|
1904
|
+
usage: "orlan brand [--save <folder>] [--agent <id>] [--json]",
|
|
1905
|
+
summary: "Print the accepted brand kit of the organisation: colors, fonts, logos, tone, rules, formats.",
|
|
1906
|
+
details: [
|
|
1907
|
+
"Use it before you make a deck, a video, a carousel or a page, so the work is on brand with no brand",
|
|
1908
|
+
"words in the prompt. Orlan gives only the sections people accepted. A section that waits for review",
|
|
1909
|
+
"is named under draft, without its content: do not guess it. The references are work people like,",
|
|
1910
|
+
"each with a note.",
|
|
1911
|
+
"",
|
|
1912
|
+
"--save <folder> Also save the logo and font files of the kit in this folder.",
|
|
1913
|
+
AGENT_TOKEN_HELP,
|
|
1914
|
+
JSON_HELP,
|
|
1915
|
+
],
|
|
1916
|
+
options: { ...agentOption, ...jsonOption, save: { type: "string" } },
|
|
1917
|
+
positionals: [0, 0],
|
|
1918
|
+
run: (values, _positionals, io) => withAgent(values, async (api, config) => {
|
|
1919
|
+
const kit = await api.call("brand", {});
|
|
1920
|
+
const saved = [];
|
|
1921
|
+
if (typeof values.save === "string" && kit.files.length > 0) {
|
|
1922
|
+
await mkdir(values.save, { recursive: true });
|
|
1923
|
+
for (const file of kit.files) {
|
|
1924
|
+
const response = await fetch(file.url, {
|
|
1925
|
+
headers: { authorization: `Bearer ${api.token}`, ...sessionHeader() },
|
|
1926
|
+
}).catch((error) => {
|
|
1927
|
+
throw unreachable(config.server, error);
|
|
1928
|
+
});
|
|
1929
|
+
if (!response.ok) {
|
|
1930
|
+
throw new OrlanError(`The download of ${quote(file.name)} answered ${response.status}.`, undefined, "run the command again");
|
|
1931
|
+
}
|
|
1932
|
+
// Two files with one name (a light and a dark logo.png) are both kept.
|
|
1933
|
+
const name = saved.includes(path.join(values.save, file.name)) ? `${file.id}-${file.name}` : file.name;
|
|
1934
|
+
const target = path.join(values.save, path.basename(name));
|
|
1935
|
+
await writeFile(target, Buffer.from(await response.arrayBuffer()));
|
|
1936
|
+
saved.push(target);
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
const { sections } = kit;
|
|
1940
|
+
const lines = [`Brand kit v${kit.version}. Only the accepted sections are here.`];
|
|
1941
|
+
if (sections.colors) {
|
|
1942
|
+
lines.push(`colors: ${sections.colors.items.map((color) => `${color.name} ${color.hex}${color.use ? ` (${color.use})` : ""}`).join(", ") || "none"}`);
|
|
1943
|
+
}
|
|
1944
|
+
if (sections.fonts) {
|
|
1945
|
+
const font = (role) => sections.fonts?.[role].family || "not set";
|
|
1946
|
+
lines.push(`fonts: heading ${font("heading")}, body ${font("body")}`);
|
|
1947
|
+
}
|
|
1948
|
+
for (const file of kit.files)
|
|
1949
|
+
lines.push(`- ${file.role}: ${quote(file.name)} (${file.mime})`);
|
|
1950
|
+
if (sections.tone)
|
|
1951
|
+
lines.push(`tone: ${sections.tone.text || "not set"}`);
|
|
1952
|
+
if (sections.rules) {
|
|
1953
|
+
for (const rule of sections.rules.items)
|
|
1954
|
+
lines.push(`${rule.kind === "do" ? "do" : "do not"}: ${rule.text}`);
|
|
1955
|
+
}
|
|
1956
|
+
if (sections.formats) {
|
|
1957
|
+
const { video, deck, carousel } = sections.formats;
|
|
1958
|
+
lines.push(`formats: video ${video.join(", ") || "-"}; deck ${deck.join(", ") || "-"}; carousel ${carousel.join(", ") || "-"}`);
|
|
1959
|
+
}
|
|
1960
|
+
if (kit.draft.length)
|
|
1961
|
+
lines.push(`draft (waits for review, not for use): ${kit.draft.join(", ")}`);
|
|
1962
|
+
for (const ref of kit.references) {
|
|
1963
|
+
lines.push(`reference ${ref.kind}: ${ref.url ?? quote(ref.file?.name ?? "")} - ${quote(ref.note)}`);
|
|
1964
|
+
}
|
|
1965
|
+
for (const target of saved)
|
|
1966
|
+
lines.push(`Saved ${quote(target)}.`);
|
|
1967
|
+
// What people asked this agent to change (F141): each request ends with its own next_step line.
|
|
1968
|
+
const requests = kit.requests ?? [];
|
|
1969
|
+
for (const request of requests)
|
|
1970
|
+
lines.push("", ...brandRequestLines(request));
|
|
1971
|
+
return finish(io, values, { ...kit, ...(typeof values.save === "string" ? { saved } : {}) }, lines, requests.length > 0
|
|
1972
|
+
? "answer each request above with orlan brand draft"
|
|
1973
|
+
: kit.files.length > 0 && typeof values.save !== "string"
|
|
1974
|
+
? "orlan brand --save brand (the logo and font files)"
|
|
1975
|
+
: "make the work with this kit");
|
|
1976
|
+
}),
|
|
1977
|
+
},
|
|
1978
|
+
"brand draft": {
|
|
1979
|
+
usage: 'orlan brand draft <section> (--file <path> | --data <json>) [--note "<text>"] [--request <id>] [--agent <id>] [--json]',
|
|
1980
|
+
summary: "Write one section of the brand kit as a draft for people to review and accept.",
|
|
1981
|
+
details: [
|
|
1982
|
+
"Draft the kit from the website, the logo and font files and the files people give you: one call for",
|
|
1983
|
+
"each section. Each section waits as a draft until a person accepts it on the Brand kit page; an agent",
|
|
1984
|
+
"never accepts one. A draft answers your open requests on that section (orlan brand lists them).",
|
|
1985
|
+
"",
|
|
1986
|
+
"<section> colors, fonts, logos, tone, rules or formats. The content of each, as JSON:",
|
|
1987
|
+
' colors {"items": [{"name": "Vivid Blue", "hex": "#0679FF", "use": "Buttons"}]}',
|
|
1988
|
+
' fonts {"heading": {"family": "Avenir Next", "fileIds": []}, "body": {"family": "Inter", "fileIds": []}}',
|
|
1989
|
+
' logos {"light": "<file id>", "dark": null, "mark": null}',
|
|
1990
|
+
' tone {"text": "Short sentences. Say what the customer gets."}',
|
|
1991
|
+
' rules {"items": [{"kind": "do", "text": "Captions on every video."}, {"kind": "dont", "text": "..."}]}',
|
|
1992
|
+
' formats {"video": ["16:9", "9:16"], "deck": ["16:9"], "carousel": ["4:5"]}',
|
|
1993
|
+
" A file id comes from orlan brand upload.",
|
|
1994
|
+
"",
|
|
1995
|
+
"--file <path> The content in a JSON file.",
|
|
1996
|
+
"--data <json> The content as JSON text.",
|
|
1997
|
+
"--note <text> Where you took it from, or what you changed. People read it in the kit versions.",
|
|
1998
|
+
"--request <id> The request you answer, for example brand_12. Default: your open requests on the section.",
|
|
1999
|
+
AGENT_TOKEN_HELP,
|
|
2000
|
+
JSON_HELP,
|
|
2001
|
+
],
|
|
2002
|
+
options: {
|
|
2003
|
+
...agentOption,
|
|
2004
|
+
...jsonOption,
|
|
2005
|
+
file: { type: "string" },
|
|
2006
|
+
data: { type: "string" },
|
|
2007
|
+
note: { type: "string" },
|
|
2008
|
+
request: { type: "string" },
|
|
2009
|
+
},
|
|
2010
|
+
positionals: [1, 1],
|
|
2011
|
+
run: async (values, [section], io) => {
|
|
2012
|
+
const file = typeof values.file === "string" ? values.file : undefined;
|
|
2013
|
+
const data = typeof values.data === "string" ? values.data : undefined;
|
|
2014
|
+
if ((file === undefined) === (data === undefined)) {
|
|
2015
|
+
throw new OrlanError("Give the content with --file or --data, one of them.", undefined, `orlan brand draft ${section} --file <path to the JSON content>`);
|
|
2016
|
+
}
|
|
2017
|
+
const text = file === undefined ? data : await readFile(file, "utf8");
|
|
2018
|
+
let content;
|
|
2019
|
+
try {
|
|
2020
|
+
content = JSON.parse(text);
|
|
2021
|
+
}
|
|
2022
|
+
catch {
|
|
2023
|
+
throw new OrlanError(`The content is not JSON${file ? ` in ${quote(file)}` : ""}.`, undefined, "orlan brand draft --help shows the content of each section");
|
|
2024
|
+
}
|
|
2025
|
+
return withAgent(values, async (api) => {
|
|
2026
|
+
const answer = await api.call("brand_draft", {
|
|
2027
|
+
section,
|
|
2028
|
+
content,
|
|
2029
|
+
...(typeof values.note === "string" ? { note: values.note } : {}),
|
|
2030
|
+
...(typeof values.request === "string" ? { requestId: values.request } : {}),
|
|
2031
|
+
});
|
|
2032
|
+
const { nextStep, ...rest } = answer;
|
|
2033
|
+
return finish(io, values, rest, [
|
|
2034
|
+
`Wrote ${answer.section} as a draft in brand kit v${answer.version}. It waits for a person to accept it.` +
|
|
2035
|
+
(answer.answered ? ` Answered ${answer.answered.join(", ")}.` : ""),
|
|
2036
|
+
], nextStep);
|
|
2037
|
+
});
|
|
2038
|
+
},
|
|
2039
|
+
},
|
|
2040
|
+
"brand upload": {
|
|
2041
|
+
usage: "orlan brand upload <path> --kind logo|font [--agent <id>] [--json]",
|
|
2042
|
+
summary: "Upload a logo or a font file for the brand kit, and print its file id.",
|
|
2043
|
+
details: [
|
|
2044
|
+
"A logo is a PNG or a JPEG file; a font is an .otf, .ttf, .woff or .woff2 file. Name the file id in",
|
|
2045
|
+
"the logos or the fonts section with orlan brand draft.",
|
|
2046
|
+
"",
|
|
2047
|
+
"--kind <kind> logo or font. Required.",
|
|
2048
|
+
AGENT_TOKEN_HELP,
|
|
2049
|
+
JSON_HELP,
|
|
2050
|
+
],
|
|
2051
|
+
options: { ...agentOption, ...jsonOption, kind: { type: "string" } },
|
|
2052
|
+
positionals: [1, 1],
|
|
2053
|
+
run: async (values, [filePath], io) => {
|
|
2054
|
+
const kind = values.kind;
|
|
2055
|
+
if (kind !== "logo" && kind !== "font") {
|
|
2056
|
+
throw new OrlanError("Add --kind logo or --kind font.", undefined, `orlan brand upload ${filePath} --kind logo`);
|
|
2057
|
+
}
|
|
2058
|
+
const form = new FormData();
|
|
2059
|
+
form.set("file", await openAsBlob(filePath), path.basename(filePath));
|
|
2060
|
+
return withAgent(values, async (api) => {
|
|
2061
|
+
const uploaded = await api.postForm("/api/agent/brand/files", { kind }, form, "the upload");
|
|
2062
|
+
return finish(io, values, uploaded, [`Uploaded ${quote(uploaded.name)} as ${uploaded.kind} file ${uploaded.id}.`], `name ${uploaded.id} in the ${kind === "logo" ? "logos" : "fonts"} section with orlan brand draft`);
|
|
2063
|
+
});
|
|
2064
|
+
},
|
|
2065
|
+
},
|
|
2066
|
+
studio: {
|
|
2067
|
+
usage: "orlan studio [<skill> [<step>]] [--agent <id>] [--json]",
|
|
2068
|
+
summary: "Print one step of a studio skill (video, deck, carousel, walkthrough): the steps come from Orlan.",
|
|
2069
|
+
details: [
|
|
2070
|
+
"A studio skill makes a piece of work on this computer in five steps: brief, script, build, render,",
|
|
2071
|
+
"review. Orlan gives the steps only to a signed-in agent, one step at a time: read each step when you",
|
|
2072
|
+
"come to it, and follow it. Read the brand kit first (orlan brand).",
|
|
2073
|
+
"",
|
|
2074
|
+
"orlan studio The skills, with their steps and helper scripts.",
|
|
2075
|
+
"orlan studio <skill> The steps and the helper scripts of one skill.",
|
|
2076
|
+
"orlan studio <skill> <step> The text of one step.",
|
|
2077
|
+
"",
|
|
2078
|
+
"Get the helper scripts of a skill with orlan studio pull <skill>.",
|
|
2079
|
+
AGENT_TOKEN_HELP,
|
|
2080
|
+
JSON_HELP,
|
|
2081
|
+
],
|
|
2082
|
+
options: { ...agentOption, ...jsonOption },
|
|
2083
|
+
positionals: [0, 2],
|
|
2084
|
+
run: (values, [skill, step], io) => withStudio(values, async (api) => {
|
|
2085
|
+
if (!skill) {
|
|
2086
|
+
const { skills } = await api.call("studio", {});
|
|
2087
|
+
const lines = skills.length
|
|
2088
|
+
? skills.map((each) => `${each.skill}: ${skillLine(each)}`)
|
|
2089
|
+
: ["Orlan has no studio skills yet."];
|
|
2090
|
+
return finish(io, values, { skills }, lines, "orlan studio <skill> brief");
|
|
2091
|
+
}
|
|
2092
|
+
if (!step) {
|
|
2093
|
+
const found = await api.call("studio", { skill });
|
|
2094
|
+
const next = `${found.scripts.length ? `orlan studio pull ${skill}, then ` : ""}orlan studio ${skill} brief`;
|
|
2095
|
+
return finish(io, values, { ...found }, [`${skill}: ${skillLine(found)}`], next);
|
|
2096
|
+
}
|
|
2097
|
+
const answer = await api.call("studio", { skill, step });
|
|
2098
|
+
const after = STUDIO_STEPS[STUDIO_STEPS.indexOf(step) + 1];
|
|
2099
|
+
return finish(io, values, answer, [answer.text.trimEnd()], after
|
|
2100
|
+
? `orlan studio ${skill} ${after}`
|
|
2101
|
+
: "post the work: orlan files add <file>, or orlan files push for a new version");
|
|
2102
|
+
}),
|
|
2103
|
+
},
|
|
2104
|
+
"studio pull": {
|
|
2105
|
+
usage: "orlan studio pull <skill> [--to <folder>] [--agent <id>] [--json]",
|
|
2106
|
+
summary: "Write the helper scripts of a studio skill into the project, from Orlan.",
|
|
2107
|
+
details: [
|
|
2108
|
+
"The steps of the skill name these scripts. Pull them again for the newest copy: each file is written",
|
|
2109
|
+
"again, and files that you added stay.",
|
|
2110
|
+
"",
|
|
2111
|
+
"--to <folder> The folder to write the scripts in. Default: .orlan/studio/<skill> in this folder.",
|
|
2112
|
+
AGENT_TOKEN_HELP,
|
|
2113
|
+
JSON_HELP,
|
|
2114
|
+
],
|
|
2115
|
+
options: { ...agentOption, ...jsonOption, to: { type: "string" } },
|
|
2116
|
+
positionals: [1, 1],
|
|
2117
|
+
run: (values, [skill], io) => withStudio(values, async (api) => {
|
|
2118
|
+
const { files } = await api.call("studio", { skill, scripts: true });
|
|
2119
|
+
const folder = path.resolve(typeof values.to === "string" ? values.to : path.join(".orlan", "studio", `${skill}`));
|
|
2120
|
+
for (const file of files) {
|
|
2121
|
+
const target = path.resolve(folder, file.path);
|
|
2122
|
+
// A path from the server stays in the folder.
|
|
2123
|
+
if (!target.startsWith(`${folder}${path.sep}`)) {
|
|
2124
|
+
throw new OrlanError(`Orlan named a script path outside the folder: ${quote(file.path)}.`, "internal_error");
|
|
2125
|
+
}
|
|
2126
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
2127
|
+
await writeFile(target, Buffer.from(file.contentBase64, "base64"));
|
|
2128
|
+
await chmod(target, file.executable ? 0o755 : 0o644);
|
|
2129
|
+
}
|
|
2130
|
+
return finish(io, values, { skill, folder, files: files.map((file) => file.path) }, [
|
|
2131
|
+
`Pulled ${files.length} helper scripts of ${skill} into ${folder}.`,
|
|
2132
|
+
...files.map((file) => `- ${file.path}`),
|
|
2133
|
+
], `orlan studio ${skill} brief`);
|
|
2134
|
+
}),
|
|
2135
|
+
},
|
|
1601
2136
|
brief: {
|
|
1602
2137
|
usage: "orlan brief [--topic <topic>] [--agent <id>] [--json]",
|
|
1603
2138
|
summary: "Print where the work stands: the topic, the open requests, the files, and the next step.",
|
|
1604
2139
|
details: [
|
|
1605
|
-
"For the start of a
|
|
1606
|
-
"
|
|
2140
|
+
"For the start of a task: one short answer, in one plain HTTP call. The topic is the one the person",
|
|
2141
|
+
"has open in Orlan now, and the first lines say so. When the agent cannot reach that topic, they say",
|
|
2142
|
+
"so, give the step that grants it, and name the topic the agent works in instead: the newer of the",
|
|
2143
|
+
"one it used last and the one the person opened last, else its only topic. Other commands with no",
|
|
2144
|
+
"--topic use the same topic; in one session they keep the topic of the last call for 5 minutes.",
|
|
2145
|
+
"It lists the open requests (`orlan inbox` prints them in full),",
|
|
1607
2146
|
"the files with their current versions, the open vote and the open tasks (one line each), and one",
|
|
1608
2147
|
"next_step line. People vote; an agent reads the vote, it does not vote.",
|
|
1609
2148
|
"",
|
|
@@ -1614,7 +2153,9 @@ export const commands = {
|
|
|
1614
2153
|
options: { ...agentOption, ...jsonOption, topic: { type: "string" } },
|
|
1615
2154
|
positionals: [0, 0],
|
|
1616
2155
|
run: (values, _positionals, io) => withAgent(values, async (api) => {
|
|
1617
|
-
const { lines, nextStep, ...answer } = await api.get("/api/agent/brief",
|
|
2156
|
+
const { lines, nextStep, ...answer } = await api.get("/api/agent/brief",
|
|
2157
|
+
// The start of a task: Orlan takes the topic the person has open now again (F123).
|
|
2158
|
+
{ topic: values.topic, start: "true" }, "the brief");
|
|
1618
2159
|
// The lines end with Orlan's next_step line.
|
|
1619
2160
|
io.out(values.json ? JSON.stringify({ ...answer, next_step: nextStep }) : lines.join("\n"));
|
|
1620
2161
|
return 0;
|
|
@@ -1632,7 +2173,7 @@ export const commands = {
|
|
|
1632
2173
|
options: { ...agentOption, ...jsonOption, new: { type: "boolean" } },
|
|
1633
2174
|
positionals: [0, 0],
|
|
1634
2175
|
run: (values, _positionals, io) => withAgent(values, async (api) => {
|
|
1635
|
-
const { requests, stopped } = await api.get("/api/inbox", { new: values.new ? "true" : undefined }, "the inbox");
|
|
2176
|
+
const { requests, stopped, brand = [], } = await api.get("/api/inbox", { new: values.new ? "true" : undefined }, "the inbox");
|
|
1636
2177
|
// A person stopped this session on the board (D109): the stop, also with --new.
|
|
1637
2178
|
if (stopped) {
|
|
1638
2179
|
const [line = "", step = ""] = stopped.lines;
|
|
@@ -1644,20 +2185,23 @@ export const commands = {
|
|
|
1644
2185
|
return 0;
|
|
1645
2186
|
}
|
|
1646
2187
|
// --new with no request prints nothing.
|
|
2188
|
+
// The brand kit requests (F141) come after the topic requests, each with its own next_step line.
|
|
2189
|
+
const all = [...requests, ...brand];
|
|
1647
2190
|
if (values.json) {
|
|
1648
|
-
if (
|
|
1649
|
-
const last =
|
|
2191
|
+
if (all.length > 0 || !values.new) {
|
|
2192
|
+
const last = all.at(-1);
|
|
1650
2193
|
printJson(io, {
|
|
1651
2194
|
requests: requests.map((each) => each.request),
|
|
2195
|
+
...(brand.length > 0 ? { brand: brand.map((each) => each.request) } : {}),
|
|
1652
2196
|
next_step: last ? deliveryStep(last) : NOTHING_STEP,
|
|
1653
2197
|
});
|
|
1654
2198
|
}
|
|
1655
2199
|
}
|
|
1656
2200
|
else {
|
|
1657
|
-
if (
|
|
2201
|
+
if (all.length === 0 && !values.new)
|
|
1658
2202
|
io.out(`No open requests.\nnext_step: ${NOTHING_STEP}`);
|
|
1659
2203
|
// Each request ends with its own next_step line. The line on quoted text comes once.
|
|
1660
|
-
for (const [index, delivery] of
|
|
2204
|
+
for (const [index, delivery] of all.entries()) {
|
|
1661
2205
|
if (index > 0)
|
|
1662
2206
|
io.out("");
|
|
1663
2207
|
const [first = "", ...rest] = delivery.lines;
|
|
@@ -1723,7 +2267,7 @@ export const commands = {
|
|
|
1723
2267
|
const api = new AgentApi(config.server, token);
|
|
1724
2268
|
// The file goes to the request's topic in one request; a done request refuses it before it is read.
|
|
1725
2269
|
const uploadId = typeof values.file === "string"
|
|
1726
|
-
? await api.upload(await
|
|
2270
|
+
? await api.upload(await openAsBlob(values.file), { request: requestId, name: path.basename(values.file) }, "respond")
|
|
1727
2271
|
: undefined;
|
|
1728
2272
|
const answer = await api.call("respond", {
|
|
1729
2273
|
requestId,
|
|
@@ -1770,14 +2314,20 @@ export const commands = {
|
|
|
1770
2314
|
},
|
|
1771
2315
|
},
|
|
1772
2316
|
};
|
|
1773
|
-
/**
|
|
1774
|
-
|
|
2317
|
+
/**
|
|
2318
|
+
* Runs edit_board on the topic of --topic, else the default topic: one step, all edits or none. Edits that
|
|
2319
|
+
* add items can make a new topic (--new-topic, F131); the answer names it.
|
|
2320
|
+
*/
|
|
2321
|
+
async function editBoard(api, values, io, edits) {
|
|
2322
|
+
const { topicId, newTopic } = edits.some((edit) => edit.op === "create")
|
|
2323
|
+
? await writeTopic(api, values, io)
|
|
2324
|
+
: { topicId: await topicOrDefault(api, values, io), newTopic: undefined };
|
|
1775
2325
|
const answer = await api.call("edit_board", {
|
|
1776
|
-
topicId
|
|
2326
|
+
topicId,
|
|
1777
2327
|
edits,
|
|
1778
2328
|
});
|
|
1779
2329
|
// A server before F096 gives no warnings.
|
|
1780
|
-
return { ...answer, warnings: answer.warnings ?? [] };
|
|
2330
|
+
return { ...answer, warnings: answer.warnings ?? [], ...(newTopic ? { newTopic } : {}) };
|
|
1781
2331
|
}
|
|
1782
2332
|
/** --assignee as the tool takes it: me (the token of this agent), user:<id>, agent:<id>, or none (null). */
|
|
1783
2333
|
function assigneeOption(config, values) {
|
|
@@ -1809,6 +2359,11 @@ function voteText(vote) {
|
|
|
1809
2359
|
`, ${vote.voters} voted` +
|
|
1810
2360
|
(result ? `, result: ${result}` : ""));
|
|
1811
2361
|
}
|
|
2362
|
+
/** A scorecard as its sum of the most it can be (F139): "65/80". */
|
|
2363
|
+
function scoreTotal(scores) {
|
|
2364
|
+
const sum = scores.reduce((total, each) => total + each.score, 0);
|
|
2365
|
+
return `${Math.round(sum * 10) / 10}/${scores.length * 10}`;
|
|
2366
|
+
}
|
|
1812
2367
|
/** --pages as page numbers: "3" or "2,5". */
|
|
1813
2368
|
function pageList(values) {
|
|
1814
2369
|
if (typeof values.pages !== "string")
|
|
@@ -1884,3 +2439,10 @@ async function pullPages(api, config, values, io, answer, since) {
|
|
|
1884
2439
|
}
|
|
1885
2440
|
/** The changed pages of a version, as words: "pages 2, 5 changed", "no page changed". */
|
|
1886
2441
|
const changedText = (pages) => pages.length === 0 ? "no page changed" : `page${pages.length === 1 ? "" : "s"} ${pages.join(", ")} changed`;
|
|
2442
|
+
/** A brand request as text, as `orlan inbox` prints it. Every text people wrote is quoted. */
|
|
2443
|
+
const brandRequestLines = (request) => [
|
|
2444
|
+
`${request.id}: a request from ${quote(request.from)} (person) on Orlan, brand kit section ${request.section}. ${QUOTED_TEXT}`,
|
|
2445
|
+
`${quote(request.from)} wrote: ${quote(request.text)}`,
|
|
2446
|
+
`${request.section} now: ${JSON.stringify(request.current)}`,
|
|
2447
|
+
`next_step: write the changed ${request.section} as JSON in a file, then orlan brand draft ${request.section} --file <path> --request ${request.id} --note "<what changed>"`,
|
|
2448
|
+
];
|