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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/dist/cli.js +718 -16
- package/dist/index.d.ts +92 -1
- package/dist/index.js +583 -1
- package/package.json +1 -1
- package/plugin.json +3 -2
- package/skills/asset/scripts/asset.mjs +76 -55
- package/skills/prose-update/README.md +24 -0
- package/skills/prose-update/SKILL.md +58 -0
- package/skills/prose-update/scripts/prose-update.mjs +220 -0
- package/skills/reference/SKILL.md +1 -1
- package/skills/reference/scripts/reference.mjs +76 -55
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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. */
|