cyber-agentkit 1.0.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 +1389 -72
- package/dist/commands.d.ts +7 -1
- package/dist/commands.js +469 -48
- package/dist/index.d.ts +116 -12
- package/dist/index.js +608 -13
- package/dist/{reference-where-BhymfNgA.js → reference-where-C84J6MsR.js} +3839 -3649
- package/dist/{resolve-reference-Cis-Ye6T.d.ts → resolve-reference-D62VJZc0.d.ts} +17 -3
- package/package.json +1 -1
- package/plugin.json +4 -2
- package/skills/asset/README.md +14 -0
- package/skills/asset/SKILL.md +52 -0
- package/skills/asset/scripts/asset.mjs +220 -0
- 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/README.md +1 -1
- package/skills/reference/SKILL.md +3 -3
- package/skills/reference/scripts/reference.mjs +77 -56
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, rmdirSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { basename, dirname, join, posix, relative, resolve, sep, win32 } from "node:path";
|
|
2
|
+
import { copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, rmdirSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { basename, dirname, extname, join, posix, relative, resolve, sep, win32 } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { inspect, styleText } from "node:util";
|
|
6
6
|
import { homedir } from "node:os";
|
|
@@ -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",
|
|
@@ -10124,6 +10124,35 @@ function headingPaths(body) {
|
|
|
10124
10124
|
return paths;
|
|
10125
10125
|
}
|
|
10126
10126
|
//#endregion
|
|
10127
|
+
//#region src/references/reference-format.ts
|
|
10128
|
+
/** Extensions other than Markdown that answer a name, in the order they answer within one folder. */
|
|
10129
|
+
const dataExtensions = [
|
|
10130
|
+
{
|
|
10131
|
+
extension: ".json",
|
|
10132
|
+
format: "json"
|
|
10133
|
+
},
|
|
10134
|
+
{
|
|
10135
|
+
extension: ".yaml",
|
|
10136
|
+
format: "yaml"
|
|
10137
|
+
},
|
|
10138
|
+
{
|
|
10139
|
+
extension: ".yml",
|
|
10140
|
+
format: "yaml"
|
|
10141
|
+
},
|
|
10142
|
+
{
|
|
10143
|
+
extension: ".toml",
|
|
10144
|
+
format: "toml"
|
|
10145
|
+
},
|
|
10146
|
+
{
|
|
10147
|
+
extension: ".txt",
|
|
10148
|
+
format: "txt"
|
|
10149
|
+
}
|
|
10150
|
+
];
|
|
10151
|
+
/** The format a file name holds, by its extension; Markdown for `.md` and a folder's `README.md`. */
|
|
10152
|
+
function formatOfFile(file) {
|
|
10153
|
+
return dataExtensions.find(({ extension }) => file.endsWith(extension))?.format ?? "markdown";
|
|
10154
|
+
}
|
|
10155
|
+
//#endregion
|
|
10127
10156
|
//#region src/references/reference-interface.ts
|
|
10128
10157
|
const interfaceSuffix = ".interface";
|
|
10129
10158
|
const implementsPattern = /^([a-z0-9]+(?:[-.][a-z0-9]+)*)@(\d+)$/i;
|
|
@@ -10291,6 +10320,8 @@ function parseReferenceName(value) {
|
|
|
10291
10320
|
invalid: reason
|
|
10292
10321
|
});
|
|
10293
10322
|
if (value.endsWith(".md")) return invalid(`"${value}" names a file. Ask for the reference by name, without the .md extension.`);
|
|
10323
|
+
const extension = dataExtensions.find((item) => value.endsWith(item.extension));
|
|
10324
|
+
if (extension) return invalid(`"${value}" names a file. Ask for the reference by name, without the ${extension.extension} extension.`);
|
|
10294
10325
|
const parts = value.split("/");
|
|
10295
10326
|
if (parts.length > 2 || value.includes("\\")) return invalid(`"${value}" is a path. A name is letters, digits, hyphens, and dots, optionally after "<plugin>/".`);
|
|
10296
10327
|
const [first, second] = parts;
|
|
@@ -10324,7 +10355,12 @@ function tryRead$1(path) {
|
|
|
10324
10355
|
}
|
|
10325
10356
|
/** The files that answer a name in a layer, in the order they answer. */
|
|
10326
10357
|
function candidateFiles(layer, name) {
|
|
10327
|
-
|
|
10358
|
+
if (layer.whole) return [`${name}.md`, ...dataExtensions.map(({ extension }) => `${name}${extension}`)];
|
|
10359
|
+
return layer.tier === "default" ? [`${name}${defaultSuffix}.md`, ...dataExtensions.map(({ extension }) => `${name}${defaultSuffix}${extension}`)] : [
|
|
10360
|
+
`${name}.md`,
|
|
10361
|
+
`${name}/README.md`,
|
|
10362
|
+
...dataExtensions.map(({ extension }) => `${name}${extension}`)
|
|
10363
|
+
];
|
|
10328
10364
|
}
|
|
10329
10365
|
function contributionLabel(layer, path, display) {
|
|
10330
10366
|
if (layer.tier === "plugin") return `plugin ${layer.plugin}`;
|
|
@@ -10508,6 +10544,90 @@ function plan(ref, tiered, holders, lookup, defaultContribution, warn) {
|
|
|
10508
10544
|
}
|
|
10509
10545
|
return planFromPlugins(holders, lookup, defaultContribution, ref.name, warn);
|
|
10510
10546
|
}
|
|
10547
|
+
/**
|
|
10548
|
+
* The stack of an `assets/` name: no `extends` orders anything, so the highest tier holding the name
|
|
10549
|
+
* answers whole. Below the tiers, one plugin answers over the skill's own file; several plugins make
|
|
10550
|
+
* the name ambiguous unless a tier answers or a qualified name picks one.
|
|
10551
|
+
*/
|
|
10552
|
+
function planWhole(ref, layers, tiered, holders, lookup, defaultContribution, warn) {
|
|
10553
|
+
const entry = (target, contribution) => ({
|
|
10554
|
+
target,
|
|
10555
|
+
contribution,
|
|
10556
|
+
operation: "replace",
|
|
10557
|
+
from: "default"
|
|
10558
|
+
});
|
|
10559
|
+
const lowest = defaultContribution ? [entry("default", defaultContribution)] : [];
|
|
10560
|
+
if (ref.plugin !== void 0) {
|
|
10561
|
+
const found = lookup(ref.plugin);
|
|
10562
|
+
if ("reason" in found) return {
|
|
10563
|
+
status: "missing",
|
|
10564
|
+
message: `the plugin ${ref.plugin} ${found.reason}`
|
|
10565
|
+
};
|
|
10566
|
+
return {
|
|
10567
|
+
entries: [entry(ref.plugin, found)],
|
|
10568
|
+
builtBy: "qualified name",
|
|
10569
|
+
replaced: [],
|
|
10570
|
+
replacedBy: ""
|
|
10571
|
+
};
|
|
10572
|
+
}
|
|
10573
|
+
if (tiered.some(({ hasBody }) => hasBody)) return {
|
|
10574
|
+
entries: lowest,
|
|
10575
|
+
builtBy: "none",
|
|
10576
|
+
replaced: [],
|
|
10577
|
+
replacedBy: ""
|
|
10578
|
+
};
|
|
10579
|
+
const chosen = readAssetSelection(layers, ref.name, warn);
|
|
10580
|
+
if (chosen !== void 0) {
|
|
10581
|
+
if (chosen === "default" && defaultContribution) return {
|
|
10582
|
+
entries: lowest,
|
|
10583
|
+
builtBy: "selection",
|
|
10584
|
+
replaced: [],
|
|
10585
|
+
replacedBy: ""
|
|
10586
|
+
};
|
|
10587
|
+
const held = holders.get(chosen);
|
|
10588
|
+
if (held) return {
|
|
10589
|
+
entries: [...lowest, entry(chosen, held)],
|
|
10590
|
+
builtBy: "selection",
|
|
10591
|
+
replaced: [],
|
|
10592
|
+
replacedBy: ""
|
|
10593
|
+
};
|
|
10594
|
+
warn("selection-invalid", `the selection of "${ref.name}" names ${chosen}, which holds no such asset; it is ignored`);
|
|
10595
|
+
}
|
|
10596
|
+
const plugins = [...holders.entries()];
|
|
10597
|
+
if (plugins.length > 1) {
|
|
10598
|
+
const names = plugins.map(([plugin]) => plugin);
|
|
10599
|
+
return {
|
|
10600
|
+
status: "ambiguous",
|
|
10601
|
+
cause: "unordered",
|
|
10602
|
+
message: `the plugins ${names.join(", ")} hold it; run "asset select ${ref.name} <option>" (options: ${names.join(", ")}${defaultContribution ? ", default" : ""}), ask for ${names[0]}/${ref.name}, or put the file in a project or user assets folder`
|
|
10603
|
+
};
|
|
10604
|
+
}
|
|
10605
|
+
const [only] = plugins;
|
|
10606
|
+
return {
|
|
10607
|
+
entries: [...lowest, ...only ? [entry(only[0], only[1])] : []],
|
|
10608
|
+
builtBy: "none",
|
|
10609
|
+
replaced: [],
|
|
10610
|
+
replacedBy: ""
|
|
10611
|
+
};
|
|
10612
|
+
}
|
|
10613
|
+
/** The plugin or `default` a managed, project, or user `<name>.select` file names; the highest one wins. */
|
|
10614
|
+
function readAssetSelection(layers, name, warn) {
|
|
10615
|
+
for (const layer of layers) {
|
|
10616
|
+
if (layer.skipped || ![
|
|
10617
|
+
"managed",
|
|
10618
|
+
"project",
|
|
10619
|
+
"user"
|
|
10620
|
+
].includes(layer.tier)) continue;
|
|
10621
|
+
const raw = tryRead$1(join(layer.dir, `${name}.select`));
|
|
10622
|
+
if (raw === void 0) continue;
|
|
10623
|
+
const value = raw.trim();
|
|
10624
|
+
if (!namePattern.test(value)) {
|
|
10625
|
+
warn("selection-invalid", `${layer.tier} ${name}.select holds "${value}", which is not a plugin name or default; it is ignored`);
|
|
10626
|
+
continue;
|
|
10627
|
+
}
|
|
10628
|
+
return value;
|
|
10629
|
+
}
|
|
10630
|
+
}
|
|
10511
10631
|
function planFromPlugins(holders, lookup, defaultContribution, name, warn) {
|
|
10512
10632
|
const defaultEntry = () => defaultContribution ? [{
|
|
10513
10633
|
target: "default",
|
|
@@ -10651,7 +10771,7 @@ function resolveReference(input, source, { display = (path) => path, without, be
|
|
|
10651
10771
|
};
|
|
10652
10772
|
const options = () => [...[...holders.keys()].map((plugin) => `${plugin}/${ref.name}`), ...defaultContribution ? ["default"] : []];
|
|
10653
10773
|
if (interfaceIdOf(ref.name) !== void 0) return resolveInterface(ref, contributions, holders, defaultContribution, result, warn, options);
|
|
10654
|
-
const decided = plan(ref, tiered, holders, lookup, defaultContribution, warn);
|
|
10774
|
+
const decided = layers.some(({ whole }) => whole) ? planWhole(ref, layers, tiered, holders, lookup, defaultContribution, warn) : plan(ref, tiered, holders, lookup, defaultContribution, warn);
|
|
10655
10775
|
if ("status" in decided) {
|
|
10656
10776
|
if (decided.status === "ambiguous") {
|
|
10657
10777
|
for (const { entry } of contributions) if (entry.outcome === "") entry.outcome = "ambiguous";
|
|
@@ -10694,8 +10814,32 @@ function resolveReference(input, source, { display = (path) => path, without, be
|
|
|
10694
10814
|
let applied = [];
|
|
10695
10815
|
let removedBy;
|
|
10696
10816
|
const used = [];
|
|
10817
|
+
let whole;
|
|
10697
10818
|
for (const { contribution, operation, from } of order) {
|
|
10698
10819
|
const { entry } = contribution;
|
|
10820
|
+
if (contribution.whole && contribution.hasBody) {
|
|
10821
|
+
entry.operation = "replace";
|
|
10822
|
+
entry.operationFrom = "default";
|
|
10823
|
+
for (const below of applied) below.entry.outcome = `shadowed by ${contribution.label} (replace)`;
|
|
10824
|
+
applied = [contribution];
|
|
10825
|
+
composed = {
|
|
10826
|
+
...emptyComposed(),
|
|
10827
|
+
removed: composed.removed
|
|
10828
|
+
};
|
|
10829
|
+
whole = contribution;
|
|
10830
|
+
removedBy = void 0;
|
|
10831
|
+
entry.outcome = "used";
|
|
10832
|
+
used.push(contribution);
|
|
10833
|
+
continue;
|
|
10834
|
+
}
|
|
10835
|
+
if (whole && operation !== "remove" && contribution.hasBody) {
|
|
10836
|
+
entry.operation = operation;
|
|
10837
|
+
entry.operationFrom = from;
|
|
10838
|
+
entry.outcome = `not used (format differs from ${whole.format})`;
|
|
10839
|
+
warnAt(contribution.label)("format-mismatch", `it is ignored: ${whole.label} is ${whole.format}, which a Markdown contribution cannot compose with`);
|
|
10840
|
+
continue;
|
|
10841
|
+
}
|
|
10842
|
+
if (operation === "remove") whole = void 0;
|
|
10699
10843
|
entry.operation = operation;
|
|
10700
10844
|
entry.operationFrom = from;
|
|
10701
10845
|
const warnHere = warnAt(contribution.label);
|
|
@@ -10768,12 +10912,20 @@ function resolveReference(input, source, { display = (path) => path, without, be
|
|
|
10768
10912
|
path,
|
|
10769
10913
|
contributions: sources
|
|
10770
10914
|
})), ...trace.sections];
|
|
10915
|
+
if (whole) return {
|
|
10916
|
+
...result,
|
|
10917
|
+
status: "found",
|
|
10918
|
+
content: whole.body,
|
|
10919
|
+
format: whole.format,
|
|
10920
|
+
metadata: {}
|
|
10921
|
+
};
|
|
10771
10922
|
const single = applied.length === 1 && !composed.removed.length ? applied[0].body : void 0;
|
|
10772
10923
|
const content = single !== void 0 && stripMergeMarkers(single) === single ? withNewline(trimLeadingBlank(single)) : renderComposed(composed);
|
|
10773
10924
|
return {
|
|
10774
10925
|
...result,
|
|
10775
10926
|
status: "found",
|
|
10776
10927
|
content,
|
|
10928
|
+
format: "markdown",
|
|
10777
10929
|
metadata: composedMetadata(shown)
|
|
10778
10930
|
};
|
|
10779
10931
|
}
|
|
@@ -10802,6 +10954,29 @@ function readContribution(layer, name, entry, { display, without, warnAt }) {
|
|
|
10802
10954
|
for (const other of ignored) warnAt(display(other.path))("file-ignored", `it is ignored: ${hit.file} answers first in that folder`);
|
|
10803
10955
|
const label = contributionLabel(layer, hit.path, display);
|
|
10804
10956
|
const warn = warnAt(label);
|
|
10957
|
+
const format = formatOfFile(hit.file);
|
|
10958
|
+
if (format !== "markdown" || layer.whole) {
|
|
10959
|
+
Object.assign(entry, {
|
|
10960
|
+
path: hit.path,
|
|
10961
|
+
file: hit.file,
|
|
10962
|
+
outcome: ""
|
|
10963
|
+
});
|
|
10964
|
+
const held = hit.raw.trim().length > 0;
|
|
10965
|
+
if (!held) entry.empty = true;
|
|
10966
|
+
return {
|
|
10967
|
+
layer,
|
|
10968
|
+
entry,
|
|
10969
|
+
label,
|
|
10970
|
+
path: hit.path,
|
|
10971
|
+
metadata: {},
|
|
10972
|
+
body: hit.raw,
|
|
10973
|
+
hasBody: held,
|
|
10974
|
+
whole: true,
|
|
10975
|
+
format,
|
|
10976
|
+
merge: "replace",
|
|
10977
|
+
steps: void 0
|
|
10978
|
+
};
|
|
10979
|
+
}
|
|
10805
10980
|
const { metadata, body } = parseDocument(hit.raw, warn);
|
|
10806
10981
|
const merge = readMergeOperation(metadata, warn);
|
|
10807
10982
|
const steps = readExtends(metadata["extends"], warn);
|
|
@@ -10823,6 +10998,8 @@ function readContribution(layer, name, entry, { display, without, warnAt }) {
|
|
|
10823
10998
|
metadata,
|
|
10824
10999
|
body,
|
|
10825
11000
|
hasBody: bodyHeld,
|
|
11001
|
+
format,
|
|
11002
|
+
whole: false,
|
|
10826
11003
|
merge,
|
|
10827
11004
|
steps
|
|
10828
11005
|
};
|
|
@@ -10902,13 +11079,14 @@ function layerNames(layer) {
|
|
|
10902
11079
|
return [];
|
|
10903
11080
|
}
|
|
10904
11081
|
const names = /* @__PURE__ */ new Set();
|
|
10905
|
-
const isDefault = layer.tier === "default";
|
|
11082
|
+
const isDefault = layer.tier === "default" && !layer.whole;
|
|
10906
11083
|
for (const entry of entries) {
|
|
10907
11084
|
let name;
|
|
10908
11085
|
if (isDefault) {
|
|
10909
|
-
|
|
10910
|
-
|
|
10911
|
-
else if (!entry.
|
|
11086
|
+
const suffix = [`${defaultSuffix}.md`, ...dataExtensions.map((item) => `${defaultSuffix}${item.extension}`)].find((item) => entry.name.endsWith(item));
|
|
11087
|
+
if (suffix) name = entry.name.slice(0, -suffix.length);
|
|
11088
|
+
} else if (!entry.isDirectory() && [".md", ...dataExtensions.map((item) => item.extension)].some((extension) => entry.name.endsWith(extension))) name = entry.name.slice(0, entry.name.lastIndexOf("."));
|
|
11089
|
+
else if (!layer.whole && !entry.isFile() && tryRead$1(join(layer.dir, entry.name, "README.md")) !== void 0) name = entry.name;
|
|
10912
11090
|
if (name === void 0 || parseReferenceName(name).invalid) continue;
|
|
10913
11091
|
names.add(name);
|
|
10914
11092
|
}
|
|
@@ -12681,14 +12859,14 @@ function canonical(path) {
|
|
|
12681
12859
|
* instruction text an agent follows. Only a package with a plugin manifest is a plugin, named by
|
|
12682
12860
|
* that manifest; one that ships `references/` without a manifest is shown, not read.
|
|
12683
12861
|
*/
|
|
12684
|
-
function dependencyLayers(root, skip) {
|
|
12862
|
+
function dependencyLayers(root, skip, folder) {
|
|
12685
12863
|
const manifestPath = nearestManifest(root);
|
|
12686
12864
|
if (!manifestPath) return [];
|
|
12687
12865
|
const layers = [];
|
|
12688
12866
|
for (const pkg of declaredDependencies(readJson$1(manifestPath))) {
|
|
12689
12867
|
const dir = packageDir(pkg, manifestPath);
|
|
12690
12868
|
if (!dir || skip.has(canonical(dir))) continue;
|
|
12691
|
-
const references = join(dir,
|
|
12869
|
+
const references = join(dir, folder);
|
|
12692
12870
|
if (!isDirectory(references)) continue;
|
|
12693
12871
|
const plugin = pluginManifestName(dir);
|
|
12694
12872
|
layers.push(plugin ? {
|
|
@@ -12747,6 +12925,16 @@ function skillDefaults(harnesses, options, pluginRoots) {
|
|
|
12747
12925
|
unverified: `the skill folders ${unverified.join(", ")} ${unverified.length === 1 ? "reads are" : "read are"} not verified, so ${unverified.length === 1 ? "its" : "their"} skills' defaults are not listed`
|
|
12748
12926
|
} : { skills };
|
|
12749
12927
|
}
|
|
12928
|
+
/** The layer with its `references` folder swapped for `folder`; a layer that names no folder is left as it is. */
|
|
12929
|
+
function inFolder(layer, folder) {
|
|
12930
|
+
if (folder === "references" || basename(layer.dir) !== "references") return layer;
|
|
12931
|
+
const dir = join(dirname(layer.dir), folder);
|
|
12932
|
+
return {
|
|
12933
|
+
...layer,
|
|
12934
|
+
dir,
|
|
12935
|
+
whole: true
|
|
12936
|
+
};
|
|
12937
|
+
}
|
|
12750
12938
|
/** The `default` tier for the skill at `folder`; a folder that does not exist warns and adds no contribution. */
|
|
12751
12939
|
function defaultLayer(folder) {
|
|
12752
12940
|
const dir = join(folder, "references");
|
|
@@ -12766,7 +12954,7 @@ function defaultLayer(folder) {
|
|
|
12766
12954
|
skill: skillName(folder) ?? basename(folder)
|
|
12767
12955
|
};
|
|
12768
12956
|
}
|
|
12769
|
-
async function referenceLayers({ root, home, platform, plugin, defaultSkill, env = process.env }) {
|
|
12957
|
+
async function referenceLayers({ root, home, platform, plugin, defaultSkill, folder = "references", env = process.env }) {
|
|
12770
12958
|
const harnesses = detectedHarnesses(env);
|
|
12771
12959
|
const chain = projectChain(root);
|
|
12772
12960
|
const ownRoot = plugin && canonical(plugin.root);
|
|
@@ -12781,7 +12969,7 @@ async function referenceLayers({ root, home, platform, plugin, defaultSkill, env
|
|
|
12781
12969
|
...enabled.flatMap(({ dir, skipped }) => skipped ? [] : [dirname(dir)]),
|
|
12782
12970
|
...dependencyPluginRoots(root)
|
|
12783
12971
|
];
|
|
12784
|
-
const skills = skillDefaults(harnesses, {
|
|
12972
|
+
const skills = folder === "assets" ? void 0 : skillDefaults(harnesses, {
|
|
12785
12973
|
chain,
|
|
12786
12974
|
home,
|
|
12787
12975
|
platform,
|
|
@@ -12802,13 +12990,13 @@ async function referenceLayers({ root, home, platform, plugin, defaultSkill, env
|
|
|
12802
12990
|
dir: join(plugin.root, "references"),
|
|
12803
12991
|
plugin: plugin.name
|
|
12804
12992
|
}] : [],
|
|
12805
|
-
...enabled.filter((
|
|
12806
|
-
...dependencyLayers(root, new Set(ownRoot ? [ownRoot] : [])),
|
|
12993
|
+
...enabled.filter((layer) => layer.skipped || isDirectory(inFolder(layer, folder).dir) && canonical(dirname(layer.dir)) !== ownRoot),
|
|
12994
|
+
...dependencyLayers(root, new Set(ownRoot ? [ownRoot] : []), folder),
|
|
12807
12995
|
...defaultSkill === void 0 ? [] : [defaultLayer(resolve(defaultSkill))]
|
|
12808
12996
|
];
|
|
12809
12997
|
const seen = /* @__PURE__ */ new Set();
|
|
12810
12998
|
return {
|
|
12811
|
-
layers: layers.filter(({ dir }) => !seen.has(dir) && seen.add(dir)),
|
|
12999
|
+
layers: layers.map((layer) => inFolder(layer, folder)).filter(({ dir }) => !seen.has(dir) && seen.add(dir)),
|
|
12812
13000
|
harnesses,
|
|
12813
13001
|
skills
|
|
12814
13002
|
};
|
|
@@ -12897,6 +13085,7 @@ function listReferences(source, options = {}) {
|
|
|
12897
13085
|
description: resolved.status === "found" ? firstLine$1(resolved.metadata?.["description"]) : "",
|
|
12898
13086
|
status: resolved.status
|
|
12899
13087
|
};
|
|
13088
|
+
if (resolved.format && resolved.format !== "markdown") row.format = resolved.format;
|
|
12900
13089
|
if (resolved.options) {
|
|
12901
13090
|
const skills = held.map(({ skill }) => `default of ${skill.name}`);
|
|
12902
13091
|
row.options = [.../* @__PURE__ */ new Set([...resolved.options.filter((option) => option !== "default"), ...skills])];
|
|
@@ -13244,7 +13433,7 @@ const reaches = {
|
|
|
13244
13433
|
user: "only you, in every repository"
|
|
13245
13434
|
};
|
|
13246
13435
|
/** The project and user files a customization of `name` is written to, with the document below each. */
|
|
13247
|
-
function whereReference(input, source, { root, display = (path) => path }) {
|
|
13436
|
+
function whereReference(input, source, { root, display = (path) => path, assets }) {
|
|
13248
13437
|
const name = typeof input === "string" ? parseReferenceName(input) : input;
|
|
13249
13438
|
const resolved = resolveReference(name, source, { display });
|
|
13250
13439
|
const report = {
|
|
@@ -13257,7 +13446,7 @@ function whereReference(input, source, { root, display = (path) => path }) {
|
|
|
13257
13446
|
if (resolved.options) report.options = resolved.options;
|
|
13258
13447
|
if (name.invalid) return report;
|
|
13259
13448
|
const layers = Array.isArray(source) ? source : source.layers;
|
|
13260
|
-
const projectDir = projectReferencesDir(root);
|
|
13449
|
+
const projectDir = assets ? join(dirname(projectReferencesDir(root)), "assets") : projectReferencesDir(root);
|
|
13261
13450
|
const userDir = layers.find(({ tier }) => tier === "user")?.dir;
|
|
13262
13451
|
const targets = [{
|
|
13263
13452
|
scope: "project",
|
|
@@ -13268,17 +13457,18 @@ function whereReference(input, source, { root, display = (path) => path }) {
|
|
|
13268
13457
|
}] : []];
|
|
13269
13458
|
for (const { scope, dir } of targets) {
|
|
13270
13459
|
const path = join(dir, `${name.name}.md`);
|
|
13271
|
-
const
|
|
13460
|
+
const held = [path, ...dataExtensions.map(({ extension }) => join(dir, `${name.name}${extension}`))];
|
|
13461
|
+
const entry = resolved.trace.layers.find((layer) => held.includes(layer.path));
|
|
13272
13462
|
const below = resolveReference(name, source, {
|
|
13273
13463
|
display,
|
|
13274
13464
|
below: dir
|
|
13275
13465
|
});
|
|
13276
13466
|
report.places.push({
|
|
13277
13467
|
scope,
|
|
13278
|
-
path: scope === "project" ? relative(root, path) : display(path),
|
|
13468
|
+
path: assets ? `${scope === "project" ? relative(root, dir) : display(dir)}/${name.name}.<ext>` : scope === "project" ? relative(root, path) : display(path),
|
|
13279
13469
|
reaches: reaches[scope],
|
|
13280
13470
|
status: entry?.file ? entry.outcome : "empty",
|
|
13281
|
-
sections: below.status === "found" ? headingPaths(below.content ?? "") : []
|
|
13471
|
+
sections: !assets && below.status === "found" && below.format === "markdown" ? headingPaths(below.content ?? "") : []
|
|
13282
13472
|
});
|
|
13283
13473
|
}
|
|
13284
13474
|
return report;
|
|
@@ -13326,7 +13516,7 @@ function parseTemplate(content, label) {
|
|
|
13326
13516
|
* A title: the top level is a single heading holding every other one. `patch` matches by heading
|
|
13327
13517
|
* path, so it would add the whole document under the title instead of patching its sections.
|
|
13328
13518
|
*/
|
|
13329
|
-
function titleOf({ body }) {
|
|
13519
|
+
function titleOf$1({ body }) {
|
|
13330
13520
|
const tree = parseSections(body, () => {});
|
|
13331
13521
|
const [only] = tree.children;
|
|
13332
13522
|
return tree.children.length === 1 && only?.children.length ? only.heading.trim() : void 0;
|
|
@@ -13491,7 +13681,7 @@ function createContribution(layers, args, options = {}) {
|
|
|
13491
13681
|
metadata: {},
|
|
13492
13682
|
body: ""
|
|
13493
13683
|
} : readTemplate(args.template === void 0 ? void 0 : resolve(args.template));
|
|
13494
|
-
const title = titleOf(template);
|
|
13684
|
+
const title = titleOf$1(template);
|
|
13495
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.`);
|
|
13496
13686
|
const before = resolveReference(name, layers, options);
|
|
13497
13687
|
const hider = hiddenBy(before, target.dir, target.path);
|
|
@@ -13556,12 +13746,20 @@ function selectContribution(layers, args, options = {}) {
|
|
|
13556
13746
|
after: resolveReference(name, layers, options)
|
|
13557
13747
|
};
|
|
13558
13748
|
}
|
|
13749
|
+
/** The data files that can answer a name in a folder. */
|
|
13750
|
+
function dataFiles(dir, name) {
|
|
13751
|
+
return dataExtensions.map(({ extension }) => join(dir, `${name}${extension}`));
|
|
13752
|
+
}
|
|
13559
13753
|
function deleteContribution(layers, args, options = {}) {
|
|
13560
13754
|
const name = bareName(args.name);
|
|
13561
13755
|
const target = writeTarget(name, args);
|
|
13562
13756
|
const shown = options.display ?? ((path) => path);
|
|
13563
13757
|
const before = resolveReference(name, layers, options);
|
|
13564
|
-
const path = (target.scope === "project" || target.scope === "user" ? [
|
|
13758
|
+
const path = (target.scope === "project" || target.scope === "user" ? [
|
|
13759
|
+
target.path,
|
|
13760
|
+
join(target.dir, name.name, "README.md"),
|
|
13761
|
+
...dataFiles(target.dir, name.name)
|
|
13762
|
+
] : [target.path]).find((candidate) => candidate !== options.without && existsSync(candidate));
|
|
13565
13763
|
if (!path) throw new ReferenceWriteError("no-file", `${shown(target.path)} does not exist, so there is no ${target.scope} contribution of "${name.name}" to delete. ${describeAnswer(before, options.display)} An installed plugin's, a managed, or an installed skill's contribution is never deleted.`);
|
|
13566
13764
|
const steps = readExtends(parseDocument(tryRead(path) ?? "", () => {}).metadata["extends"], () => {});
|
|
13567
13765
|
const dryRun = Boolean(args.dryRun);
|
|
@@ -13586,7 +13784,7 @@ function deleteContribution(layers, args, options = {}) {
|
|
|
13586
13784
|
}
|
|
13587
13785
|
//#endregion
|
|
13588
13786
|
//#region src/references/reference.command.ts
|
|
13589
|
-
const rootOption = {
|
|
13787
|
+
const rootOption$1 = {
|
|
13590
13788
|
description: "Directory the project tier is read from, and each folder above it up to the repository root; a project write goes to its `.agents/references/`. Defaults to the current directory.",
|
|
13591
13789
|
type: t.string().optional()
|
|
13592
13790
|
};
|
|
@@ -13598,14 +13796,15 @@ const defaultOption = {
|
|
|
13598
13796
|
description: "The calling skill's folder: its `references/<name>.default.md` joins as the `default` tier.",
|
|
13599
13797
|
type: t.string().optional()
|
|
13600
13798
|
};
|
|
13601
|
-
function layerSource(plugin) {
|
|
13799
|
+
function layerSource(plugin, folder = "references") {
|
|
13602
13800
|
return (args, home) => referenceLayers({
|
|
13603
13801
|
root: args.root ?? process.cwd(),
|
|
13604
13802
|
home,
|
|
13605
13803
|
platform: process.platform,
|
|
13606
13804
|
env: process.env,
|
|
13607
13805
|
plugin,
|
|
13608
|
-
defaultSkill: args.default
|
|
13806
|
+
defaultSkill: args.default,
|
|
13807
|
+
folder
|
|
13609
13808
|
});
|
|
13610
13809
|
}
|
|
13611
13810
|
/** A field with nothing to say is left out: `undefined`, an empty string, or an empty list. */
|
|
@@ -13613,7 +13812,7 @@ function compact(value) {
|
|
|
13613
13812
|
return Object.fromEntries(Object.entries(value).filter(([, field]) => field !== void 0 && field !== "" && !(Array.isArray(field) && !field.length)));
|
|
13614
13813
|
}
|
|
13615
13814
|
/** stderr, not stdout: an agent parses stdout. Rethrows what the cli already reports. */
|
|
13616
|
-
function fail(error, fallback) {
|
|
13815
|
+
function fail$1(error, fallback) {
|
|
13617
13816
|
if (isCliError(error)) throw error;
|
|
13618
13817
|
if (error instanceof ReferenceWriteError) {
|
|
13619
13818
|
process.stderr.write(`error: ${error.code}: ${error.message}\n`);
|
|
@@ -13622,7 +13821,7 @@ function fail(error, fallback) {
|
|
|
13622
13821
|
process.stderr.write(`error: ${error instanceof Error ? error.message : fallback}\n`);
|
|
13623
13822
|
throw CliError.reported(exitCodes.error);
|
|
13624
13823
|
}
|
|
13625
|
-
function usage(message) {
|
|
13824
|
+
function usage$2(message) {
|
|
13626
13825
|
process.stderr.write(`error: usage: ${message}\n`);
|
|
13627
13826
|
throw CliError.reported(exitCodes.usage);
|
|
13628
13827
|
}
|
|
@@ -13678,11 +13877,12 @@ function errorText(entry) {
|
|
|
13678
13877
|
return `${entry.name}: ${entry.message}${options}`;
|
|
13679
13878
|
}
|
|
13680
13879
|
function showEntry(resolved, home, { metadata, trace }) {
|
|
13681
|
-
if (resolved.status === "found" && !resolved.warnings.length && !metadata && !trace) return resolved.content;
|
|
13880
|
+
if (resolved.status === "found" && !resolved.warnings.length && !metadata && !trace && resolved.format === "markdown") return resolved.content;
|
|
13682
13881
|
return compact({
|
|
13683
13882
|
name: resolved.name,
|
|
13684
13883
|
status: resolved.status,
|
|
13685
13884
|
content: resolved.content,
|
|
13885
|
+
format: resolved.format === "markdown" ? void 0 : resolved.format,
|
|
13686
13886
|
code: resolved.code,
|
|
13687
13887
|
options: resolved.options,
|
|
13688
13888
|
metadata: metadata ? resolved.metadata : void 0,
|
|
@@ -13702,9 +13902,9 @@ function showText(resolved) {
|
|
|
13702
13902
|
}
|
|
13703
13903
|
return document(resolved.map((entry) => entry.content === void 0 ? `<reference name="${entry.name}" status="${entry.status}" />` : `<reference name="${entry.name}">\n${entry.content}</reference>`).join("\n\n"));
|
|
13704
13904
|
}
|
|
13705
|
-
function showCommand(layersFor) {
|
|
13905
|
+
function showCommand$1(layersFor, name = "show") {
|
|
13706
13906
|
return command({
|
|
13707
|
-
name
|
|
13907
|
+
name,
|
|
13708
13908
|
description: "Print one or more references, each composed from its contributions as their extends and merge declare.",
|
|
13709
13909
|
arguments: [{
|
|
13710
13910
|
name: "names",
|
|
@@ -13712,7 +13912,7 @@ function showCommand(layersFor) {
|
|
|
13712
13912
|
type: t.array(t.string())
|
|
13713
13913
|
}],
|
|
13714
13914
|
options: {
|
|
13715
|
-
root: rootOption,
|
|
13915
|
+
root: rootOption$1,
|
|
13716
13916
|
default: defaultOption,
|
|
13717
13917
|
metadata: {
|
|
13718
13918
|
description: "Make every entry a record with its status and composed frontmatter.",
|
|
@@ -13722,7 +13922,7 @@ function showCommand(layersFor) {
|
|
|
13722
13922
|
},
|
|
13723
13923
|
async run(args) {
|
|
13724
13924
|
const format = this.format;
|
|
13725
|
-
if (!args.names.length) usage("name at least one reference.");
|
|
13925
|
+
if (!args.names.length) usage$2("name at least one reference.");
|
|
13726
13926
|
try {
|
|
13727
13927
|
const home = homedir();
|
|
13728
13928
|
const layers = await layersFor(args, home);
|
|
@@ -13757,14 +13957,14 @@ function showCommand(layersFor) {
|
|
|
13757
13957
|
const answer = result(resolved.map((entry) => showEntry(entry, home, flags)), { render: bareArray });
|
|
13758
13958
|
return ok ? answer : answerThenFail(answer, format);
|
|
13759
13959
|
} catch (error) {
|
|
13760
|
-
return fail(error, "Reference lookup failed.");
|
|
13960
|
+
return fail$1(error, "Reference lookup failed.");
|
|
13761
13961
|
}
|
|
13762
13962
|
}
|
|
13763
13963
|
});
|
|
13764
13964
|
}
|
|
13765
|
-
function listCommand(layersFor) {
|
|
13965
|
+
function listCommand$1(layersFor, name = "list") {
|
|
13766
13966
|
return command({
|
|
13767
|
-
name
|
|
13967
|
+
name,
|
|
13768
13968
|
description: "List every reference name, one row each, with its description, and its status when not found. A pattern keeps only the names it matches exactly.",
|
|
13769
13969
|
arguments: [{
|
|
13770
13970
|
name: "pattern",
|
|
@@ -13772,7 +13972,7 @@ function listCommand(layersFor) {
|
|
|
13772
13972
|
type: t.string().optional()
|
|
13773
13973
|
}],
|
|
13774
13974
|
options: {
|
|
13775
|
-
root: rootOption,
|
|
13975
|
+
root: rootOption$1,
|
|
13776
13976
|
trace: {
|
|
13777
13977
|
...traceOption,
|
|
13778
13978
|
description: "Add one row per name per layer holding it, with its outcome."
|
|
@@ -13790,6 +13990,7 @@ function listCommand(layersFor) {
|
|
|
13790
13990
|
references: listing.references.length ? listing.references.map((row) => compact({
|
|
13791
13991
|
name: row.name,
|
|
13792
13992
|
description: row.description,
|
|
13993
|
+
format: row.format,
|
|
13793
13994
|
status: row.status === "found" ? void 0 : row.status,
|
|
13794
13995
|
options: row.options
|
|
13795
13996
|
})) : args.pattern ? `0 references match "${args.pattern}"` : "0 references — no layer holds one",
|
|
@@ -13802,7 +14003,7 @@ function listCommand(layersFor) {
|
|
|
13802
14003
|
if (listing.warnings.length) report.warnings = listing.warnings;
|
|
13803
14004
|
return result(report);
|
|
13804
14005
|
} catch (error) {
|
|
13805
|
-
return fail(error, "Reference listing failed.");
|
|
14006
|
+
return fail$1(error, "Reference listing failed.");
|
|
13806
14007
|
}
|
|
13807
14008
|
}
|
|
13808
14009
|
});
|
|
@@ -13817,7 +14018,7 @@ function searchCommand(layersFor) {
|
|
|
13817
14018
|
type: t.string()
|
|
13818
14019
|
}],
|
|
13819
14020
|
options: {
|
|
13820
|
-
root: rootOption,
|
|
14021
|
+
root: rootOption$1,
|
|
13821
14022
|
trace: {
|
|
13822
14023
|
...traceOption,
|
|
13823
14024
|
description: "Add the tier and plugin or skill of each match."
|
|
@@ -13825,7 +14026,7 @@ function searchCommand(layersFor) {
|
|
|
13825
14026
|
},
|
|
13826
14027
|
async run(args) {
|
|
13827
14028
|
const query = args.query.trim();
|
|
13828
|
-
if (!query) usage("search needs a query.");
|
|
14029
|
+
if (!query) usage$2("search needs a query.");
|
|
13829
14030
|
try {
|
|
13830
14031
|
const home = homedir();
|
|
13831
14032
|
const { matches, defaults } = searchReferences(query, await layersFor(args, home), { display: (path) => collapseHome(home, path) });
|
|
@@ -13847,14 +14048,14 @@ function searchCommand(layersFor) {
|
|
|
13847
14048
|
defaults
|
|
13848
14049
|
}));
|
|
13849
14050
|
} catch (error) {
|
|
13850
|
-
return fail(error, "Reference search failed.");
|
|
14051
|
+
return fail$1(error, "Reference search failed.");
|
|
13851
14052
|
}
|
|
13852
14053
|
}
|
|
13853
14054
|
});
|
|
13854
14055
|
}
|
|
13855
|
-
function whereCommand(layersFor) {
|
|
14056
|
+
function whereCommand(layersFor, assets = false, name = "where") {
|
|
13856
14057
|
return command({
|
|
13857
|
-
name
|
|
14058
|
+
name,
|
|
13858
14059
|
description: "Give the project and user files a customization or a selection of a reference is written to, who each reaches, and the heading paths of the document below each.",
|
|
13859
14060
|
arguments: [{
|
|
13860
14061
|
name: "name",
|
|
@@ -13862,7 +14063,7 @@ function whereCommand(layersFor) {
|
|
|
13862
14063
|
type: t.string()
|
|
13863
14064
|
}],
|
|
13864
14065
|
options: {
|
|
13865
|
-
root: rootOption,
|
|
14066
|
+
root: rootOption$1,
|
|
13866
14067
|
default: defaultOption,
|
|
13867
14068
|
trace: {
|
|
13868
14069
|
...traceOption,
|
|
@@ -13876,7 +14077,8 @@ function whereCommand(layersFor) {
|
|
|
13876
14077
|
const display = (path) => collapseHome(home, path);
|
|
13877
14078
|
const report = whereReference(args.name, await layersFor(args, home), {
|
|
13878
14079
|
root: resolve(args.root ?? process.cwd()),
|
|
13879
|
-
display
|
|
14080
|
+
display,
|
|
14081
|
+
assets
|
|
13880
14082
|
});
|
|
13881
14083
|
const output = compact({
|
|
13882
14084
|
name: report.name,
|
|
@@ -13895,7 +14097,7 @@ function whereCommand(layersFor) {
|
|
|
13895
14097
|
process.stderr.write(`error: reference-invalid: ${parseReferenceName(args.name).invalid}\n`);
|
|
13896
14098
|
return answerThenFail(answer, format);
|
|
13897
14099
|
} catch (error) {
|
|
13898
|
-
return fail(error, "Reference placement lookup failed.");
|
|
14100
|
+
return fail$1(error, "Reference placement lookup failed.");
|
|
13899
14101
|
}
|
|
13900
14102
|
}
|
|
13901
14103
|
});
|
|
@@ -13903,7 +14105,7 @@ function whereCommand(layersFor) {
|
|
|
13903
14105
|
function parseAccept(value) {
|
|
13904
14106
|
if (value === void 0) return void 0;
|
|
13905
14107
|
const versions = value.split(",").map((part) => part.trim());
|
|
13906
|
-
if (!versions.length || versions.some((part) => !/^[1-9]\d*$/.test(part))) usage(`--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}".`);
|
|
13907
14109
|
return versions.map(Number);
|
|
13908
14110
|
}
|
|
13909
14111
|
function implementationsCommand(layersFor) {
|
|
@@ -13916,7 +14118,7 @@ function implementationsCommand(layersFor) {
|
|
|
13916
14118
|
type: t.string()
|
|
13917
14119
|
}],
|
|
13918
14120
|
options: {
|
|
13919
|
-
root: rootOption,
|
|
14121
|
+
root: rootOption$1,
|
|
13920
14122
|
accept: {
|
|
13921
14123
|
description: "Versions the consumer knows, comma-separated, such as 1,2. Defaults to the interface's `versions`.",
|
|
13922
14124
|
type: t.string().optional()
|
|
@@ -13956,7 +14158,7 @@ function implementationsCommand(layersFor) {
|
|
|
13956
14158
|
}));
|
|
13957
14159
|
return result(report);
|
|
13958
14160
|
} catch (error) {
|
|
13959
|
-
return fail(error, "Implementation lookup failed.");
|
|
14161
|
+
return fail$1(error, "Implementation lookup failed.");
|
|
13960
14162
|
}
|
|
13961
14163
|
}
|
|
13962
14164
|
});
|
|
@@ -13971,7 +14173,7 @@ function checkCommand(layersFor) {
|
|
|
13971
14173
|
type: t.array(t.string()).optional()
|
|
13972
14174
|
}],
|
|
13973
14175
|
options: {
|
|
13974
|
-
root: rootOption,
|
|
14176
|
+
root: rootOption$1,
|
|
13975
14177
|
trace: {
|
|
13976
14178
|
...traceOption,
|
|
13977
14179
|
description: "Add every entry checked, with its kind and unsupported sections."
|
|
@@ -14010,7 +14212,7 @@ function checkCommand(layersFor) {
|
|
|
14010
14212
|
if (format === "text") process.stderr.write(`error: interface-check: ${failing.length} of ${entries.length} references fail their check.\n`);
|
|
14011
14213
|
return answerThenFail(answer, format);
|
|
14012
14214
|
} catch (error) {
|
|
14013
|
-
return fail(error, "Reference check failed.");
|
|
14215
|
+
return fail$1(error, "Reference check failed.");
|
|
14014
14216
|
}
|
|
14015
14217
|
}
|
|
14016
14218
|
});
|
|
@@ -14023,7 +14225,7 @@ const pluginOption = {
|
|
|
14023
14225
|
description: "A plugin's source folder in this repository: acts on its `references/<name>.md`.",
|
|
14024
14226
|
type: t.string().optional()
|
|
14025
14227
|
};
|
|
14026
|
-
const skillOption = {
|
|
14228
|
+
const skillOption$1 = {
|
|
14027
14229
|
description: "A skill's source folder in this repository: acts on its `references/<name>.default.md`.",
|
|
14028
14230
|
type: t.string().optional()
|
|
14029
14231
|
};
|
|
@@ -14037,10 +14239,10 @@ function createCommand(layersFor) {
|
|
|
14037
14239
|
type: t.string()
|
|
14038
14240
|
}],
|
|
14039
14241
|
options: {
|
|
14040
|
-
root: rootOption,
|
|
14242
|
+
root: rootOption$1,
|
|
14041
14243
|
scope: scopeOption,
|
|
14042
14244
|
plugin: pluginOption,
|
|
14043
|
-
default: skillOption,
|
|
14245
|
+
default: skillOption$1,
|
|
14044
14246
|
merge: {
|
|
14045
14247
|
description: "How it applies over what is below: replace, patch (default), combine, or remove. remove writes no body.",
|
|
14046
14248
|
type: t.string().optional()
|
|
@@ -14095,12 +14297,12 @@ function createCommand(layersFor) {
|
|
|
14095
14297
|
if (args.trace && created.after) process.stderr.write(traceText(created.after, home));
|
|
14096
14298
|
return document(created.dryRun ? `${report.path}\n\n${created.content}` : `${report.path}\n\n${report.hint}\n`);
|
|
14097
14299
|
} catch (error) {
|
|
14098
|
-
return fail(error, "Reference creation failed.");
|
|
14300
|
+
return fail$1(error, "Reference creation failed.");
|
|
14099
14301
|
}
|
|
14100
14302
|
}
|
|
14101
14303
|
});
|
|
14102
14304
|
}
|
|
14103
|
-
function selectCommand(layersFor) {
|
|
14305
|
+
function selectCommand$1(layersFor) {
|
|
14104
14306
|
return command({
|
|
14105
14307
|
name: "select",
|
|
14106
14308
|
description: "Choose what a name builds on: write an `extends` listing the options, bottom first, in the project or user file. Its body and other keys are kept.",
|
|
@@ -14114,7 +14316,7 @@ function selectCommand(layersFor) {
|
|
|
14114
14316
|
type: t.array(t.string()).optional()
|
|
14115
14317
|
}],
|
|
14116
14318
|
options: {
|
|
14117
|
-
root: rootOption,
|
|
14319
|
+
root: rootOption$1,
|
|
14118
14320
|
scope: scopeOption,
|
|
14119
14321
|
clear: {
|
|
14120
14322
|
description: "Remove the selection; a file left with nothing in it is deleted.",
|
|
@@ -14148,7 +14350,7 @@ function selectCommand(layersFor) {
|
|
|
14148
14350
|
const what = args.clear ? selected.deleted ? "selection cleared; the file was left empty and deleted" : "selection cleared" : `extends: [${selected.extends.join(", ")}]`;
|
|
14149
14351
|
return document(`${report.path}\n\n${what}. ${report.answer}\n`);
|
|
14150
14352
|
} catch (error) {
|
|
14151
|
-
return fail(error, "Reference selection failed.");
|
|
14353
|
+
return fail$1(error, "Reference selection failed.");
|
|
14152
14354
|
}
|
|
14153
14355
|
}
|
|
14154
14356
|
});
|
|
@@ -14165,7 +14367,7 @@ function deleteReport(deleted, home) {
|
|
|
14165
14367
|
options: deleted.after.options
|
|
14166
14368
|
});
|
|
14167
14369
|
}
|
|
14168
|
-
function deleteCommand(layersFor) {
|
|
14370
|
+
function deleteCommand$1(layersFor) {
|
|
14169
14371
|
return command({
|
|
14170
14372
|
name: "delete",
|
|
14171
14373
|
description: "Delete a project or user contribution, or one in a plugin's or a skill's source folder, and report what answers the name afterwards. Never deletes an installed or managed one.",
|
|
@@ -14175,13 +14377,13 @@ function deleteCommand(layersFor) {
|
|
|
14175
14377
|
type: t.string()
|
|
14176
14378
|
}],
|
|
14177
14379
|
options: {
|
|
14178
|
-
root: rootOption,
|
|
14380
|
+
root: rootOption$1,
|
|
14179
14381
|
scope: {
|
|
14180
14382
|
...scopeOption,
|
|
14181
14383
|
description: "Tier to delete from: project (default) or user."
|
|
14182
14384
|
},
|
|
14183
14385
|
plugin: pluginOption,
|
|
14184
|
-
default: skillOption,
|
|
14386
|
+
default: skillOption$1,
|
|
14185
14387
|
"dry-run": {
|
|
14186
14388
|
description: "Print the file that would be deleted and what would answer the name then, and delete nothing.",
|
|
14187
14389
|
type: t.boolean().optional()
|
|
@@ -14209,7 +14411,7 @@ function deleteCommand(layersFor) {
|
|
|
14209
14411
|
const selection = report.selection ? ` Also, ${report.selection}.` : "";
|
|
14210
14412
|
return document(`${report.path}\n\n${deleted.dryRun ? "would delete" : "deleted"}. ${report.answer}${selection}\n`);
|
|
14211
14413
|
} catch (error) {
|
|
14212
|
-
return fail(error, "Reference deletion failed.");
|
|
14414
|
+
return fail$1(error, "Reference deletion failed.");
|
|
14213
14415
|
}
|
|
14214
14416
|
}
|
|
14215
14417
|
});
|
|
@@ -14218,13 +14420,13 @@ function deleteCommand(layersFor) {
|
|
|
14218
14420
|
function createReferenceCommands({ plugin } = {}) {
|
|
14219
14421
|
const layersFor = layerSource(plugin);
|
|
14220
14422
|
return {
|
|
14221
|
-
show: showCommand(layersFor),
|
|
14222
|
-
list: listCommand(layersFor),
|
|
14423
|
+
show: showCommand$1(layersFor),
|
|
14424
|
+
list: listCommand$1(layersFor),
|
|
14223
14425
|
search: searchCommand(layersFor),
|
|
14224
14426
|
where: whereCommand(layersFor),
|
|
14225
14427
|
create: createCommand(layersFor),
|
|
14226
|
-
select: selectCommand(layersFor),
|
|
14227
|
-
delete: deleteCommand(layersFor),
|
|
14428
|
+
select: selectCommand$1(layersFor),
|
|
14429
|
+
delete: deleteCommand$1(layersFor),
|
|
14228
14430
|
check: checkCommand(layersFor),
|
|
14229
14431
|
implementations: implementationsCommand(layersFor)
|
|
14230
14432
|
};
|
|
@@ -14247,6 +14449,1115 @@ function createReferenceCommand(options = {}) {
|
|
|
14247
14449
|
]
|
|
14248
14450
|
});
|
|
14249
14451
|
}
|
|
14452
|
+
/**
|
|
14453
|
+
* The tiered, by-name commands over `assets/` folders: the reference engine reading `assets/` where it
|
|
14454
|
+
* reads `references/`. `load` is `show`, `names` is `list`. `asset`'s own commands act on a path.
|
|
14455
|
+
*/
|
|
14456
|
+
function createAssetResolveCommands({ plugin } = {}) {
|
|
14457
|
+
const layersFor = layerSource(plugin, "assets");
|
|
14458
|
+
return {
|
|
14459
|
+
load: showCommand$1(layersFor, "load"),
|
|
14460
|
+
names: listCommand$1(layersFor, "names"),
|
|
14461
|
+
where: whereCommand(layersFor, true)
|
|
14462
|
+
};
|
|
14463
|
+
}
|
|
14464
|
+
//#endregion
|
|
14465
|
+
//#region src/assets/skill-assets.ts
|
|
14466
|
+
/** A refusal. `usage` marks a wrong invocation (exit 2) rather than a refusal (exit 1). */
|
|
14467
|
+
var AssetError = class extends Error {
|
|
14468
|
+
code;
|
|
14469
|
+
usage;
|
|
14470
|
+
constructor(code, message, usage = false) {
|
|
14471
|
+
super(message);
|
|
14472
|
+
this.code = code;
|
|
14473
|
+
this.usage = usage;
|
|
14474
|
+
}
|
|
14475
|
+
};
|
|
14476
|
+
/** The skill's `assets/` folder, for a skill folder that holds a `SKILL.md`. */
|
|
14477
|
+
function assetsDir(skill, root, write) {
|
|
14478
|
+
const folder = resolve(skill);
|
|
14479
|
+
if (skillName(folder) === void 0) throw new AssetError("folder-invalid", `${folder} holds no SKILL.md, so it is not a skill folder.`);
|
|
14480
|
+
if (write && !insideRepository(folder, root)) throw new AssetError("folder-invalid", `${folder} is outside the repository, such as an installed copy; write to the skill's source instead.`);
|
|
14481
|
+
return join(folder, "assets");
|
|
14482
|
+
}
|
|
14483
|
+
/** An asset path: relative, `/`-separated, with no `..` or empty part. */
|
|
14484
|
+
function assetFile(dir, path) {
|
|
14485
|
+
const parts = path.split("/");
|
|
14486
|
+
if (!path || path.includes("\\") || path.startsWith("/") || parts.some((part) => part === "" || part === "." || part === "..")) throw new AssetError("path-invalid", `"${path}" is not an asset path. Use a relative path inside assets/, with "/" separators and no "..".`, true);
|
|
14487
|
+
return join(dir, ...parts);
|
|
14488
|
+
}
|
|
14489
|
+
/** True when the real location of an existing file or its nearest existing parent stays inside the folder. */
|
|
14490
|
+
function contained(dir, file) {
|
|
14491
|
+
if (!existsSync(dir)) return true;
|
|
14492
|
+
const base = realpathSync(dir);
|
|
14493
|
+
let probe = file;
|
|
14494
|
+
while (!existsSync(probe)) probe = dirname(probe);
|
|
14495
|
+
const real = realpathSync(probe);
|
|
14496
|
+
return real === base || real.startsWith(base + sep);
|
|
14497
|
+
}
|
|
14498
|
+
function guard(dir, file, path) {
|
|
14499
|
+
if (!contained(dir, file)) throw new AssetError("path-invalid", `"${path}" leaves the assets/ folder through a link.`, true);
|
|
14500
|
+
}
|
|
14501
|
+
function listAssets(skill, root) {
|
|
14502
|
+
const dir = assetsDir(skill, root, false);
|
|
14503
|
+
const entries = [];
|
|
14504
|
+
const walk = (folder, prefix) => {
|
|
14505
|
+
let items;
|
|
14506
|
+
try {
|
|
14507
|
+
items = readdirSync(folder, { withFileTypes: true });
|
|
14508
|
+
} catch {
|
|
14509
|
+
return;
|
|
14510
|
+
}
|
|
14511
|
+
for (const item of items) {
|
|
14512
|
+
const full = join(folder, item.name);
|
|
14513
|
+
const stat = lstatSync(full);
|
|
14514
|
+
if (stat.isSymbolicLink()) continue;
|
|
14515
|
+
if (stat.isDirectory()) walk(full, `${prefix}${item.name}/`);
|
|
14516
|
+
else if (stat.isFile()) entries.push({
|
|
14517
|
+
path: `${prefix}${item.name}`,
|
|
14518
|
+
bytes: stat.size
|
|
14519
|
+
});
|
|
14520
|
+
}
|
|
14521
|
+
};
|
|
14522
|
+
walk(dir, "");
|
|
14523
|
+
return entries.sort((a, b) => a.path.localeCompare(b.path));
|
|
14524
|
+
}
|
|
14525
|
+
function showAsset(skill, root, path) {
|
|
14526
|
+
const dir = assetsDir(skill, root, false);
|
|
14527
|
+
const file = assetFile(dir, path);
|
|
14528
|
+
guard(dir, file, path);
|
|
14529
|
+
if (!existsSync(file) || !lstatSync(file).isFile()) throw new AssetError("no-file", `${path} does not exist in assets/.`);
|
|
14530
|
+
const bytes = readFileSync(file);
|
|
14531
|
+
if (bytes.includes(0)) throw new AssetError("binary", `${path} is binary (${bytes.length} bytes); point to it by path instead of showing it.`);
|
|
14532
|
+
return bytes.toString("utf8");
|
|
14533
|
+
}
|
|
14534
|
+
function writeAsset(mode, skill, root, path, content) {
|
|
14535
|
+
const dir = assetsDir(skill, root, true);
|
|
14536
|
+
const file = assetFile(dir, path);
|
|
14537
|
+
guard(dir, file, path);
|
|
14538
|
+
const exists = existsSync(file);
|
|
14539
|
+
if (mode === "create" && exists) throw new AssetError("exists", `${path} already exists in assets/; use update to replace it.`);
|
|
14540
|
+
if (mode === "update" && !exists) throw new AssetError("no-file", `${path} does not exist in assets/; use create to add it.`);
|
|
14541
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
14542
|
+
writeFileSync(file, content);
|
|
14543
|
+
return file;
|
|
14544
|
+
}
|
|
14545
|
+
function deleteAsset(skill, root, path) {
|
|
14546
|
+
const dir = assetsDir(skill, root, true);
|
|
14547
|
+
const file = assetFile(dir, path);
|
|
14548
|
+
guard(dir, file, path);
|
|
14549
|
+
if (!existsSync(file) || !lstatSync(file).isFile()) throw new AssetError("no-file", `${path} does not exist in assets/.`);
|
|
14550
|
+
rmSync(file);
|
|
14551
|
+
for (let folder = dirname(file); folder !== dir && folder.startsWith(dir + sep); folder = dirname(folder)) try {
|
|
14552
|
+
rmdirSync(folder);
|
|
14553
|
+
} catch {
|
|
14554
|
+
break;
|
|
14555
|
+
}
|
|
14556
|
+
return file;
|
|
14557
|
+
}
|
|
14558
|
+
//#endregion
|
|
14559
|
+
//#region src/assets/copy-asset.ts
|
|
14560
|
+
/**
|
|
14561
|
+
* Creates `<name>.<ext>` in the project or user `assets/` folder as a copy of the file another tier or
|
|
14562
|
+
* plugin holds, so it can be edited as an override. The managed tier is never a source: it is policy an
|
|
14563
|
+
* administrator keeps out of repositories and home folders, and a copy would show stale text as if it
|
|
14564
|
+
* were in effect (the managed file would still win).
|
|
14565
|
+
*/
|
|
14566
|
+
function copyAsset(layers, args) {
|
|
14567
|
+
const name = parseReferenceName(args.name);
|
|
14568
|
+
if (name.invalid || name.plugin !== void 0) throw new AssetError("name-invalid", name.invalid ?? "create takes a bare name.", true);
|
|
14569
|
+
if (args.scope !== "project" && args.scope !== "user") throw new AssetError("options-invalid", "--scope is project or user.", true);
|
|
14570
|
+
if (args.copy === "managed") throw new AssetError("managed-copy-refused", "the managed tier is policy and is never copied into a project or user folder. Ask the administrator, or read it with \"asset load\".");
|
|
14571
|
+
const found = resolveReference(name.name, layers).trace.layers.filter(({ file }) => file);
|
|
14572
|
+
const source = found.find((layer) => args.copy === "default" || args.copy === "project" || args.copy === "user" ? layer.tier === args.copy : layer.tier === "plugin" && layer.plugin === args.copy);
|
|
14573
|
+
if (!source) {
|
|
14574
|
+
const held = found.map((layer) => layer.tier === "plugin" ? layer.plugin : layer.tier);
|
|
14575
|
+
throw new AssetError("no-source", `"${args.copy}" holds no asset "${name.name}". Held by: ${held.filter((tier) => tier !== "managed").join(", ") || "nothing"}.`);
|
|
14576
|
+
}
|
|
14577
|
+
const extension = extname(source.path);
|
|
14578
|
+
if (source.file && ![...dataExtensions.map((item) => item.extension), ".md"].includes(extension)) throw new AssetError("no-source", `${basename(source.path)} is not a file an asset name resolves to.`);
|
|
14579
|
+
const base = args.scope === "user" ? userReferencesDir(args.home) : projectReferencesDir(resolve(args.root));
|
|
14580
|
+
const dir = join(dirname(base), "assets");
|
|
14581
|
+
const taken = [".md", ...dataExtensions.map((item) => item.extension)].map((item) => join(dir, `${name.name}${item}`)).find((path) => existsSync(path));
|
|
14582
|
+
if (taken) throw new AssetError("exists", `${taken} already exists; edit it, or delete it first.`);
|
|
14583
|
+
const path = join(dir, `${name.name}${extension}`);
|
|
14584
|
+
mkdirSync(dir, { recursive: true });
|
|
14585
|
+
copyFileSync(source.path, path);
|
|
14586
|
+
return {
|
|
14587
|
+
path,
|
|
14588
|
+
source: source.path,
|
|
14589
|
+
from: args.copy
|
|
14590
|
+
};
|
|
14591
|
+
}
|
|
14592
|
+
//#endregion
|
|
14593
|
+
//#region src/assets/select-asset.ts
|
|
14594
|
+
/** The options a name can be selected between: each plugin holding it, and `default` when the skill's file exists. */
|
|
14595
|
+
function options(layers, name) {
|
|
14596
|
+
const found = resolveReference(name, layers).trace.layers;
|
|
14597
|
+
return [...found.filter(({ tier, file }) => tier === "plugin" && file).map(({ plugin }) => plugin), ...found.some(({ tier, file }) => tier === "default" && file) ? ["default"] : []];
|
|
14598
|
+
}
|
|
14599
|
+
/**
|
|
14600
|
+
* Writes `<name>.select` in the project or user `assets/` folder: one line, the plugin or `default` that
|
|
14601
|
+
* answers the name. Nothing else is in the file, and nothing extends or merges.
|
|
14602
|
+
*/
|
|
14603
|
+
function selectAsset(layers, args) {
|
|
14604
|
+
const name = parseReferenceName(args.name);
|
|
14605
|
+
if (name.invalid || name.plugin !== void 0) throw new AssetError("name-invalid", name.invalid ?? "select takes a bare name.", true);
|
|
14606
|
+
if (args.scope !== void 0 && args.scope !== "project" && args.scope !== "user") throw new AssetError("options-invalid", "--scope is project or user.", true);
|
|
14607
|
+
const base = args.scope === "user" ? userReferencesDir(args.home) : projectReferencesDir(resolve(args.root));
|
|
14608
|
+
const path = join(dirname(base), "assets", `${name.name}.select`);
|
|
14609
|
+
if (args.clear) {
|
|
14610
|
+
if (!existsSync(path)) throw new AssetError("no-file", `${path} does not exist, so there is no selection to clear.`);
|
|
14611
|
+
rmSync(path);
|
|
14612
|
+
} else {
|
|
14613
|
+
const held = options(layers, name.name);
|
|
14614
|
+
if (!args.option || !held.includes(args.option)) throw new AssetError("option-invalid", `"${args.option ?? ""}" cannot be selected for "${name.name}". Options: ${held.join(", ") || "none, no plugin or default holds it"}.`, !args.option);
|
|
14615
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
14616
|
+
writeFileSync(path, `${args.option}\n`);
|
|
14617
|
+
}
|
|
14618
|
+
const result = {
|
|
14619
|
+
path,
|
|
14620
|
+
cleared: Boolean(args.clear),
|
|
14621
|
+
after: resolveReference(name.name, layers, { without: args.clear ? path : void 0 })
|
|
14622
|
+
};
|
|
14623
|
+
return args.clear ? result : {
|
|
14624
|
+
...result,
|
|
14625
|
+
selected: args.option
|
|
14626
|
+
};
|
|
14627
|
+
}
|
|
14628
|
+
//#endregion
|
|
14629
|
+
//#region src/assets/asset.command.ts
|
|
14630
|
+
const skillOption = {
|
|
14631
|
+
description: "A skill's source folder in this repository: its `assets/` folder is acted on.",
|
|
14632
|
+
type: t.string()
|
|
14633
|
+
};
|
|
14634
|
+
const rootOption = {
|
|
14635
|
+
description: "The directory the repository is found from. Defaults to the current directory.",
|
|
14636
|
+
type: t.string().optional()
|
|
14637
|
+
};
|
|
14638
|
+
function fail(error, fallback) {
|
|
14639
|
+
if (isCliError(error)) throw error;
|
|
14640
|
+
if (error instanceof AssetError) {
|
|
14641
|
+
process.stderr.write(`error: ${error.code}: ${error.message}\n`);
|
|
14642
|
+
throw CliError.reported(error.usage ? exitCodes.usage : exitCodes.error);
|
|
14643
|
+
}
|
|
14644
|
+
process.stderr.write(`error: ${error instanceof Error ? error.message : fallback}\n`);
|
|
14645
|
+
throw CliError.reported(exitCodes.error);
|
|
14646
|
+
}
|
|
14647
|
+
function usage$1(message) {
|
|
14648
|
+
process.stderr.write(`error: usage: ${message}\n`);
|
|
14649
|
+
throw CliError.reported(exitCodes.usage);
|
|
14650
|
+
}
|
|
14651
|
+
const pathDescription = "Asset path inside `assets/`, such as `data/rules.json`.";
|
|
14652
|
+
function rootOf(args) {
|
|
14653
|
+
return resolve(args.root ?? process.cwd());
|
|
14654
|
+
}
|
|
14655
|
+
function listCommand() {
|
|
14656
|
+
return command({
|
|
14657
|
+
name: "list",
|
|
14658
|
+
description: "List every file in a skill's assets/ folder with its size.",
|
|
14659
|
+
options: {
|
|
14660
|
+
skill: skillOption,
|
|
14661
|
+
root: rootOption
|
|
14662
|
+
},
|
|
14663
|
+
async run(args) {
|
|
14664
|
+
try {
|
|
14665
|
+
const entries = listAssets(args.skill, rootOf(args));
|
|
14666
|
+
return result({ assets: entries.length ? entries : "0 assets — the skill holds none" });
|
|
14667
|
+
} catch (error) {
|
|
14668
|
+
return fail(error, "Asset listing failed.");
|
|
14669
|
+
}
|
|
14670
|
+
}
|
|
14671
|
+
});
|
|
14672
|
+
}
|
|
14673
|
+
function showCommand() {
|
|
14674
|
+
return command({
|
|
14675
|
+
name: "show",
|
|
14676
|
+
description: "Print a text asset as written.",
|
|
14677
|
+
arguments: [{
|
|
14678
|
+
name: "path",
|
|
14679
|
+
description: pathDescription,
|
|
14680
|
+
type: t.string()
|
|
14681
|
+
}],
|
|
14682
|
+
options: {
|
|
14683
|
+
skill: skillOption,
|
|
14684
|
+
root: rootOption
|
|
14685
|
+
},
|
|
14686
|
+
async run(args) {
|
|
14687
|
+
try {
|
|
14688
|
+
return document(showAsset(args.skill, rootOf(args), args.path));
|
|
14689
|
+
} catch (error) {
|
|
14690
|
+
return fail(error, "Asset read failed.");
|
|
14691
|
+
}
|
|
14692
|
+
}
|
|
14693
|
+
});
|
|
14694
|
+
}
|
|
14695
|
+
function writeCommand(mode, host) {
|
|
14696
|
+
const create = mode === "create";
|
|
14697
|
+
return command({
|
|
14698
|
+
name: mode,
|
|
14699
|
+
description: create ? "Add a new asset: by path in a skill from --content or --from, or, with --copy, by name in the project or user assets folder as a copy of what a plugin, the default, or another tier holds (never the managed tier). Never overwrites a file." : "Replace an existing asset from --content or --from.",
|
|
14700
|
+
arguments: [{
|
|
14701
|
+
name: "path",
|
|
14702
|
+
description: `${pathDescription} With --copy, the asset name.`,
|
|
14703
|
+
type: t.string()
|
|
14704
|
+
}],
|
|
14705
|
+
options: {
|
|
14706
|
+
skill: {
|
|
14707
|
+
...skillOption,
|
|
14708
|
+
type: t.string().optional()
|
|
14709
|
+
},
|
|
14710
|
+
root: rootOption,
|
|
14711
|
+
content: {
|
|
14712
|
+
description: "The text to write.",
|
|
14713
|
+
type: t.string().optional()
|
|
14714
|
+
},
|
|
14715
|
+
from: {
|
|
14716
|
+
description: "A local file whose bytes to write.",
|
|
14717
|
+
type: t.string().optional()
|
|
14718
|
+
},
|
|
14719
|
+
...create ? {
|
|
14720
|
+
default: {
|
|
14721
|
+
description: "With --copy, the calling skill's folder: its `assets/<name>.<ext>` is the `default` source.",
|
|
14722
|
+
type: t.string().optional()
|
|
14723
|
+
},
|
|
14724
|
+
copy: {
|
|
14725
|
+
description: "Copy the asset of this source: a plugin's name, `default` (the skill's own file), `project`, or `user`. `managed` is refused.",
|
|
14726
|
+
type: t.string().optional()
|
|
14727
|
+
},
|
|
14728
|
+
scope: {
|
|
14729
|
+
description: "With --copy, where the copy goes: project (default), `.agents/assets/`, or user, `~/.agents/assets/`.",
|
|
14730
|
+
type: t.string().optional()
|
|
14731
|
+
}
|
|
14732
|
+
} : {}
|
|
14733
|
+
},
|
|
14734
|
+
async run(args) {
|
|
14735
|
+
try {
|
|
14736
|
+
if (args.copy !== void 0) {
|
|
14737
|
+
if (args.content !== void 0 || args.from !== void 0) usage$1("--copy takes neither --content nor --from.");
|
|
14738
|
+
const home = homedir();
|
|
14739
|
+
const root = rootOf(args);
|
|
14740
|
+
const done = copyAsset(await referenceLayers({
|
|
14741
|
+
root,
|
|
14742
|
+
home,
|
|
14743
|
+
platform: process.platform,
|
|
14744
|
+
env: process.env,
|
|
14745
|
+
plugin: host.plugin,
|
|
14746
|
+
defaultSkill: args.default ?? args.skill,
|
|
14747
|
+
folder: "assets"
|
|
14748
|
+
}), {
|
|
14749
|
+
root,
|
|
14750
|
+
home,
|
|
14751
|
+
name: args.path,
|
|
14752
|
+
scope: args.scope ?? "project",
|
|
14753
|
+
copy: args.copy
|
|
14754
|
+
});
|
|
14755
|
+
return document(`${done.path}\n\ncopied from ${done.from}. Edit it; it now answers over the plugin tier.\n`);
|
|
14756
|
+
}
|
|
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
|
+
const content = args.content ?? readFileSync(resolve(args.from), "utf8");
|
|
14760
|
+
return document(`${writeAsset(mode, args.skill, rootOf(args), args.path, content)}\n\n${create ? "created" : "updated"}.\n`);
|
|
14761
|
+
} catch (error) {
|
|
14762
|
+
return fail(error, `Asset ${mode} failed.`);
|
|
14763
|
+
}
|
|
14764
|
+
}
|
|
14765
|
+
});
|
|
14766
|
+
}
|
|
14767
|
+
function deleteCommand() {
|
|
14768
|
+
return command({
|
|
14769
|
+
name: "delete",
|
|
14770
|
+
description: "Delete an asset from a skill's source folder, and any folder it leaves empty.",
|
|
14771
|
+
arguments: [{
|
|
14772
|
+
name: "path",
|
|
14773
|
+
description: pathDescription,
|
|
14774
|
+
type: t.string()
|
|
14775
|
+
}],
|
|
14776
|
+
options: {
|
|
14777
|
+
skill: skillOption,
|
|
14778
|
+
root: rootOption
|
|
14779
|
+
},
|
|
14780
|
+
async run(args) {
|
|
14781
|
+
try {
|
|
14782
|
+
return document(`${deleteAsset(args.skill, rootOf(args), args.path)}\n\ndeleted.\n`);
|
|
14783
|
+
} catch (error) {
|
|
14784
|
+
return fail(error, "Asset deletion failed.");
|
|
14785
|
+
}
|
|
14786
|
+
}
|
|
14787
|
+
});
|
|
14788
|
+
}
|
|
14789
|
+
function selectCommand(options) {
|
|
14790
|
+
return command({
|
|
14791
|
+
name: "select",
|
|
14792
|
+
description: "Choose which plugin, or the skill itself, answers an asset name that several hold, by writing `<name>.select` in the project or user assets folder.",
|
|
14793
|
+
arguments: [{
|
|
14794
|
+
name: "name",
|
|
14795
|
+
description: "Asset name, without its extension.",
|
|
14796
|
+
type: t.string()
|
|
14797
|
+
}, {
|
|
14798
|
+
name: "option",
|
|
14799
|
+
description: "A plugin name, or `default` for the skill's own file. Not used with --clear.",
|
|
14800
|
+
type: t.string().optional()
|
|
14801
|
+
}],
|
|
14802
|
+
options: {
|
|
14803
|
+
root: rootOption,
|
|
14804
|
+
default: {
|
|
14805
|
+
description: "The calling skill's folder: its `assets/<name>.<ext>` is the `default` option.",
|
|
14806
|
+
type: t.string().optional()
|
|
14807
|
+
},
|
|
14808
|
+
scope: {
|
|
14809
|
+
description: "Where to write: project (default), `.agents/assets/`, or user, `~/.agents/assets/`.",
|
|
14810
|
+
type: t.string().optional()
|
|
14811
|
+
},
|
|
14812
|
+
clear: {
|
|
14813
|
+
description: "Remove the selection.",
|
|
14814
|
+
type: t.boolean().optional()
|
|
14815
|
+
}
|
|
14816
|
+
},
|
|
14817
|
+
async run(args) {
|
|
14818
|
+
try {
|
|
14819
|
+
const home = homedir();
|
|
14820
|
+
const root = resolve(args.root ?? process.cwd());
|
|
14821
|
+
const done = selectAsset(await referenceLayers({
|
|
14822
|
+
root,
|
|
14823
|
+
home,
|
|
14824
|
+
platform: process.platform,
|
|
14825
|
+
env: process.env,
|
|
14826
|
+
plugin: options.plugin,
|
|
14827
|
+
defaultSkill: args.default,
|
|
14828
|
+
folder: "assets"
|
|
14829
|
+
}), {
|
|
14830
|
+
...args,
|
|
14831
|
+
root,
|
|
14832
|
+
home
|
|
14833
|
+
});
|
|
14834
|
+
const answer = done.after.status === "found" ? `${args.name} now answers from ${done.after.trace.stack.join(", ") || "a tier"}.` : `${args.name} is ${done.after.status}: ${done.after.message ?? ""}`;
|
|
14835
|
+
return document(`${done.path}\n\n${done.cleared ? "selection cleared" : `selected ${done.selected}`}. ${answer}\n`);
|
|
14836
|
+
} catch (error) {
|
|
14837
|
+
return fail(error, "Asset selection failed.");
|
|
14838
|
+
}
|
|
14839
|
+
}
|
|
14840
|
+
});
|
|
14841
|
+
}
|
|
14842
|
+
function createAssetCommand(options = {}) {
|
|
14843
|
+
const { load, names, where } = createAssetResolveCommands(options);
|
|
14844
|
+
return command({
|
|
14845
|
+
name: "asset",
|
|
14846
|
+
description: "Manage a skill's assets/ files by path (list, show, create, update, delete), and load one by name through the tiers (load, names, where, select).",
|
|
14847
|
+
commands: [
|
|
14848
|
+
listCommand(),
|
|
14849
|
+
showCommand(),
|
|
14850
|
+
writeCommand("create", options),
|
|
14851
|
+
writeCommand("update", options),
|
|
14852
|
+
deleteCommand(),
|
|
14853
|
+
load,
|
|
14854
|
+
names,
|
|
14855
|
+
where,
|
|
14856
|
+
selectCommand(options)
|
|
14857
|
+
]
|
|
14858
|
+
});
|
|
14859
|
+
}
|
|
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
|
+
}
|
|
14250
15561
|
//#endregion
|
|
14251
15562
|
//#region src/cli.ts
|
|
14252
15563
|
function readJson(url) {
|
|
@@ -14271,11 +15582,14 @@ function app({ name, version, root }, format) {
|
|
|
14271
15582
|
format,
|
|
14272
15583
|
name: "cyber-agentkit",
|
|
14273
15584
|
version,
|
|
14274
|
-
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."
|
|
14275
15586
|
}).command(createReferenceCommand({ plugin: {
|
|
14276
15587
|
name,
|
|
14277
15588
|
root
|
|
14278
|
-
} }))
|
|
15589
|
+
} })).command(createAssetCommand({ plugin: {
|
|
15590
|
+
name,
|
|
15591
|
+
root
|
|
15592
|
+
} })).command(createProseUpdateCommand());
|
|
14279
15593
|
}
|
|
14280
15594
|
/**
|
|
14281
15595
|
* `show`, `create`, `select`, and `delete` answer in `text` unless `--format` says otherwise; the other
|
|
@@ -14285,12 +15599,15 @@ function app({ name, version, root }, format) {
|
|
|
14285
15599
|
function defaultFormat(argv) {
|
|
14286
15600
|
const args = argv.slice(2);
|
|
14287
15601
|
if (args.some((arg) => arg === "--format" || arg.startsWith("--format="))) return "toon";
|
|
14288
|
-
const subcommand = args[0] === "reference" ? args[1] : void 0;
|
|
15602
|
+
const subcommand = args[0] === "reference" || args[0] === "asset" || args[0] === "prose-update" ? args[1] : void 0;
|
|
14289
15603
|
return [
|
|
14290
15604
|
"show",
|
|
15605
|
+
"load",
|
|
14291
15606
|
"create",
|
|
15607
|
+
"update",
|
|
14292
15608
|
"select",
|
|
14293
|
-
"delete"
|
|
15609
|
+
"delete",
|
|
15610
|
+
"retire"
|
|
14294
15611
|
].includes(subcommand ?? "") ? "text" : "toon";
|
|
14295
15612
|
}
|
|
14296
15613
|
/** argv in, exit code out — returned rather than written, so a caller that is not the process can act on it. */
|