cyber-agentkit 1.1.0 → 1.2.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/dist/cli.js CHANGED
@@ -938,7 +938,7 @@ function countFollowing(entry, o) {
938
938
  return o.inline.length === 0 && takesValue$1(optionType(entry)) ? 1 : 0;
939
939
  }
940
940
  const flag = t.boolean().optional();
941
- const text = t.string();
941
+ const text$1 = t.string();
942
942
  function optionType(entry) {
943
943
  return entry.type ?? flag;
944
944
  }
@@ -956,7 +956,7 @@ function fillArguments(state) {
956
956
  const args = [...state.rawArgs._];
957
957
  const argSpecs = command.arguments || [];
958
958
  for (const s of argSpecs) {
959
- const type = s.type ?? text;
959
+ const type = s.type ?? text$1;
960
960
  if (args.length === 0) {
961
961
  if (!type.isOptional) state.errors.push({
962
962
  type: "missing-argument",
@@ -13516,7 +13516,7 @@ function parseTemplate(content, label) {
13516
13516
  * A title: the top level is a single heading holding every other one. `patch` matches by heading
13517
13517
  * path, so it would add the whole document under the title instead of patching its sections.
13518
13518
  */
13519
- function titleOf({ body }) {
13519
+ function titleOf$1({ body }) {
13520
13520
  const tree = parseSections(body, () => {});
13521
13521
  const [only] = tree.children;
13522
13522
  return tree.children.length === 1 && only?.children.length ? only.heading.trim() : void 0;
@@ -13681,7 +13681,7 @@ function createContribution(layers, args, options = {}) {
13681
13681
  metadata: {},
13682
13682
  body: ""
13683
13683
  } : readTemplate(args.template === void 0 ? void 0 : resolve(args.template));
13684
- const title = titleOf(template);
13684
+ const title = titleOf$1(template);
13685
13685
  if (title) throw new ReferenceWriteError("template-title", `the template's top level is the single heading "${title}" holding the others; patch matches by heading path, so make its sections top-level.`);
13686
13686
  const before = resolveReference(name, layers, options);
13687
13687
  const hider = hiddenBy(before, target.dir, target.path);
@@ -13821,7 +13821,7 @@ function fail$1(error, fallback) {
13821
13821
  process.stderr.write(`error: ${error instanceof Error ? error.message : fallback}\n`);
13822
13822
  throw CliError.reported(exitCodes.error);
13823
13823
  }
13824
- function usage$1(message) {
13824
+ function usage$2(message) {
13825
13825
  process.stderr.write(`error: usage: ${message}\n`);
13826
13826
  throw CliError.reported(exitCodes.usage);
13827
13827
  }
@@ -13922,7 +13922,7 @@ function showCommand$1(layersFor, name = "show") {
13922
13922
  },
13923
13923
  async run(args) {
13924
13924
  const format = this.format;
13925
- if (!args.names.length) usage$1("name at least one reference.");
13925
+ if (!args.names.length) usage$2("name at least one reference.");
13926
13926
  try {
13927
13927
  const home = homedir();
13928
13928
  const layers = await layersFor(args, home);
@@ -14026,7 +14026,7 @@ function searchCommand(layersFor) {
14026
14026
  },
14027
14027
  async run(args) {
14028
14028
  const query = args.query.trim();
14029
- if (!query) usage$1("search needs a query.");
14029
+ if (!query) usage$2("search needs a query.");
14030
14030
  try {
14031
14031
  const home = homedir();
14032
14032
  const { matches, defaults } = searchReferences(query, await layersFor(args, home), { display: (path) => collapseHome(home, path) });
@@ -14105,7 +14105,7 @@ function whereCommand(layersFor, assets = false, name = "where") {
14105
14105
  function parseAccept(value) {
14106
14106
  if (value === void 0) return void 0;
14107
14107
  const versions = value.split(",").map((part) => part.trim());
14108
- if (!versions.length || versions.some((part) => !/^[1-9]\d*$/.test(part))) usage$1(`--accept is a comma-separated list of versions, such as 1,2; got "${value}".`);
14108
+ if (!versions.length || versions.some((part) => !/^[1-9]\d*$/.test(part))) usage$2(`--accept is a comma-separated list of versions, such as 1,2; got "${value}".`);
14109
14109
  return versions.map(Number);
14110
14110
  }
14111
14111
  function implementationsCommand(layersFor) {
@@ -14644,7 +14644,7 @@ function fail(error, fallback) {
14644
14644
  process.stderr.write(`error: ${error instanceof Error ? error.message : fallback}\n`);
14645
14645
  throw CliError.reported(exitCodes.error);
14646
14646
  }
14647
- function usage(message) {
14647
+ function usage$1(message) {
14648
14648
  process.stderr.write(`error: usage: ${message}\n`);
14649
14649
  throw CliError.reported(exitCodes.usage);
14650
14650
  }
@@ -14734,7 +14734,7 @@ function writeCommand(mode, host) {
14734
14734
  async run(args) {
14735
14735
  try {
14736
14736
  if (args.copy !== void 0) {
14737
- if (args.content !== void 0 || args.from !== void 0) usage("--copy takes neither --content nor --from.");
14737
+ if (args.content !== void 0 || args.from !== void 0) usage$1("--copy takes neither --content nor --from.");
14738
14738
  const home = homedir();
14739
14739
  const root = rootOf(args);
14740
14740
  const done = copyAsset(await referenceLayers({
@@ -14754,8 +14754,8 @@ function writeCommand(mode, host) {
14754
14754
  });
14755
14755
  return document(`${done.path}\n\ncopied from ${done.from}. Edit it; it now answers over the plugin tier.\n`);
14756
14756
  }
14757
- if (!args.skill) usage("pass --skill, or --copy to create an override by name.");
14758
- if (args.content === void 0 === (args.from === void 0)) usage("pass exactly one of --content and --from.");
14757
+ if (!args.skill) usage$1("pass --skill, or --copy to create an override by name.");
14758
+ if (args.content === void 0 === (args.from === void 0)) usage$1("pass exactly one of --content and --from.");
14759
14759
  const content = args.content ?? readFileSync(resolve(args.from), "utf8");
14760
14760
  return document(`${writeAsset(mode, args.skill, rootOf(args), args.path, content)}\n\n${create ? "created" : "updated"}.\n`);
14761
14761
  } catch (error) {
@@ -14858,6 +14858,707 @@ function createAssetCommand(options = {}) {
14858
14858
  });
14859
14859
  }
14860
14860
  //#endregion
14861
+ //#region src/prose-update/split-sentences.ts
14862
+ const ABBREVIATIONS = [
14863
+ "e.g.",
14864
+ "i.e.",
14865
+ "etc.",
14866
+ "vs."
14867
+ ];
14868
+ const FENCE$2 = /^\s*(`{3,}|~{3,})/;
14869
+ const HEADING$2 = /^#{1,6}\s/;
14870
+ const ITEM = /^\s*(?:[-*+]|\d+[.)])\s/;
14871
+ const ROW = /^\s*\|/;
14872
+ const QUOTE = /^\s*>/;
14873
+ function endsAbbreviation(before) {
14874
+ return ABBREVIATIONS.some((abbreviation) => {
14875
+ if (!before.endsWith(abbreviation)) return false;
14876
+ const start = before.length - abbreviation.length;
14877
+ return start === 0 || /\s/.test(before[start - 1]);
14878
+ });
14879
+ }
14880
+ function splitParagraph(paragraph) {
14881
+ const parts = [];
14882
+ let start = 0;
14883
+ for (let i = 0; i < paragraph.length; i++) {
14884
+ if (!".;?!".includes(paragraph[i])) continue;
14885
+ const next = paragraph[i + 1];
14886
+ if (next !== void 0 && !/\s/.test(next)) continue;
14887
+ if (paragraph[i] === "." && endsAbbreviation(paragraph.slice(start, i + 1))) continue;
14888
+ parts.push(paragraph.slice(start, i + 1));
14889
+ start = i + 1;
14890
+ }
14891
+ parts.push(paragraph.slice(start));
14892
+ return parts.map((part) => part.trim()).filter(Boolean);
14893
+ }
14894
+ /**
14895
+ * Splits text into sentences (K4): at `.`, `;`, `?` or `!` before whitespace or the end, except after an
14896
+ * abbreviation; at every blank line; a heading line, list item and table row are one sentence each; a fenced
14897
+ * code block and a blockquote are each one sentence.
14898
+ */
14899
+ function splitSentences(input) {
14900
+ const lines = input.split(/\r?\n/);
14901
+ const out = [];
14902
+ let paragraph = [];
14903
+ let blank = false;
14904
+ const gapBefore = () => out.length === 0 ? "" : blank ? "\n\n" : "\n";
14905
+ const add = (text, kind) => {
14906
+ out.push({
14907
+ text,
14908
+ kind,
14909
+ gap: gapBefore()
14910
+ });
14911
+ blank = false;
14912
+ };
14913
+ const flush = () => {
14914
+ if (!paragraph.length) return;
14915
+ splitParagraph(paragraph.join(" ")).forEach((text, index) => {
14916
+ if (index === 0) add(text, "text");
14917
+ else out.push({
14918
+ text,
14919
+ kind: "text",
14920
+ gap: " "
14921
+ });
14922
+ });
14923
+ paragraph = [];
14924
+ };
14925
+ for (let i = 0; i < lines.length; i++) {
14926
+ const line = lines[i];
14927
+ const fence = FENCE$2.exec(line);
14928
+ if (fence) {
14929
+ flush();
14930
+ const marker = fence[1][0];
14931
+ const block = [line.trimEnd()];
14932
+ while (++i < lines.length) {
14933
+ block.push(lines[i].trimEnd());
14934
+ const close = FENCE$2.exec(lines[i]);
14935
+ if (close && close[1][0] === marker && lines[i].trim() === close[1]) break;
14936
+ }
14937
+ add(block.join("\n"), "fence");
14938
+ } else if (!line.trim()) {
14939
+ flush();
14940
+ blank = true;
14941
+ } else if (HEADING$2.test(line)) {
14942
+ flush();
14943
+ add(line.trim(), "heading");
14944
+ } else if (QUOTE.test(line)) {
14945
+ flush();
14946
+ const block = [line.trim()];
14947
+ while (i + 1 < lines.length && QUOTE.test(lines[i + 1])) block.push(lines[++i].trim());
14948
+ add(block.join("\n"), "quote");
14949
+ } else if (ROW.test(line)) {
14950
+ flush();
14951
+ add(line.trim(), "row");
14952
+ } else if (ITEM.test(line)) {
14953
+ flush();
14954
+ add(line.trim(), "item");
14955
+ } else paragraph.push(line.trim());
14956
+ }
14957
+ flush();
14958
+ return out;
14959
+ }
14960
+ /** Two sentences are equal when they match after each run of whitespace becomes one space (K4). */
14961
+ function sameSentence(a, b) {
14962
+ return a.replace(/\s+/g, " ").trim() === b.replace(/\s+/g, " ").trim();
14963
+ }
14964
+ //#endregion
14965
+ //#region src/prose-update/align.ts
14966
+ /**
14967
+ * Pairs of indexes `[a, b]` of the longest common subsequence of two sentence lists (K10). Equal sentences
14968
+ * repeated in both lists are matched in order, earliest first.
14969
+ */
14970
+ function matchSentences(a, b) {
14971
+ const table = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
14972
+ const at = (i, j) => table[i][j];
14973
+ for (let i = a.length - 1; i >= 0; i--) for (let j = b.length - 1; j >= 0; j--) table[i][j] = sameSentence(a[i].text, b[j].text) ? at(i + 1, j + 1) + 1 : Math.max(at(i + 1, j), at(i, j + 1));
14974
+ const pairs = [];
14975
+ let i = 0;
14976
+ let j = 0;
14977
+ while (i < a.length && j < b.length) if (sameSentence(a[i].text, b[j].text) && at(i, j) === at(i + 1, j + 1) + 1) pairs.push([i++, j++]);
14978
+ else if (at(i + 1, j) >= at(i, j + 1)) i++;
14979
+ else j++;
14980
+ return pairs;
14981
+ }
14982
+ /** The number of sentences two lists share (K10). */
14983
+ function shared(a, b) {
14984
+ return matchSentences(a, b).length;
14985
+ }
14986
+ //#endregion
14987
+ //#region src/prose-update/history.ts
14988
+ const HEADING$1 = /^## Superseded(?:\s+(.*))?$/;
14989
+ const FENCE$1 = /^(`{3,}|~{3,})(.*)$/;
14990
+ const SEPARATOR = " — ";
14991
+ function linesOf(text) {
14992
+ const lines = [];
14993
+ let start = 0;
14994
+ while (start <= text.length) {
14995
+ const newline = text.indexOf("\n", start);
14996
+ const end = newline === -1 ? text.length : newline;
14997
+ lines.push({
14998
+ text: text.slice(start, end),
14999
+ start,
15000
+ end
15001
+ });
15002
+ if (newline === -1) break;
15003
+ start = newline + 1;
15004
+ }
15005
+ return lines;
15006
+ }
15007
+ /** Each line index that opens a `## Superseded` entry, outside fences. */
15008
+ function entryHeadings(lines) {
15009
+ const found = [];
15010
+ let fence;
15011
+ lines.forEach((line, index) => {
15012
+ const open = FENCE$1.exec(line.text);
15013
+ if (fence) {
15014
+ if (open && open[1] === fence && !open[2]?.trim()) fence = void 0;
15015
+ } else if (open) fence = open[1];
15016
+ else if (HEADING$1.test(line.text)) found.push(index);
15017
+ });
15018
+ return found;
15019
+ }
15020
+ /**
15021
+ * Reads the retired wordings of a history text, newest first. Each entry is a `## Superseded <date> — <reason>`
15022
+ * heading followed by the wording in a fence. An entry that does not parse is returned in `invalid`.
15023
+ */
15024
+ function parseHistory(text) {
15025
+ const lines = linesOf(text);
15026
+ const headings = entryHeadings(lines);
15027
+ const entries = [];
15028
+ const invalid = [];
15029
+ headings.forEach((headingIndex, position) => {
15030
+ const heading = lines[headingIndex];
15031
+ const limit = headings[position + 1] ?? lines.length;
15032
+ const bad = () => {
15033
+ invalid.push({
15034
+ position,
15035
+ line: headingIndex + 1,
15036
+ heading: heading.text
15037
+ });
15038
+ };
15039
+ const openIndex = lines.findIndex((line, index) => index > headingIndex && index < limit && FENCE$1.test(line.text));
15040
+ if (openIndex === -1) {
15041
+ bad();
15042
+ return;
15043
+ }
15044
+ const open = lines[openIndex];
15045
+ const marker = FENCE$1.exec(open.text)[1];
15046
+ const closeIndex = lines.findIndex((line, index) => index > openIndex && index < limit && line.text.trimEnd() === marker);
15047
+ const close = lines[closeIndex];
15048
+ if (!close) {
15049
+ bad();
15050
+ return;
15051
+ }
15052
+ const rest = HEADING$1.exec(heading.text)[1] ?? "";
15053
+ const split = rest.indexOf(SEPARATOR);
15054
+ const wording = text.slice(open.end + 1, Math.max(open.end + 1, close.start - 1));
15055
+ entries.push({
15056
+ wording,
15057
+ date: split === -1 ? rest.trim() : rest.slice(0, split).trim(),
15058
+ reason: split === -1 ? "" : rest.slice(split + 3).trim()
15059
+ });
15060
+ });
15061
+ return {
15062
+ entries,
15063
+ invalid
15064
+ };
15065
+ }
15066
+ function fenceFor(wording) {
15067
+ const longest = Math.max(0, ...(wording.match(/^\s*`{3,}/gm) ?? []).map((run) => run.trim().length));
15068
+ return "`".repeat(Math.max(3, longest + 1));
15069
+ }
15070
+ /**
15071
+ * Returns the history text with the outgoing wording added as its newest entry and every other byte unchanged.
15072
+ * The wording equal to the newest retired one changes nothing.
15073
+ */
15074
+ function retire(history, wording, date, reason) {
15075
+ if (parseHistory(history).entries[0]?.wording === wording) return history;
15076
+ const fence = fenceFor(wording);
15077
+ const entry = `## Superseded ${date} ${SEPARATOR.trim()} ${reason}\n\n${fence}markdown\n${wording}\n${fence}\n`;
15078
+ const lines = linesOf(history);
15079
+ const first = entryHeadings(lines)[0];
15080
+ if (first !== void 0) {
15081
+ const at = lines[first].start;
15082
+ return `${history.slice(0, at)}${entry}\n${history.slice(at)}`;
15083
+ }
15084
+ const body = history.endsWith("\n") || history === "" ? history : `${history}\n`;
15085
+ return `${body}${body ? "\n" : ""}${entry}`;
15086
+ }
15087
+ //#endregion
15088
+ //#region src/prose-update/merge.ts
15089
+ function equal(a, b) {
15090
+ return a.length === b.length && a.every((sentence, index) => sameSentence(sentence.text, b[index].text));
15091
+ }
15092
+ /** The revision kind of an offered part over its base part (K6). */
15093
+ function revisionKind(base, offered) {
15094
+ return offered.length >= base.length && equal(base, offered.slice(0, base.length)) ? "append" : "rewrite";
15095
+ }
15096
+ function appendTo(base, found, added) {
15097
+ const last = matchSentences(base, found).find(([i]) => i === base.length - 1);
15098
+ const out = [...found];
15099
+ let at = last ? last[1] + 1 : out.length;
15100
+ for (const sentence of added) {
15101
+ const had = out.findIndex((x, index) => index >= at && sameSentence(x.text, sentence.text));
15102
+ if (had === -1) out.splice(at++, 0, sentence);
15103
+ else at = had + 1;
15104
+ }
15105
+ return out;
15106
+ }
15107
+ /**
15108
+ * Three-way merge at sentence level of one part (R6 to R9). An append revision puts the new sentences after
15109
+ * the base's last sentence as found (R8) and never collides. A rewrite is merged between the sentences both
15110
+ * sides left alone; where both changed the same stretch differently the found sentences stay and the stretch
15111
+ * is reported as a collision.
15112
+ */
15113
+ function mergeSentences(base, found, offered) {
15114
+ const revision = revisionKind(base, offered);
15115
+ if (revision === "append") return {
15116
+ revision,
15117
+ merged: appendTo(base, found, offered.slice(base.length)),
15118
+ collisions: []
15119
+ };
15120
+ const inFound = new Map(matchSentences(base, found));
15121
+ const inOffered = new Map(matchSentences(base, offered));
15122
+ const anchors = [...inFound.keys()].filter((i) => inOffered.has(i)).sort((x, y) => x - y);
15123
+ const merged = [];
15124
+ const collisions = [];
15125
+ let b = 0;
15126
+ let f = 0;
15127
+ let o = 0;
15128
+ for (const anchor of [...anchors, base.length]) {
15129
+ const toB = anchor;
15130
+ const toF = anchor === base.length ? found.length : inFound.get(anchor);
15131
+ const toO = anchor === base.length ? offered.length : inOffered.get(anchor);
15132
+ const segment = {
15133
+ base: base.slice(b, toB),
15134
+ found: found.slice(f, toF),
15135
+ offered: offered.slice(o, toO)
15136
+ };
15137
+ if (equal(segment.found, segment.base)) merged.push(...segment.offered);
15138
+ else if (equal(segment.offered, segment.base) || equal(segment.found, segment.offered)) merged.push(...segment.found);
15139
+ else {
15140
+ merged.push(...segment.found);
15141
+ collisions.push(segment);
15142
+ }
15143
+ if (anchor < base.length) merged.push(found[toF]);
15144
+ b = toB + 1;
15145
+ f = toF + 1;
15146
+ o = toO + 1;
15147
+ }
15148
+ return {
15149
+ revision,
15150
+ merged,
15151
+ collisions
15152
+ };
15153
+ }
15154
+ //#endregion
15155
+ //#region src/prose-update/parts.ts
15156
+ const HEADING = /^(#{1,6})[ \t](.*)$/;
15157
+ const FENCE = /^\s*(`{3,}|~{3,})/;
15158
+ function titleOf(text) {
15159
+ let end = text.trimEnd().length;
15160
+ while (end > 0 && text[end - 1] === "#") end--;
15161
+ return text.slice(0, end).trim();
15162
+ }
15163
+ /** Divides a document into parts by heading path. A heading inside a fence is not a heading. */
15164
+ function parseParts(input) {
15165
+ const lines = input.split(/\r?\n/);
15166
+ const raw = [{
15167
+ level: 0,
15168
+ title: "",
15169
+ body: []
15170
+ }];
15171
+ let fence;
15172
+ for (const line of lines) {
15173
+ const open = FENCE.exec(line);
15174
+ if (fence) {
15175
+ if (open && open[1][0] === fence[0] && line.trim() === open[1]) fence = void 0;
15176
+ } else if (open) fence = open[1];
15177
+ else {
15178
+ const heading = HEADING.exec(line);
15179
+ if (heading) {
15180
+ raw.push({
15181
+ heading: line.trim(),
15182
+ level: heading[1].length,
15183
+ title: titleOf(heading[2]),
15184
+ body: []
15185
+ });
15186
+ continue;
15187
+ }
15188
+ }
15189
+ raw[raw.length - 1]?.body.push(line);
15190
+ }
15191
+ const stack = [];
15192
+ const seen = /* @__PURE__ */ new Map();
15193
+ const parts = [];
15194
+ for (const entry of raw) {
15195
+ const sentences = splitSentences(entry.body.join("\n"));
15196
+ const first = sentences[0];
15197
+ if (entry.heading !== void 0 && first) first.gap = entry.body[0]?.trim() === "" ? "\n\n" : "\n";
15198
+ if (entry.heading === void 0) {
15199
+ if (!sentences.length) continue;
15200
+ } else {
15201
+ while (stack.length && stack[stack.length - 1].level >= entry.level) stack.pop();
15202
+ stack.push({
15203
+ level: entry.level,
15204
+ title: entry.title
15205
+ });
15206
+ }
15207
+ const path = stack.map((item) => item.title);
15208
+ const base = path.join(" > ");
15209
+ const count = (seen.get(base) ?? 0) + 1;
15210
+ seen.set(base, count);
15211
+ parts.push({
15212
+ path,
15213
+ key: count === 1 ? base : `${base} #${count}`,
15214
+ heading: entry.heading,
15215
+ sentences,
15216
+ raw: [entry.heading, ...entry.body].filter((line) => line !== void 0).join("\n").trimEnd().replace(/^\n+/, "")
15217
+ });
15218
+ }
15219
+ return parts;
15220
+ }
15221
+ /** Writes a part: its heading, then each sentence after the separator it came with. */
15222
+ function renderPart(heading, sentences) {
15223
+ let out = heading ?? "";
15224
+ sentences.forEach((sentence, index) => {
15225
+ if (index > 0) out += sentence.gap || " ";
15226
+ else if (heading !== void 0) out += sentence.gap === "\n" ? "\n" : "\n\n";
15227
+ out += sentence.text;
15228
+ });
15229
+ return out;
15230
+ }
15231
+ /** Lines the parts of several texts up by key. Keys keep the order of their first appearance. */
15232
+ function alignParts(texts) {
15233
+ const rows = /* @__PURE__ */ new Map();
15234
+ texts.forEach((parts, column) => {
15235
+ for (const part of parts) {
15236
+ const row = rows.get(part.key) ?? {
15237
+ key: part.key,
15238
+ parts: texts.map(() => void 0)
15239
+ };
15240
+ row.parts[column] = part;
15241
+ rows.set(part.key, row);
15242
+ }
15243
+ });
15244
+ return [...rows.values()];
15245
+ }
15246
+ //#endregion
15247
+ //#region src/prose-update/classify.ts
15248
+ /** The offered wording is empty, so nothing can be classified (R33). */
15249
+ var ProseUpdateError = class extends Error {
15250
+ code;
15251
+ constructor(code, message) {
15252
+ super(message);
15253
+ this.code = code;
15254
+ this.name = "ProseUpdateError";
15255
+ }
15256
+ };
15257
+ const ORDER = [
15258
+ "conflict",
15259
+ "undetermined",
15260
+ "update",
15261
+ "owners",
15262
+ "up-to-date"
15263
+ ];
15264
+ const norm = (text) => text.replace(/\s+/g, " ").trim();
15265
+ const sameList = (a, b) => a.length === b.length && a.every((sentence, index) => sameSentence(sentence.text, b[index].text));
15266
+ function retiredOf(history) {
15267
+ const list = typeof history === "string" ? parseHistory(history).entries.map(({ wording, date }) => ({
15268
+ wording,
15269
+ date
15270
+ })) : history.map((wording) => ({
15271
+ wording,
15272
+ date: void 0
15273
+ }));
15274
+ const seen = /* @__PURE__ */ new Set();
15275
+ return list.map(({ wording, date }, index) => ({
15276
+ text: wording,
15277
+ date,
15278
+ sentences: splitSentences(wording),
15279
+ index
15280
+ })).filter((retired) => {
15281
+ const key = retired.sentences.map((sentence) => norm(sentence.text)).join("\0");
15282
+ if (seen.has(key)) return false;
15283
+ seen.add(key);
15284
+ return true;
15285
+ });
15286
+ }
15287
+ function chooseBase(found, retired) {
15288
+ const counts = retired.map((r) => ({
15289
+ r,
15290
+ count: shared(found, r.sentences)
15291
+ }));
15292
+ const most = Math.max(0, ...counts.map((c) => c.count));
15293
+ if (most < 1) return {};
15294
+ const top = counts.filter((c) => c.count === most).map((c) => c.r);
15295
+ return top.length > 1 ? { tied: top } : { base: top[0] };
15296
+ }
15297
+ function render(part, heading, sentences) {
15298
+ return renderPart(heading ?? part?.heading, sentences);
15299
+ }
15300
+ /** Classifies each part of the found text against the offered wording and the history, and merges where it can. */
15301
+ function classify(input) {
15302
+ if (!splitSentences(input.offered).length) throw new ProseUpdateError("empty-offered", "The offered wording is empty.");
15303
+ const foundSentences = splitSentences(input.found);
15304
+ const foundParts = parseParts(input.found);
15305
+ const offeredParts = parseParts(input.offered);
15306
+ const retired = retiredOf(input.history);
15307
+ const whole = (reason, extra = {}) => {
15308
+ const keys = alignParts([foundParts, offeredParts]);
15309
+ return {
15310
+ outcome: "undetermined",
15311
+ reason,
15312
+ parts: (keys.length ? keys : [{
15313
+ key: "",
15314
+ parts: []
15315
+ }]).map((row) => ({
15316
+ path: (row.parts[0] ?? row.parts[1])?.path ?? [],
15317
+ key: row.key,
15318
+ outcome: "undetermined",
15319
+ reason,
15320
+ ...extra
15321
+ })),
15322
+ text: input.found
15323
+ };
15324
+ };
15325
+ if (!foundSentences.length) return whole("empty-section");
15326
+ if (!retired.length) return whole("no-history");
15327
+ const { base, tied } = chooseBase(foundSentences, retired);
15328
+ if (tied) return whole("tied-base", { tied: tied.map((r) => r.index) });
15329
+ const baseParts = base ? parseParts(base.text) : [];
15330
+ const historySet = new Set(retired.flatMap((r) => r.sentences.map((sentence) => norm(sentence.text))));
15331
+ const baseRef = base && {
15332
+ index: base.index,
15333
+ date: base.date
15334
+ };
15335
+ const rows = alignParts([
15336
+ baseParts,
15337
+ foundParts,
15338
+ offeredParts
15339
+ ]);
15340
+ const results = /* @__PURE__ */ new Map();
15341
+ const merged = /* @__PURE__ */ new Map();
15342
+ for (const row of rows) {
15343
+ const [b, f, o] = row.parts;
15344
+ const result = {
15345
+ path: (f ?? o ?? b)?.path ?? [],
15346
+ key: row.key,
15347
+ outcome: "owners"
15348
+ };
15349
+ results.set(row.key, result);
15350
+ const withBase = (outcome, baseSentences, foundSentencesOfPart, offeredSentencesOfPart) => {
15351
+ result.outcome = outcome;
15352
+ if (baseRef) result.base = baseRef;
15353
+ result.shared = shared(foundSentencesOfPart, baseSentences);
15354
+ result.baseSentences = baseSentences.length;
15355
+ result.revision = revisionKind(baseSentences, offeredSentencesOfPart);
15356
+ };
15357
+ if (!f && o) {
15358
+ if (!b) {
15359
+ result.outcome = "update";
15360
+ result.text = o.raw;
15361
+ merged.set(row.key, o.raw);
15362
+ } else if (sameList(b.sentences, o.sentences)) {
15363
+ result.outcome = "up-to-date";
15364
+ result.text = "";
15365
+ } else {
15366
+ withBase("conflict", b.sentences, [], o.sentences);
15367
+ result.collisions = [{
15368
+ base: b.sentences,
15369
+ found: [],
15370
+ offered: o.sentences
15371
+ }];
15372
+ }
15373
+ } else if (f && !o) {
15374
+ if (!b) result.text = f.raw;
15375
+ else if (sameList(b.sentences, f.sentences)) {
15376
+ result.outcome = "update";
15377
+ result.text = "";
15378
+ merged.set(row.key, "");
15379
+ } else {
15380
+ withBase("conflict", b.sentences, f.sentences, []);
15381
+ result.collisions = [{
15382
+ base: b.sentences,
15383
+ found: f.sentences,
15384
+ offered: []
15385
+ }];
15386
+ }
15387
+ } else if (f && o) {
15388
+ const stale = f.sentences.some((sentence) => historySet.has(norm(sentence.text)) && !o.sentences.some((x) => sameSentence(x.text, sentence.text)));
15389
+ const common = shared(f.sentences, o.sentences);
15390
+ if (!f.sentences.length) {
15391
+ result.outcome = "undetermined";
15392
+ result.reason = "empty-section";
15393
+ } else if (common === o.sentences.length && !stale) {
15394
+ result.outcome = "up-to-date";
15395
+ result.text = f.raw;
15396
+ } else if (!base || !b?.sentences.length) {
15397
+ if (common > 0) {
15398
+ result.outcome = "undetermined";
15399
+ result.reason = "no-base";
15400
+ } else result.text = f.raw;
15401
+ } else if (!shared(f.sentences, b.sentences) && common === 0) result.text = f.raw;
15402
+ else {
15403
+ const merge = mergeSentences(b.sentences, f.sentences, o.sentences);
15404
+ withBase(merge.collisions.length ? "conflict" : "update", b.sentences, f.sentences, o.sentences);
15405
+ result.text = render(f, f.heading ?? o.heading, merge.merged);
15406
+ merged.set(row.key, result.text);
15407
+ if (merge.collisions.length) result.collisions = merge.collisions;
15408
+ }
15409
+ }
15410
+ }
15411
+ const outcome = ORDER.find((o) => [...results.values()].some((r) => r.outcome === o));
15412
+ const changed = [...results.values()].some((r) => r.outcome === "update" || r.outcome === "conflict");
15413
+ const parts = documentOrder(foundParts, offeredParts, results).map((key) => results.get(key));
15414
+ const text = changed ? assemble(input.found, foundParts, offeredParts, merged, results) : input.found;
15415
+ const reason = outcome === "undetermined" ? parts.find((part) => part.outcome === "undetermined")?.reason : void 0;
15416
+ return {
15417
+ outcome,
15418
+ ...reason ? { reason } : {},
15419
+ ...baseRef ? { base: baseRef } : {},
15420
+ parts,
15421
+ text
15422
+ };
15423
+ }
15424
+ /** Found parts in order, each added part after the nearest preceding offered part the found text holds (R22). */
15425
+ function documentOrder(found, offered, results) {
15426
+ const inFound = new Set(found.map((part) => part.key));
15427
+ const after = /* @__PURE__ */ new Map();
15428
+ offered.forEach((part, index) => {
15429
+ if (inFound.has(part.key)) return;
15430
+ const anchor = offered.slice(0, index).reverse().find((earlier) => inFound.has(earlier.key))?.key;
15431
+ after.set(anchor, [...after.get(anchor) ?? [], part.key]);
15432
+ });
15433
+ const keys = [...after.get(void 0) ?? []];
15434
+ for (const part of found) keys.push(part.key, ...after.get(part.key) ?? []);
15435
+ const rest = [...results.keys()].filter((key) => !keys.includes(key));
15436
+ return [...keys, ...rest];
15437
+ }
15438
+ function assemble(found, foundParts, offeredParts, merged, results) {
15439
+ const raw = new Map([...offeredParts, ...foundParts].map((part) => [part.key, part.raw]));
15440
+ return documentOrder(foundParts, offeredParts, results).filter((key) => results.get(key).outcome !== "up-to-date" || foundParts.some((p) => p.key === key)).map((key) => merged.get(key) ?? (foundParts.some((p) => p.key === key) ? raw.get(key) : void 0)).filter((piece) => piece !== void 0 && piece !== "").join("\n\n") + (found.endsWith("\n") ? "\n" : "");
15441
+ }
15442
+ //#endregion
15443
+ //#region src/prose-update/prose-update.command.ts
15444
+ const text = (what) => ({
15445
+ description: `${what}, as text.`,
15446
+ type: t.string().optional()
15447
+ });
15448
+ const file = (what) => ({
15449
+ description: `${what}, read from this file.`,
15450
+ type: t.string().optional()
15451
+ });
15452
+ function usage(message) {
15453
+ process.stderr.write(`error: usage: ${message}\n`);
15454
+ throw CliError.reported(exitCodes.usage);
15455
+ }
15456
+ /** Exactly one of `--<name>` and `--<name>-from`. */
15457
+ function input(name, inline, from) {
15458
+ if (inline === void 0 === (from === void 0)) usage(`pass exactly one of --${name} and --${name}-from.`);
15459
+ return inline ?? readFileSync(resolve(from), "utf8");
15460
+ }
15461
+ function classifyCommand() {
15462
+ return command({
15463
+ name: "classify",
15464
+ description: "Compare found prose with an offered wording and the retired wordings: the outcome for each part and the merged text, with no judgment involved.",
15465
+ options: {
15466
+ offered: text("The wording offered now"),
15467
+ "offered-from": file("The wording offered now"),
15468
+ found: text("The text found in the file being updated"),
15469
+ "found-from": file("The text found in the file being updated"),
15470
+ history: text("A history text of retired wordings, newest first"),
15471
+ "history-from": file("A history text of retired wordings, newest first"),
15472
+ retired: {
15473
+ description: "A retired wording, as text. Repeat for several, newest first. Used when no history text is given.",
15474
+ type: t.array(t.string()).optional()
15475
+ },
15476
+ "retired-from": {
15477
+ description: "A file holding a retired wording. Repeat for several, newest first.",
15478
+ type: t.array(t.string()).optional()
15479
+ }
15480
+ },
15481
+ async run(args) {
15482
+ const offered = input("offered", args.offered, args["offered-from"]);
15483
+ const found = input("found", args.found, args["found-from"]);
15484
+ const retired = [...args.retired ?? [], ...(args["retired-from"] ?? []).map((path) => readFileSync(resolve(path), "utf8"))];
15485
+ const hasHistory = args.history !== void 0 || args["history-from"] !== void 0;
15486
+ if (hasHistory && retired.length) usage("pass a history text or retired wordings, not both.");
15487
+ const history = hasHistory ? input("history", args.history, args["history-from"]) : retired;
15488
+ if (typeof history === "string") {
15489
+ const { invalid } = parseHistory(history);
15490
+ for (const entry of invalid) process.stderr.write(`warning: history entry ${entry.position + 1} (line ${entry.line}) did not parse: ${entry.heading}\n`);
15491
+ }
15492
+ try {
15493
+ const out = classify({
15494
+ found,
15495
+ offered,
15496
+ history
15497
+ });
15498
+ return result({
15499
+ outcome: out.outcome,
15500
+ ...out.reason ? { reason: out.reason } : {},
15501
+ ...out.base ? { base: out.base } : {},
15502
+ parts: out.parts.map((part) => ({
15503
+ path: part.path.join(" > "),
15504
+ outcome: part.outcome,
15505
+ ...part.reason ? { reason: part.reason } : {},
15506
+ ...part.tied ? { tied: part.tied } : {},
15507
+ ...part.base ? { base: part.base } : {},
15508
+ ...part.shared === void 0 ? {} : {
15509
+ shared: part.shared,
15510
+ baseSentences: part.baseSentences
15511
+ },
15512
+ ...part.revision ? { revision: part.revision } : {},
15513
+ ...part.text === void 0 ? {} : { text: part.text },
15514
+ ...part.collisions ? { collisions: part.collisions.map((c) => ({
15515
+ base: c.base.map((s) => s.text),
15516
+ found: c.found.map((s) => s.text),
15517
+ offered: c.offered.map((s) => s.text)
15518
+ })) } : {}
15519
+ })),
15520
+ text: out.text
15521
+ });
15522
+ } catch (error) {
15523
+ if (!(error instanceof ProseUpdateError)) throw error;
15524
+ process.stderr.write(`error: ${error.code}: ${error.message}\n`);
15525
+ throw CliError.reported(exitCodes.error);
15526
+ }
15527
+ }
15528
+ });
15529
+ }
15530
+ function retireCommand() {
15531
+ return command({
15532
+ name: "retire",
15533
+ description: "Print the history text with the outgoing wording added as its newest entry. Writes no file: the caller saves the answer.",
15534
+ options: {
15535
+ history: text("The history text"),
15536
+ "history-from": file("The history text"),
15537
+ wording: text("The outgoing wording"),
15538
+ "wording-from": file("The outgoing wording"),
15539
+ date: {
15540
+ description: "The date of the entry, written as given.",
15541
+ type: t.string()
15542
+ },
15543
+ reason: {
15544
+ description: "Why the wording was replaced.",
15545
+ type: t.string()
15546
+ }
15547
+ },
15548
+ async run(args) {
15549
+ return document(retire(input("history", args.history, args["history-from"]), input("wording", args.wording, args["wording-from"]), args.date, args.reason));
15550
+ }
15551
+ });
15552
+ }
15553
+ /** `classify` and `retire`: the script behind the `prose-update` skill (R26). */
15554
+ function createProseUpdateCommand() {
15555
+ return command({
15556
+ name: "prose-update",
15557
+ description: "Bring prose a tool wrote earlier to its offered wording without losing the owner’s edits: classify found prose, and extend the history of retired wordings.",
15558
+ commands: [classifyCommand(), retireCommand()]
15559
+ });
15560
+ }
15561
+ //#endregion
14861
15562
  //#region src/cli.ts
14862
15563
  function readJson(url) {
14863
15564
  return JSON.parse(readFileSync(url, "utf8"));
@@ -14881,14 +15582,14 @@ function app({ name, version, root }, format) {
14881
15582
  format,
14882
15583
  name: "cyber-agentkit",
14883
15584
  version,
14884
- description: "Work with the AI agent harness running here: read its layered reference documents."
15585
+ description: "Work with the AI agent harness running here: read its layered reference documents, and update prose a tool wrote earlier."
14885
15586
  }).command(createReferenceCommand({ plugin: {
14886
15587
  name,
14887
15588
  root
14888
15589
  } })).command(createAssetCommand({ plugin: {
14889
15590
  name,
14890
15591
  root
14891
- } }));
15592
+ } })).command(createProseUpdateCommand());
14892
15593
  }
14893
15594
  /**
14894
15595
  * `show`, `create`, `select`, and `delete` answer in `text` unless `--format` says otherwise; the other
@@ -14898,14 +15599,15 @@ function app({ name, version, root }, format) {
14898
15599
  function defaultFormat(argv) {
14899
15600
  const args = argv.slice(2);
14900
15601
  if (args.some((arg) => arg === "--format" || arg.startsWith("--format="))) return "toon";
14901
- const subcommand = args[0] === "reference" || args[0] === "asset" ? args[1] : void 0;
15602
+ const subcommand = args[0] === "reference" || args[0] === "asset" || args[0] === "prose-update" ? args[1] : void 0;
14902
15603
  return [
14903
15604
  "show",
14904
15605
  "load",
14905
15606
  "create",
14906
15607
  "update",
14907
15608
  "select",
14908
- "delete"
15609
+ "delete",
15610
+ "retire"
14909
15611
  ].includes(subcommand ?? "") ? "text" : "toon";
14910
15612
  }
14911
15613
  /** argv in, exit code out — returned rather than written, so a caller that is not the process can act on it. */