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/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
- return layer.tier === "default" ? [`${name}${defaultSuffix}.md`] : [`${name}.md`, `${name}/README.md`];
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
- if (entry.name.endsWith(`.default.md`)) name = entry.name.slice(0, -`${defaultSuffix}.md`.length);
10910
- } else if (entry.name.endsWith(".md") && !entry.isDirectory()) name = entry.name.slice(0, -3);
10911
- else if (!entry.isFile() && tryRead$1(join(layer.dir, entry.name, "README.md")) !== void 0) name = entry.name;
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, "references");
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(({ dir, skipped }) => skipped || isDirectory(dir) && canonical(dirname(dir)) !== ownRoot),
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 entry = resolved.trace.layers.find((layer) => layer.path === path);
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" ? [target.path, join(target.dir, name.name, "README.md")] : [target.path]).find((candidate) => candidate !== options.without && existsSync(candidate));
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: "show",
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: "list",
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: "where",
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. */