@orlan-maker/cli 0.11.0 → 0.12.1

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 CHANGED
@@ -67,7 +67,8 @@ orlan topics list
67
67
  orlan files list [--topic <topic>]
68
68
  orlan files add <path> [--at <x,y>] [--topic <topic>]
69
69
  orlan files pull <file> [--version <n>] [--pages <n,n> | --since <n>] [--topic <topic>] [--out <path>] [--edit [--shared] [--ttl <time>]]
70
- orlan files push <file> <path> --changelog "<what changed>" [--answers <n,n>] [--scores <scores.json>] [--base <n>] [--topic <topic>]
70
+ orlan files push <file> <path> --changelog "<what changed>" [--answers <n,n>] [--scores <scores.json>] [--source <folder>] [--base <n>] [--topic <topic>]
71
+ orlan files source <file> [--number <n>] [--out <folder>] [--topic <topic>]
71
72
  orlan files rollback <file> --version <n> [--changelog "<why>"] [--topic <topic>]
72
73
  orlan comments list [--topic <topic>] [--status open|resolved|all] [--file <file>]
73
74
  orlan comment <thread> "<text>" | --file <file> --page <n> [--region <x,y,w,h>] "<text>" | --at <x,y> "<text>" [--topic <topic>]
package/dist/commands.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * arguments and runs one of them.
4
4
  */
5
5
  import { openAsBlob } from "node:fs";
6
- import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
6
+ import { chmod, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
7
7
  import { hostname } from "node:os";
8
8
  import path from "node:path";
9
9
  import { agentIds, agentLabels, detectAgent, parseAgent, planOldInstall, skillsDir, } from "./agents.js";
@@ -135,6 +135,12 @@ const shotsHelp = (column) => [
135
135
  '[{ "name": "Hook", "start": 0, "end": 4.5 }], in seconds. The review shows the scene list.',
136
136
  "Orlan refuses a bad list with shots_invalid, and names the problem.",
137
137
  ].map((line, index) => (index === 0 ? "--shots <file>" : "").padEnd(column) + line);
138
+ /** The help of --source (F149), its text from `column` on. */
139
+ const sourceHelp = (column) => [
140
+ "For a video: its source folder, the hf/ folder of the video skill. Orlan keeps it with the",
141
+ "version, so a person can change its fields with Quick edit and you render it again. Files and",
142
+ "folders whose names start with a dot are left out. Orlan refuses a source with GSAP code.",
143
+ ].map((line, index) => (index === 0 ? "--source <folder>" : "").padEnd(column) + line);
138
144
  /** The help of --scores (F139), its text from `column` on. */
139
145
  const scoresHelp = (column) => [
140
146
  "The critic scorecard of the version: a JSON file of 1 to 20 scores, for example",
@@ -298,6 +304,33 @@ async function shotsOption(values) {
298
304
  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
305
  }
300
306
  }
307
+ /**
308
+ * --source of files add and files push (F149): the files of a video's source folder, as one JSON bundle
309
+ * { format: "orlan-video-source", files: { path: base64 } }. Names that start with a dot (hf/.hyperframes,
310
+ * .DS_Store) and node_modules are left out. Orlan checks the source.
311
+ */
312
+ async function sourceOption(values) {
313
+ const folder = values.source;
314
+ if (typeof folder !== "string")
315
+ return undefined;
316
+ const entries = await readdir(folder, { recursive: true, withFileTypes: true }).catch(() => {
317
+ throw new OrlanError(`Cannot read the source folder ${quote(folder)}.`, "source_invalid", "check the path of --source");
318
+ });
319
+ const files = {};
320
+ for (const entry of entries) {
321
+ if (!entry.isFile())
322
+ continue;
323
+ const full = path.join(entry.parentPath, entry.name);
324
+ const relative = path.relative(folder, full).split(path.sep).join("/");
325
+ if (relative.split("/").some((part) => part.startsWith(".") || part === "node_modules"))
326
+ continue;
327
+ files[relative] = (await readFile(full)).toString("base64");
328
+ }
329
+ if (!files["index.html"]) {
330
+ throw new OrlanError(`The source folder ${quote(folder)} has no index.html.`, "source_invalid", "give the hf folder of the video project: --source hf");
331
+ }
332
+ return new Blob([JSON.stringify({ format: "orlan-video-source", files })], { type: "application/json" });
333
+ }
301
334
  /** --scores of files add and files push (F139): the JSON of the scorecard file. Orlan checks the scores. */
302
335
  async function scoresOption(values) {
303
336
  const file = values.scores;
@@ -759,7 +792,7 @@ export const commands = {
759
792
  }),
760
793
  },
761
794
  "files add": {
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]",
795
+ usage: "orlan files add <path>... [--carousel [--name <name>]] [--at <x,y>] [--shots <shots.json>] [--scores <scores.json>] [--source <folder>] [--topic <topic> | --new-topic <name>] [--agent <id>] [--json]",
763
796
  summary: "Post a local file as a new file on a topic, and put it on the board.",
764
797
  details: [
765
798
  "One request sends the file and places it on the board, right of the other items. The server",
@@ -773,6 +806,7 @@ export const commands = {
773
806
  " place. `orlan board` shows where the items are.",
774
807
  ...shotsHelp(17),
775
808
  ...scoresHelp(17),
809
+ ...sourceHelp(17),
776
810
  "--topic <topic> The topic id or its name. Default: the topic open in Orlan now, else the one used last.",
777
811
  ...NEW_TOPIC_HELP,
778
812
  AGENT_TOKEN_HELP,
@@ -786,6 +820,7 @@ export const commands = {
786
820
  at: { type: "string" },
787
821
  shots: { type: "string" },
788
822
  scores: { type: "string" },
823
+ source: { type: "string" },
789
824
  carousel: { type: "boolean" },
790
825
  name: { type: "string" },
791
826
  },
@@ -803,6 +838,10 @@ export const commands = {
803
838
  if (values.carousel && shots.shots !== undefined) {
804
839
  throw new OrlanError("--shots goes with a video, not with --carousel.", "shots_invalid", "leave out --shots");
805
840
  }
841
+ if (values.carousel && values.source !== undefined) {
842
+ throw new OrlanError("--source goes with a video, not with --carousel.", "source_invalid", "leave out --source");
843
+ }
844
+ const source = await sourceOption(values);
806
845
  const { topicId, newTopic } = await writeTopic(api, values, io);
807
846
  const madeTopic = newTopic ? { newTopic } : {};
808
847
  if (values.carousel) {
@@ -837,12 +876,17 @@ export const commands = {
837
876
  `${slides.map((slide) => path.basename(slide)).join(", ")}. It is on the board.`,
838
877
  ], doneStep("comments on the file"));
839
878
  }
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.
879
+ if (at || shots.shots !== undefined || scores.scores !== undefined || source) {
880
+ // A place on the board, a shot list, a scorecard or a source: the file goes up first, then the post
881
+ // places it.
842
882
  const uploadId = await api.upload(await openAsBlob(filePath), {
843
883
  topic: topicId,
844
884
  name: path.basename(filePath),
845
885
  });
886
+ // The source goes up as its own upload, with the name of the video: it has the limit of a video.
887
+ const sourceId = source
888
+ ? await api.upload(source, { topic: topicId, name: path.basename(filePath) }, "the source")
889
+ : undefined;
846
890
  const posted = await api.call("post", {
847
891
  topicId,
848
892
  name: path.basename(filePath),
@@ -850,6 +894,7 @@ export const commands = {
850
894
  ...(at ? { position: { x: at[0], y: at[1] } } : {}),
851
895
  ...shots,
852
896
  ...scores,
897
+ ...(sourceId ? { source: sourceId } : {}),
853
898
  });
854
899
  return finish(io, values, {
855
900
  file: { id: posted.file.id, name: posted.file.name, version: posted.posted.number },
@@ -859,6 +904,7 @@ export const commands = {
859
904
  `Added ${quote(posted.file.name)} as file ${posted.file.id}, v${posted.posted.number}. It is on the ` +
860
905
  `board${at ? ` at ${at.join(",")}, or at the nearest free place` : ""}; its pages render now.`,
861
906
  ...(shots.shots !== undefined ? ["Its shot list shows as the scene list of the video."] : []),
907
+ ...(sourceId ? ["Its source is in Orlan: people can change its fields with Quick edit."] : []),
862
908
  ], doneStep("comments on the file"));
863
909
  }
864
910
  const { nextStep, ...answer } = await api.postFile("/api/agent/files", { name: path.basename(filePath), topic: topicId }, await openAsBlob(filePath), "the new file");
@@ -962,7 +1008,7 @@ export const commands = {
962
1008
  }),
963
1009
  },
964
1010
  "files push": {
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]',
1011
+ usage: 'orlan files push <file> <path>... --changelog "<what changed>" [--answers <n,n>] [--base <n>] [--shots <shots.json>] [--scores <scores.json>] [--source <folder>] [--topic <topic>] [--agent <id>] [--json]',
966
1012
  summary: "Post a local file as a new version of a file.",
967
1013
  details: [
968
1014
  "Two requests: one sends the file, one posts it with the changelog. It must be the same type of",
@@ -978,6 +1024,7 @@ export const commands = {
978
1024
  "--base <n> The version you edited, for example 4 or v4. Default: the version your `pull --edit` got.",
979
1025
  ...shotsHelp(20),
980
1026
  ...scoresHelp(20),
1027
+ ...sourceHelp(20),
981
1028
  "--topic <topic> The topic of a file name, when two topics have a file with that name.",
982
1029
  AGENT_TOKEN_HELP,
983
1030
  JSON_HELP,
@@ -990,6 +1037,7 @@ export const commands = {
990
1037
  topic: { type: "string" },
991
1038
  shots: { type: "string" },
992
1039
  scores: { type: "string" },
1040
+ source: { type: "string" },
993
1041
  answers: { type: "string" },
994
1042
  },
995
1043
  positionals: [2, CAROUSEL_SLIDES_MAX + 1],
@@ -1002,6 +1050,7 @@ export const commands = {
1002
1050
  const shots = await shotsOption(values);
1003
1051
  const scores = await scoresOption(values);
1004
1052
  const answers = answersOption(values);
1053
+ const source = await sourceOption(values);
1005
1054
  // Several paths are the slides of a new carousel version (F136), in the order of their names.
1006
1055
  const uploadIds = [];
1007
1056
  for (const filePath of filePaths.length > 1 ? slideOrder(filePaths) : filePaths) {
@@ -1019,12 +1068,81 @@ export const commands = {
1019
1068
  ...shots,
1020
1069
  ...scores,
1021
1070
  ...answers,
1071
+ ...(source
1072
+ ? {
1073
+ source: await api.upload(source, { file: fileRef, topic: values.topic }, "the source"),
1074
+ }
1075
+ : {}),
1022
1076
  });
1023
1077
  return finish(io, values, { file: { id: posted.file.id, name: posted.file.name }, version: posted.posted.number }, [
1024
1078
  `Posted version ${posted.posted.number} of ${quote(posted.file.name)}. It waits for a person to make it current.`,
1025
1079
  ], doneStep("comments on the version"));
1026
1080
  }),
1027
1081
  },
1082
+ "files source": {
1083
+ usage: "orlan files source <file> [--number <n>] [--out <folder>] [--topic <topic>] [--agent <id>] [--json]",
1084
+ summary: "Pull the source of a video into a folder, to render it again.",
1085
+ details: [
1086
+ "The source is the hf/ folder that went up with `files push --source`. A person changes its fields",
1087
+ "with Quick edit in Orlan: that saves a new source, which waits for your render. Pull it, render",
1088
+ "it with the video skill, and push the mp4 with --source as the next version.",
1089
+ "",
1090
+ "<file> The file id, or its name where it is unique on the topic.",
1091
+ "--number <n> A source number. Default: the newest source.",
1092
+ "--out <folder> The folder to write it into. Default: hf. Files of the source replace the ones",
1093
+ " there; other files stay.",
1094
+ "--topic <topic> The topic of a file name, when two topics have a file with that name.",
1095
+ AGENT_TOKEN_HELP,
1096
+ JSON_HELP,
1097
+ ],
1098
+ options: {
1099
+ ...agentOption,
1100
+ ...jsonOption,
1101
+ number: { type: "string" },
1102
+ out: { type: "string" },
1103
+ topic: { type: "string" },
1104
+ },
1105
+ positionals: [1, 1],
1106
+ run: (values, [fileRef], io) => withAgent(values, async (api) => {
1107
+ const number = typeof values.number === "string" ? values.number.replace(/^#/, "") : undefined;
1108
+ if (number !== undefined && !/^[1-9]\d*$/.test(number)) {
1109
+ throw new OrlanError("--number is a source number, for example 2.", undefined, "leave out --number for the newest");
1110
+ }
1111
+ const found = await api.call("get_file", {
1112
+ artifactId: fileRef,
1113
+ ...topicInput(values),
1114
+ pageImages: "none",
1115
+ });
1116
+ const source = await api.get(`/api/artifacts/${found.file.id}/source/files`, { number }, "the source");
1117
+ const out = typeof values.out === "string" ? values.out : "hf";
1118
+ const root = path.resolve(out);
1119
+ for (const [name, base64] of Object.entries(source.files)) {
1120
+ const target = path.resolve(root, name);
1121
+ // Orlan checks the paths of a source; the CLI writes nothing outside the folder all the same.
1122
+ if (!target.startsWith(`${root}${path.sep}`))
1123
+ throw new OrlanError(`The source has a bad path ${quote(name)}.`);
1124
+ await mkdir(path.dirname(target), { recursive: true });
1125
+ await writeFile(target, Buffer.from(base64, "base64"));
1126
+ }
1127
+ const count = Object.keys(source.files).length;
1128
+ const waits = source.versionNumber === null;
1129
+ return finish(io, values, {
1130
+ file: { id: found.file.id, name: found.file.name },
1131
+ source: source.number,
1132
+ version: source.versionNumber,
1133
+ changes: source.changes,
1134
+ saved: out,
1135
+ files: count,
1136
+ }, [
1137
+ `Saved source ${source.number} of ${quote(found.file.name)} to ${quote(out)}: ${count} file${count === 1 ? "" : "s"}.`,
1138
+ waits
1139
+ ? `It waits for a render. Changed in Quick edit: ${source.changes.join("; ") || "nothing named"}.`
1140
+ : `Version ${source.versionNumber} was rendered from it.`,
1141
+ ], waits
1142
+ ? `render it (node video.mjs audio, then node video.mjs render), then orlan files push ${found.file.id} renders/<video>.mp4 --source ${out} --changelog "Rendered source ${source.number}"`
1143
+ : doneStep());
1144
+ }),
1145
+ },
1028
1146
  "files rollback": {
1029
1147
  usage: 'orlan files rollback <file> --version <n> [--changelog "<why>"] [--topic <topic>] [--agent <id>] [--json]',
1030
1148
  summary: "Post an older version of a file again, as its newest version.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orlan-maker/cli",
3
- "version": "0.11.0",
3
+ "version": "0.12.1",
4
4
  "description": "The orlan command: connect Claude Code, Codex, Cursor or OpenCode to Orlan with a token and skills, and work with its topics, files and comments from the terminal.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -38,6 +38,12 @@ description: Get a file from Orlan and post a new version of it with a changelog
38
38
  critic round. People see it read only in the review and in Compare, with the change from the version
39
39
  before. Orlan refuses a bad scorecard with `scores_invalid` and names the problem. `orlan files list`
40
40
  shows the total of the newest scored version; `orlan comments list` shows "answered in v<n>".
41
+ - `--source hf` on `files add` or `files push` of a video - its source folder (the hf/ folder of the
42
+ video skill). Orlan keeps it with the version. A person changes its fields (scene text, images, colors,
43
+ scene lengths) with Quick edit in Orlan. That saves a new source, and a comment asks you to render it.
44
+ Orlan refuses a source with GSAP code with `source_invalid`.
45
+ - `orlan files source <file> [--out hf]` - saves the newest source of a video into the folder. When it
46
+ waits for a render, render it and push the mp4 with `--source hf` as the next version.
41
47
  - `orlan files rollback <file> --version 3 --changelog "<why>"` - posts an older version again, as the
42
48
  newest version. It waits for a person to make it current, as a push does.
43
49
  - `<file>` is the file id or the file name, and `<topic>` the topic id or its name. When a name is on two