@tryaura/aura-cli 0.2.1 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,6 @@
1
- import { McpWriteError, advanceMarkdownFence, defineOwnProperty, definePlugin, detectLineEnding as detectLineEnding$1, jsonPropertyPath, mcpServerNameProblem, normalizeMcpServerDefinition, parseMcpServerDefinition, pluginContentUrl, splitSourceLines as splitSourceLines$1 } from "@tryaura/aura-sdk";
1
+ import { advanceMarkdownFence, defineOwnProperty, definePlugin, jsonPropertyPath, mcpServerNameProblem, parseMcpServerDefinition, pluginContentUrl, splitSourceLines as splitSourceLines$1 } from "@tryaura/aura-sdk";
2
2
  import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
3
- import { Buffer, isUtf8 } from "node:buffer";
4
3
  import { createHash } from "node:crypto";
5
- import { createTwoFilesPatch } from "diff";
6
4
  import { gt, valid } from "semver";
7
5
  import { isDeepStrictEqual } from "node:util";
8
6
  //#region ../core/src/display-path.ts
@@ -157,6 +155,8 @@ var AuraManifestError = class extends Error {
157
155
  };
158
156
  //#endregion
159
157
  //#region ../core/src/manifest/protocol.ts
158
+ /** Home-relative protocol path shared by every Aura distribution. */
159
+ const AURA_MANIFEST_PATH = "~/agents/aura.json";
160
160
  /** Resolves the manifest protocol path against one captured home directory. */
161
161
  function resolveAuraManifestPath(homeDir) {
162
162
  return join(homeDir, "agents", "aura.json");
@@ -353,6 +353,48 @@ function targetApps(value, path, name, scope, claimed) {
353
353
  }
354
354
  return entries;
355
355
  }
356
+ //#endregion
357
+ //#region ../core/src/manifest/schema-snippets.ts
358
+ /**
359
+ * The same grammar `CONTENT_ID_PATTERN` admits for a preset's snippet ids.
360
+ *
361
+ * Deliberately not stricter: a preset may name any id this pattern accepts, and a manifest that
362
+ * refused one would fail on a selection the preset was allowed to make.
363
+ */
364
+ const SNIPPET_ID_PATTERN = /^[a-z0-9][a-z0-9._/-]*$/u;
365
+ /**
366
+ * Reads the install record for every snippet appended to the shared instructions.
367
+ *
368
+ * Two older shapes name an install that has to survive the read: the released record carried a
369
+ * `pinned` revision Aura no longer holds content at, and an early install-once build wrote a bare
370
+ * id. Both normalize to `{ id, hash? }` — the dead revision keys are dropped rather than carried
371
+ * forward, and a record with no hash stays installed rather than being discarded for the shape.
372
+ */
373
+ function snippets(value) {
374
+ if (!Array.isArray(value)) throw invalid("$.snippets", "must be an array");
375
+ const seen = /* @__PURE__ */ new Set();
376
+ const installed = [];
377
+ for (const [index, candidate] of value.entries()) {
378
+ const path = `$.snippets[${String(index)}]`;
379
+ const record = typeof candidate === "string" ? { id: candidate } : requiredObject(candidate, path);
380
+ const id = requiredString(record, "id", path);
381
+ if (!SNIPPET_ID_PATTERN.test(id)) throw invalid(`${path}.id`, "must be a lowercase snippet id such as \"acme/rules\"");
382
+ const hash = optionalSnippetHash(record, path);
383
+ if (seen.has(id)) continue;
384
+ seen.add(id);
385
+ installed.push(Object.freeze({
386
+ ...hash === void 0 ? {} : { hash },
387
+ id
388
+ }));
389
+ }
390
+ return Object.freeze(installed);
391
+ }
392
+ function optionalSnippetHash(record, path) {
393
+ const hash = record["hash"];
394
+ if (hash === void 0) return;
395
+ if (typeof hash !== "string" || !SHA256_PATTERN.test(hash)) throw invalid(`${path}.hash`, "must be a lowercase SHA-256 hash");
396
+ return hash;
397
+ }
356
398
  const MAX_TRUSTED_PATH_LENGTH = 1024;
357
399
  /**
358
400
  * Reads `trustedRepoPresets`, the repository presets the user accepted during setup.
@@ -403,7 +445,7 @@ function trustedPath(value, jsonPath) {
403
445
  const APP_ID_PATTERN = /^[a-z0-9][a-z0-9._-]*$/u;
404
446
  const MCP_CATALOG_ID_PATTERN = /^[a-z0-9][a-z0-9._-]*\/[a-zA-Z0-9][a-zA-Z0-9._-]*$/u;
405
447
  const SKILL_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
406
- const SKILL_SOURCE_PATTERN = /^(?:directory|driver|plugin):[^\s:]+$/u;
448
+ const SKILL_SOURCE_PATTERN = /^(?:directory|driver|plugin|repo):[^\s:]+$/u;
407
449
  const OVERRIDE_KEY_PATTERN = /^[a-z][a-zA-Z0-9]{0,63}$/u;
408
450
  /** Room for several future override kinds without leaving the forward-compat window unbounded. */
409
451
  const MAX_OVERRIDE_KEYS = 32;
@@ -495,9 +537,10 @@ function skills(value) {
495
537
  }
496
538
  function requiredSkillSource(value, path) {
497
539
  const source = requiredString(value, "source", path);
498
- if (!SKILL_SOURCE_PATTERN.test(source)) throw invalid(`${path}.source`, "must be a plugin:, directory:, or driver: source ID");
540
+ if (!SKILL_SOURCE_PATTERN.test(source)) throw invalid(`${path}.source`, "must be a plugin:, directory:, driver:, or repo: source ID");
499
541
  if (source.startsWith("plugin:")) return `plugin:${source.slice(7)}`;
500
542
  if (source.startsWith("directory:")) return `directory:${source.slice(10)}`;
543
+ if (source.startsWith("repo:")) return `repo:${source.slice(5)}`;
501
544
  return `driver:${source.slice(7)}`;
502
545
  }
503
546
  var UnsupportedAuraManifestVersionError = class extends Error {
@@ -521,22 +564,6 @@ function apps(value) {
521
564
  }
522
565
  return Object.freeze(result);
523
566
  }
524
- function snippets(value) {
525
- if (!Array.isArray(value)) throw invalid("$.snippets", "must be an array");
526
- return Object.freeze(value.map((candidate, index) => {
527
- const path = `$.snippets[${String(index)}]`;
528
- const snippet = requiredObject(candidate, path);
529
- const hash = requiredString(snippet, "hash", path);
530
- if (!SHA256_PATTERN.test(hash)) throw invalid(`${path}.hash`, "must be a lowercase SHA-256 hash");
531
- return Object.freeze({
532
- ...snippet,
533
- hash,
534
- id: requiredString(snippet, "id", path),
535
- pinned: requiredBoolean(snippet, "pinned", path),
536
- version: requiredString(snippet, "version", path)
537
- });
538
- }));
539
- }
540
567
  function ownership(value) {
541
568
  const source = requiredObject(value, "$.ownership");
542
569
  const result = {};
@@ -705,228 +732,49 @@ function createAuraManifestWriteOperation(state, manifest) {
705
732
  });
706
733
  }
707
734
  //#endregion
708
- //#region ../core/src/fix-plan/limits.ts
735
+ //#region ../core/src/content-hash.ts
736
+ /** Shape of every hash this module produces, for validators that must recognize one. */
737
+ const CONTENT_HASH_PATTERN = /^[0-9a-f]{64}$/u;
709
738
  /**
710
- * The largest file Aura will write over, remove, or replace with a link.
739
+ * Normalizes Markdown to LF and exactly one trailing newline.
711
740
  *
712
- * A mutation is only reversible if its previous contents were captured, so this doubles as the
713
- * ceiling on what one operation retains in memory. Agent configuration is measured in kilobytes;
714
- * anything approaching this size is not something a fix should be rewriting unattended.
741
+ * Line endings and trailing blank lines are what a checkout, an editor, or an append seam change
742
+ * without changing what the text says, so a hash that counted them would report drift on every
743
+ * machine that rewrote them and a fragment appended in this form is the same text the hash
744
+ * covers. Trailing newlines are trimmed by scanning rather than with `/\n+$/`, whose backtracking
745
+ * is quadratic when a long newline run does not reach the end of the string.
715
746
  */
716
- const MAX_MUTABLE_FILE_BYTES = 4194304;
717
- /**
718
- * The most file content Aura retains across a whole plan.
719
- *
720
- * {@link MAX_MUTABLE_FILE_BYTES} bounds one operation; without a plan-wide budget a long plan still
721
- * multiplies it by the operation count.
722
- */
723
- const MAX_RETAINED_PLAN_BYTES = 67108864;
724
- /**
725
- * The combined before/after size above which a preview summarizes instead of rendering a patch.
726
- *
727
- * A unified diff is larger than the inputs that produced it, and a preview no human can read is not
728
- * worth the memory it costs.
729
- */
730
- const MAX_DIFF_BYTES = 262144;
731
- /**
732
- * Every mode a plan may request.
733
- *
734
- * `FileMode` already closes this set, but a type is erased at runtime and plugins ship as compiled
735
- * JavaScript. Since the mode reaches `chmod` unchanged, checking it here is what actually keeps a
736
- * plan from asking for a world-writable or setuid file.
737
- */
738
- const FILE_MODES = /* @__PURE__ */ new Set([
739
- 384,
740
- 420,
741
- 448,
742
- 493
743
- ]);
744
- //#endregion
745
- //#region ../core/src/fix-plan/diff.ts
746
- const NULL_PATH = "/dev/null";
747
- function renderWriteDiff(path, before, content, requestedMode, mode) {
748
- const note = modeNote(before, requestedMode, mode);
749
- const previous = textOf(before);
750
- if (previous === void 0) return renderSummary("write", path, "Binary or oversized file would change.") + note;
751
- if (exceedsDiffBudget(previous, content)) return renderSummary("write", path, "File is too large to diff; contents would change.") + note;
752
- return renderPatch("write", path, before.kind === "missing" ? NULL_PATH : path, path, previous, content) + note;
753
- }
754
- /** Fail-closed write preview used when a semantic redactor cannot safely project both sides. */
755
- function renderWriteSummary(path, before, requestedMode, mode) {
756
- return renderSummary("write", path, "Sensitive MCP configuration would change.") + modeNote(before, requestedMode, mode);
757
- }
758
- function renderRemoveDiff(path, before) {
759
- if (before.kind === "directory") return renderSummary("remove", path, "Remove empty directory.");
760
- const previous = textOf(before);
761
- if (previous === void 0) return renderSummary("remove", path, "Binary or oversized file would be removed.");
762
- if (exceedsDiffBudget(previous, "")) return renderSummary("remove", path, "File is too large to diff; it would be removed.");
763
- return renderPatch("remove", path, path, NULL_PATH, previous, "");
764
- }
765
- function renderMoveDiff(sourcePath, destinationPath) {
766
- return [
767
- `diff --aura move ${sourcePath} ${destinationPath}`,
768
- `rename from ${sourcePath}`,
769
- `rename to ${destinationPath}`,
770
- ""
771
- ].join("\n");
772
- }
773
- function renderArchiveDiff(path, relativePath, before, replacement, replacementMode) {
774
- const archive = renderSummary("archive", path, `Preserve the original at <backup>/consolidation/${relativePath}.`);
775
- if (replacement === void 0) return `${archive}${renderRemoveDiff(path, before)}`;
776
- return replacement.type === "symlink" ? `${archive}${renderSymlinkDiff(path, before, replacement.target)}` : `${archive}${renderWriteDiff(path, before, replacement.content, replacement.mode, replacementMode ?? 420)}`;
777
- }
778
- function renderSymlinkDiff(path, before, target) {
779
- const previous = textOf(before);
780
- if (previous === void 0) return renderSummary("symlink", path, `Binary or oversized file would become a link.`);
781
- if (exceedsDiffBudget(previous, target)) return renderSummary("symlink", path, "File is too large to diff; it would become a link.");
782
- return `${renderPatch("symlink", path, before.kind === "missing" ? NULL_PATH : path, path, previous, `${target}\n`)}link target ${target}\n`;
783
- }
784
- /** Describes a conflict in the same shape as a diff, so a renderer can treat previews uniformly. */
785
- function renderConflict(operation, path, reason) {
786
- return renderSummary(operation, path, `Blocked: ${reason}.`);
787
- }
788
- function renderPatch(operation, displayPath, oldPath, newPath, oldContent, newContent) {
789
- return `diff --aura ${operation} ${displayPath}\n${createTwoFilesPatch(oldPath, newPath, oldContent, newContent, "before", "after", { context: 3 })}`;
790
- }
791
- function renderSummary(operation, path, detail) {
792
- return [
793
- `diff --aura ${operation} ${path}`,
794
- detail,
795
- ""
796
- ].join("\n");
797
- }
798
- /**
799
- * Says what happens to an existing file's permissions.
800
- *
801
- * Two cases are worth a line. A mode that changes is a change the diff itself cannot show, and core
802
- * only does that to files it owns as protocol. A `mode` a plan asked for and will not get is the
803
- * commoner one: an existing file keeps whatever the user set, and saying so is the difference
804
- * between a deliberate choice and a silent one.
805
- */
806
- function modeNote(before, requestedMode, mode) {
807
- if (before.kind !== "file") return "";
808
- if (before.mode !== mode) return `mode ${formatMode(before.mode)} changes to ${formatMode(mode)}\n`;
809
- if (requestedMode !== void 0 && requestedMode !== before.mode) return `mode ${formatMode(requestedMode)} requested; existing mode ${formatMode(before.mode)} is preserved\n`;
810
- return "";
811
- }
812
- function formatMode(mode) {
813
- return `0o${mode.toString(8).padStart(3, "0")}`;
814
- }
815
- function exceedsDiffBudget(before, after) {
816
- return Buffer.byteLength(before, "utf8") + Buffer.byteLength(after, "utf8") > MAX_DIFF_BYTES;
817
- }
818
- function textOf(state) {
819
- switch (state.kind) {
820
- case "directory":
821
- case "unsupported": return;
822
- case "file": return state.content !== void 0 && isUtf8(state.content) ? state.content.toString("utf8") : void 0;
823
- case "missing": return "";
824
- case "symlink": return `${state.target}\n`;
825
- }
826
- }
827
- //#endregion
828
- //#region ../core/src/fix-plan/write-redaction.ts
829
- const REDACTORS = /* @__PURE__ */ new WeakMap();
830
- /**
831
- * Paths some operation has asked to have redacted, kept beside the identity-keyed registry above.
832
- *
833
- * The registry is keyed on the operation object, so anything that copies an operation between
834
- * planning and preview silently drops its masker — and the symptom of that would be a credential
835
- * rendered into a diff rather than an error. Remembering the path as well turns the silent case
836
- * into the conservative one: a write to a path that has ever needed masking and arrives without a
837
- * masker gets a summary, not a patch.
838
- */
839
- const REDACTED_PATHS = /* @__PURE__ */ new Set();
840
- /** Associates a semantic content masker with a write without changing the public operation schema. */
841
- function rememberWriteRedactor(operation, redactor) {
842
- const existing = REDACTORS.get(operation) ?? [];
843
- REDACTORS.set(operation, [...existing, redactor]);
844
- REDACTED_PATHS.add(operation.path);
747
+ function canonicalizeContent(content) {
748
+ const normalized = content.replace(/\r\n?/gu, "\n");
749
+ let end = normalized.length;
750
+ while (end > 0 && normalized[end - 1] === "\n") end -= 1;
751
+ return `${normalized.slice(0, end)}\n`;
845
752
  }
846
753
  /**
847
- * Wraps an adapter's transform so a projection that could not account for every field fails closed.
754
+ * Fingerprints Markdown Aura records but does not own: an installed snippet, a trusted preset.
848
755
  *
849
- * `unresolved` names the fields whose server entry this content does not contain. That is not the
850
- * same as "nothing to mask": a rewritten side legitimately has nothing left to mask and reports no
851
- * unresolved fields, while a document shaped in a way the adapter cannot navigate reports them all
852
- * and still holds the credential.
756
+ * One function for both because the two records answer the same question "is this still the text
757
+ * the user accepted?" and two spellings of the canonical form would answer it differently on the
758
+ * first file whose line endings a checkout rewrote.
853
759
  */
854
- function mcpSecretRedactor(transform, sightings) {
855
- return (content) => {
856
- const redaction = transform.redact({
857
- content,
858
- sightings
859
- });
860
- return redaction === void 0 || redaction.unresolved.length > 0 ? void 0 : redaction.content;
861
- };
760
+ function hashContent(content) {
761
+ return createHash("sha256").update(canonicalizeContent(content), "utf8").digest("hex");
862
762
  }
863
- /** Renders a write only after every registered semantic masker succeeds on both diff sides. */
864
- function renderRedactedWriteDiff(operations, path, before, content, requestedMode, mode) {
865
- const redactors = operations.flatMap((operation) => REDACTORS.get(operation) ?? []);
866
- if (redactors.length === 0) return REDACTED_PATHS.has(path) ? renderWriteSummary(path, before, requestedMode, mode) : renderWriteDiff(path, before, content, requestedMode, mode);
867
- const next = redactAll(redactors, content);
868
- if (next === void 0) return renderWriteSummary(path, before, requestedMode, mode);
869
- if (before.kind !== "file" || before.content === void 0) return renderWriteDiff(path, before, next, requestedMode, mode);
870
- const previous = redactAll(redactors, before.content.toString("utf8"));
871
- if (previous === void 0) return renderWriteSummary(path, before, requestedMode, mode);
872
- return renderWriteDiff(path, {
873
- ...before,
874
- content: Buffer.from(previous, "utf8")
875
- }, next, requestedMode, mode);
876
- }
877
- function redactAll(redactors, content) {
878
- let projected = content;
879
- for (const redact of redactors) {
880
- const result = safeRedact(redact, projected);
881
- if (result === void 0) return;
882
- projected = result;
883
- }
884
- return projected;
885
- }
886
- function safeRedact(redact, content) {
887
- try {
888
- return redact(content);
889
- } catch {
890
- return;
891
- }
892
- }
893
- //#endregion
894
- //#region ../core/src/managed-block/protocol.ts
895
- /** Distribution-independent outer marker opening Aura-managed content. */
896
- const AURA_MANAGED_BLOCK_BEGIN = "<!-- aura:begin -->";
897
- /** Distribution-independent outer marker closing Aura-managed content. */
898
- const AURA_MANAGED_BLOCK_END = "<!-- aura:end -->";
899
- /** Prefix shared by every Aura snippet opening marker. */
900
- const AURA_MANAGED_SNIPPET_BEGIN_PREFIX = "<!-- aura:begin id=";
901
- /** Prefix shared by every Aura snippet closing marker. */
902
- const AURA_MANAGED_SNIPPET_END_PREFIX = "<!-- aura:end id=";
903
- /** Human-facing ownership warning rendered once inside the outer block. */
904
- const AURA_MANAGED_BLOCK_NOTICE = "Managed by Aura. Edit via the Aura CLI; manual edits to this block are overwritten.";
905
763
  const MANAGED_SNIPPET_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/;
906
- const MANAGED_SNIPPET_HASH_PATTERN = /^[0-9a-f]{64}$/;
764
+ const MANAGED_SNIPPET_HASH_PATTERN = CONTENT_HASH_PATTERN;
907
765
  /** Whether an id is both line-safe and legal inside an HTML comment. */
908
766
  function isManagedSnippetId(id) {
909
767
  return MANAGED_SNIPPET_ID_PATTERN.test(id) && !id.includes("--");
910
768
  }
911
769
  /**
912
- * Normalizes snippet text to the bytes covered by the managed-block hash protocol.
770
+ * Computes the protocol hash for a snippet's canonical UTF-8 contents.
913
771
  *
914
- * Trailing newlines are trimmed by scanning rather than with `/\n+$/`, whose backtracking is
915
- * quadratic when a long newline run does not reach the end of the string.
772
+ * The legacy block's hash is {@link hashContent} under another name: a marked section read back
773
+ * from an older file has to compare equal to the same text hashed anywhere else in Aura, or a
774
+ * migration would report drift on content nobody touched.
916
775
  */
917
- function canonicalizeManagedSnippet(content) {
918
- const normalized = content.replace(/\r\n?/g, "\n");
919
- let end = normalized.length;
920
- while (end > 0 && normalized[end - 1] === "\n") end -= 1;
921
- return `${normalized.slice(0, end)}\n`;
922
- }
923
- /** Computes the protocol hash for text already in {@link canonicalizeManagedSnippet} form. */
924
- function hashCanonicalManagedSnippet(canonical) {
925
- return createHash("sha256").update(canonical, "utf8").digest("hex");
926
- }
927
- /** Computes the protocol hash for a snippet's canonical UTF-8 contents. */
928
776
  function hashManagedSnippet(content) {
929
- return hashCanonicalManagedSnippet(canonicalizeManagedSnippet(content));
777
+ return hashContent(content);
930
778
  }
931
779
  //#endregion
932
780
  //#region ../core/src/managed-block/parse-state.ts
@@ -1148,324 +996,23 @@ function collectPlainLine(source, line, protectedByFence, block, snippet, notes)
1148
996
  }));
1149
997
  }
1150
998
  //#endregion
1151
- //#region ../core/src/managed-block/scan.ts
1152
- /**
1153
- * Finds protocol markers the reader would honour, applying the same fence rules as
1154
- * {@link readManagedBlock} so fenced marker examples stay ordinary text.
1155
- */
1156
- function scanForMarkers(text) {
1157
- let fence;
1158
- let markerLine;
1159
- for (const line of splitSourceLines$1(text)) {
1160
- const previousFence = fence;
1161
- fence = advanceMarkdownFence(line.text, fence);
1162
- if (previousFence !== void 0 || fence !== void 0) continue;
1163
- if (markerLine === void 0 && parseMarker(line.text) !== void 0) markerLine = line.number;
1164
- }
1165
- return {
1166
- markerLine,
1167
- unterminatedFence: fence !== void 0
1168
- };
1169
- }
1170
- /**
1171
- * Rejects content that would escape its own snippet. Written verbatim, a marker inside a snippet
1172
- * body reopens or closes the protocol, and the resulting file parses as invalid forever — so the
1173
- * damage has to be caught before the write, not after.
1174
- *
1175
- * Every write path shares this guard. A narrower path that skipped it would be a way to write the
1176
- * exact bytes the wider one refuses, and the file it corrupts is the same file either way.
1177
- */
1178
- function managedSnippetContentProblems(id, canonical) {
1179
- const scan = scanForMarkers(canonical);
1180
- if (scan.markerLine !== void 0) return Object.freeze([Object.freeze({
1181
- code: "invalid-snippet-content",
1182
- message: `Snippet "${id}" declares an Aura marker on content line ${String(scan.markerLine)}. Wrap marker examples in a Markdown fence.`
1183
- })]);
1184
- if (scan.unterminatedFence) return Object.freeze([Object.freeze({
1185
- code: "invalid-snippet-content",
1186
- message: `Snippet "${id}" ends inside an unclosed Markdown fence, which would hide every marker after it. Close the fence.`
1187
- })]);
1188
- return Object.freeze([]);
1189
- }
1190
- /**
1191
- * Drops every protocol line the reader honours, plus the notice that follows an opening marker,
1192
- * leaving handwritten text byte-for-byte. Fenced marker examples survive because the reader never
1193
- * treated them as protocol in the first place.
1194
- */
1195
- function stripManagedMarkers(source) {
1196
- const kept = [];
1197
- let fence;
1198
- let afterBlockBegin = false;
1199
- for (const line of splitSourceLines$1(source)) {
1200
- const previousFence = fence;
1201
- fence = advanceMarkdownFence(line.text, fence);
1202
- const marker = previousFence !== void 0 || fence !== void 0 ? void 0 : parseMarker(line.text);
1203
- if (marker !== void 0) {
1204
- afterBlockBegin = marker.kind === "block-begin";
1205
- continue;
1206
- }
1207
- if (afterBlockBegin && line.text === "Managed by Aura. Edit via the Aura CLI; manual edits to this block are overwritten.") {
1208
- afterBlockBegin = false;
1209
- continue;
1210
- }
1211
- afterBlockBegin = false;
1212
- kept.push(source.slice(line.start, line.end));
1213
- }
1214
- return kept.join("");
1215
- }
1216
- //#endregion
1217
- //#region ../core/src/managed-block/reconcile-snippet.ts
1218
- /** Reconciles exactly one snippet while preserving every other source byte. */
1219
- function reconcileManagedSnippet(source, snippetId, resolution) {
1220
- const current = readManagedBlock(source);
1221
- if (current.status === "invalid") return invalidResult$1(source, current.notes, current.problems);
1222
- if (current.status === "absent") return missingSnippet(source, current.notes, snippetId, "an Aura-managed block");
1223
- const snippet = current.block.snippets.find((candidate) => candidate.id === snippetId);
1224
- if (snippet === void 0) return missingSnippet(source, current.notes, snippetId, "the Aura-managed block");
1225
- const lineEnding = markerLineEnding(source, snippet.startOffset, snippet.contentStartOffset);
1226
- const canonical = resolution.kind === "restore" ? canonicalizeManagedSnippet(resolution.content) : void 0;
1227
- if (canonical !== void 0) {
1228
- const problems = managedSnippetContentProblems(snippet.id, canonical);
1229
- if (problems.length > 0) return invalidResult$1(source, current.notes, problems);
1230
- }
1231
- const content = canonical === void 0 ? snippet.content : withLineEnding(canonical, lineEnding);
1232
- const hash = canonical === void 0 ? snippet.computedHash : hashCanonicalManagedSnippet(canonical);
1233
- const opening = `${AURA_MANAGED_SNIPPET_BEGIN_PREFIX}${snippet.id} sha256=${hash} -->${lineEnding}`;
1234
- const updated = source.slice(0, snippet.startOffset) + opening + content + source.slice(snippet.contentEndOffset);
1235
- return updated === source ? Object.freeze({
1236
- content: source,
1237
- notes: current.notes,
1238
- status: "unchanged"
1239
- }) : Object.freeze({
1240
- content: updated,
1241
- notes: current.notes,
1242
- status: "updated"
1243
- });
1244
- }
1245
- /** Creates the opt-in edited-versus-canonical detail used by the merge choice. */
1246
- function diffManagedSnippet(path, editedContent, canonicalContent) {
1247
- return createTwoFilesPatch(`${path} (edited)`, `${path} (canonical)`, canonicalizeManagedSnippet(editedContent), canonicalizeManagedSnippet(canonicalContent), "edited", "canonical", { context: 3 });
1248
- }
1249
- function missingSnippet(source, notes, snippetId, location) {
1250
- return invalidResult$1(source, notes, [Object.freeze({
1251
- code: "missing-snippet",
1252
- message: `Snippet "${snippetId}" does not exist in ${location}.`
1253
- })]);
1254
- }
1255
- function invalidResult$1(source, notes, problems) {
1256
- return Object.freeze({
1257
- content: source,
1258
- notes,
1259
- problems,
1260
- status: "invalid"
1261
- });
1262
- }
1263
- function markerLineEnding(source, startOffset, contentStartOffset) {
1264
- return source.slice(startOffset, contentStartOffset).endsWith("\r\n") ? "\r\n" : "\n";
1265
- }
1266
- function withLineEnding(content, lineEnding) {
1267
- return lineEnding === "\n" ? content : content.replaceAll("\n", lineEnding);
1268
- }
1269
- //#endregion
1270
- //#region ../core/src/managed-block/reconcile-desired.ts
1271
- function prepareDesiredSnippets(snippets, options) {
1272
- const ids = /* @__PURE__ */ new Set();
1273
- const preserved = new Set(options.preserveSnippetIds ?? []);
1274
- const prepared = [];
1275
- const problems = [];
1276
- for (const snippet of snippets) {
1277
- if (!isManagedSnippetId(snippet.id)) problems.push(Object.freeze({
1278
- code: "invalid-snippet-id",
1279
- message: `Snippet ID "${snippet.id}" is not safe inside an HTML comment marker.`
1280
- }));
1281
- if (ids.has(snippet.id)) problems.push(Object.freeze({
1282
- code: "duplicate-snippet",
1283
- message: `Desired snippet ID "${snippet.id}" appears more than once.`
1284
- }));
1285
- if (preserved.has(snippet.id)) problems.push(Object.freeze({
1286
- code: "duplicate-snippet",
1287
- message: `Snippet ID "${snippet.id}" is both desired and preserved; writing both would duplicate it.`
1288
- }));
1289
- ids.add(snippet.id);
1290
- const canonical = canonicalizeManagedSnippet(snippet.content);
1291
- problems.push(...managedSnippetContentProblems(snippet.id, canonical));
1292
- prepared.push({
1293
- canonical,
1294
- hash: hashCanonicalManagedSnippet(canonical),
1295
- id: snippet.id,
1296
- kind: "desired"
1297
- });
1298
- }
1299
- return {
1300
- prepared,
1301
- problems: Object.freeze(problems)
1302
- };
1303
- }
1304
- //#endregion
1305
- //#region ../core/src/managed-block/reconcile-ledger.ts
1306
- function renderLedgerSnippets(source, block, desired, options) {
1307
- if (block === void 0 || options.ownedSnippetIds === void 0) return desired;
1308
- const controlled = controlledSnippetIds(desired, options);
1309
- const preserved = new Set(options.preserveSnippetIds ?? []);
1310
- const remaining = [...desired];
1311
- const rendered = [];
1312
- for (const snippet of block.snippets) {
1313
- if (preserved.has(snippet.id) || !controlled.has(snippet.id)) {
1314
- rendered.push({
1315
- id: snippet.id,
1316
- kind: "preserved",
1317
- raw: source.slice(snippet.startOffset, snippet.endOffset)
1318
- });
1319
- continue;
1320
- }
1321
- const replacement = remaining.shift();
1322
- if (replacement !== void 0) rendered.push(replacement);
1323
- }
1324
- rendered.push(...remaining);
1325
- return rendered;
1326
- }
1327
- function controlledSnippetIds(desired, options) {
1328
- return /* @__PURE__ */ new Set([...options.ownedSnippetIds ?? [], ...desired.map((snippet) => snippet.id)]);
1329
- }
1330
- function preservedUnownedNotes(block, desired, options) {
1331
- if (block === void 0 || options.ownedSnippetIds === void 0) return [];
1332
- const controlled = controlledSnippetIds(desired, options);
1333
- return block.snippets.filter((snippet) => !controlled.has(snippet.id)).map((snippet) => Object.freeze({
1334
- code: "preserved-unowned-snippet",
1335
- line: snippet.startLine,
1336
- message: `Snippet "${snippet.id}" is not recorded in Aura's manifest; its section was preserved.`
1337
- }));
1338
- }
1339
- //#endregion
1340
- //#region ../core/src/managed-block/reconcile.ts
999
+ //#region ../core/src/managed-block/strip.ts
1341
1000
  /**
1342
- * Reconciles one source string against the complete ordered set of desired snippets.
1001
+ * Removes a legacy Aura-managed block from a source string, keeping everything else byte-for-byte.
1343
1002
  *
1344
- * Existing hash mismatches remain observable through {@link readManagedBlock}, but reconciliation
1345
- * deliberately replaces managed content with the desired canonical version, reporting each
1346
- * discarded hand edit as an `overwritten-snippet` note. Invalid structures fail closed and return
1347
- * the original source unchanged unless the caller opts into `onInvalid: "repair"`.
1348
- */
1349
- function reconcileManagedBlock(source, desiredSnippets, options = {}) {
1350
- return reconcileParsedManagedBlock(source, readManagedBlock(source), desiredSnippets, options);
1351
- }
1352
- /**
1353
- * {@link reconcileManagedBlock}, for a caller that already parsed `source`.
1354
- *
1355
- * `current` must be `readManagedBlock(source)` for the same string; passing a result read from
1356
- * anything else splices content at offsets that no longer describe the source.
1357
- */
1358
- function reconcileParsedManagedBlock(source, current, desiredSnippets, options = {}) {
1359
- const desired = prepareDesiredSnippets(desiredSnippets, options);
1360
- if (desired.problems.length > 0) return invalidResult(source, current.notes, desired.problems);
1361
- const hiddenMarker = current.notes.find((note) => note.code === "unterminated-fence");
1362
- if (hiddenMarker !== void 0) return invalidResult(source, current.notes, [Object.freeze({
1363
- code: "unterminated-fence",
1364
- line: hiddenMarker.line,
1365
- message: hiddenMarker.message
1366
- })]);
1367
- if (current.status !== "invalid") {
1368
- const block = current.status === "present" ? current.block : void 0;
1369
- const rendered = renderLedgerSnippets(source, block, desired.prepared, options);
1370
- const notes = [
1371
- ...current.notes,
1372
- ...overwrittenNotes(current, desired.prepared, options),
1373
- ...preservedUnownedNotes(block, desired.prepared, options)
1374
- ];
1375
- return settle(source, buildContent(source, rendered, current), notes);
1376
- }
1377
- if (options.onInvalid !== "repair") return invalidResult(source, current.notes, current.problems);
1378
- if (options.ownedSnippetIds !== void 0) return invalidResult(source, current.notes, current.problems);
1379
- const stripped = stripManagedMarkers(source);
1380
- const repaired = readManagedBlock(stripped);
1381
- if (repaired.status === "invalid") return invalidResult(source, current.notes, current.problems);
1382
- const notes = [...current.notes, ...repairNotes(current.problems)];
1383
- return settle(source, buildContent(stripped, desired.prepared, repaired), notes);
1384
- }
1385
- function buildContent(source, prepared, current) {
1386
- if (prepared.length === 0) return current.status === "absent" ? source : source.slice(0, current.block.startOffset) + current.block.unmanagedContent + source.slice(current.block.endOffset);
1387
- const lineEnding = detectLineEnding$1(source);
1388
- if (current.status === "absent") return `${source}${source.length === 0 || source.endsWith("\n") ? "" : lineEnding}${renderManagedBlock(prepared, lineEnding, true)}`;
1389
- const replacement = renderManagedBlock(prepared, lineEnding, source[current.block.endOffset - 1] === "\n" || current.block.unmanagedContent.length > 0);
1390
- if (current.block.unmanagedContent.length === 0 && replacement === source.slice(current.block.startOffset, current.block.endOffset)) return source;
1391
- return source.slice(0, current.block.startOffset) + replacement + current.block.unmanagedContent + source.slice(current.block.endOffset);
1392
- }
1393
- function renderManagedBlock(snippets, lineEnding, endsWithLineEnding) {
1394
- const parts = [
1395
- AURA_MANAGED_BLOCK_BEGIN,
1396
- lineEnding,
1397
- AURA_MANAGED_BLOCK_NOTICE,
1398
- lineEnding
1399
- ];
1400
- for (const snippet of snippets) {
1401
- if (snippet.kind === "preserved") {
1402
- parts.push(snippet.raw);
1403
- continue;
1404
- }
1405
- const content = lineEnding === "\n" ? snippet.canonical : snippet.canonical.replaceAll("\n", lineEnding);
1406
- parts.push(`${AURA_MANAGED_SNIPPET_BEGIN_PREFIX}${snippet.id} sha256=${snippet.hash} -->`, lineEnding, content, `${AURA_MANAGED_SNIPPET_END_PREFIX}${snippet.id} -->`, lineEnding);
1407
- }
1408
- parts.push(AURA_MANAGED_BLOCK_END);
1409
- if (endsWithLineEnding) parts.push(lineEnding);
1410
- return parts.join("");
1411
- }
1412
- function overwrittenNotes(current, desired, options) {
1413
- if (current.status === "absent") return [];
1414
- const controlled = controlledSnippetIds(desired, options);
1415
- const desiredById = new Map(desired.map((snippet) => [snippet.id, snippet]));
1416
- const preserved = new Set(options.preserveSnippetIds ?? []);
1417
- return current.block.snippets.flatMap((snippet) => {
1418
- if (options.ownedSnippetIds !== void 0 && !controlled.has(snippet.id) || preserved.has(snippet.id) || !handEdited(snippet, options)) return [];
1419
- const replacement = desiredById.get(snippet.id);
1420
- if (replacement === void 0) return [Object.freeze({
1421
- code: "removed-snippet",
1422
- line: snippet.startLine,
1423
- message: `Snippet "${snippet.id}" was edited by hand since Aura wrote it; the edit is being removed.`
1424
- })];
1425
- if (replacement.hash === snippet.computedHash) return [];
1426
- return [Object.freeze({
1427
- code: "overwritten-snippet",
1428
- line: snippet.startLine,
1429
- message: `Snippet "${snippet.id}" was edited by hand since Aura wrote it; the edit is being replaced.`
1430
- })];
1431
- });
1432
- }
1433
- /**
1434
- * Whether a section differs from what Aura last recorded for it.
1003
+ * For a consumer deciding what a file's *user-authored* content is — instruction consolidation
1004
+ * above all — the managed block is Aura's own artifact: merging it elsewhere plants links and
1005
+ * legacy ledger sections in files that must not carry them. Unmanaged lines found inside the block
1006
+ * are user text and are kept.
1435
1007
  *
1436
- * The marker hash is written by whoever wrote the marker, so a re-stamped section certifies itself
1437
- * and `hashMatches` alone cannot tell a hand edit from a catalog upgrade. The manifest is the only
1438
- * record the editor did not control; fall back to the marker only when there is no manifest entry.
1008
+ * A source with no block, and one whose block does not parse, are returned unchanged: a malformed
1009
+ * block cannot be attributed to Aura with confidence, and dropping bytes on that guess would lose
1010
+ * user content.
1439
1011
  */
1440
- function handEdited(snippet, options) {
1441
- const previousHash = options.previousSnippetHashes?.get(snippet.id);
1442
- return previousHash === void 0 ? !snippet.hashMatches : previousHash !== snippet.computedHash;
1443
- }
1444
- function repairNotes(problems) {
1445
- return problems.map((problem) => Object.freeze({
1446
- code: "repaired-invalid-block",
1447
- line: problem.line,
1448
- message: `Rebuilt the managed block to repair: ${problem.message}`
1449
- }));
1450
- }
1451
- function invalidResult(source, notes, problems) {
1452
- return Object.freeze({
1453
- content: source,
1454
- notes: Object.freeze([...notes]),
1455
- problems,
1456
- status: "invalid"
1457
- });
1458
- }
1459
- function settle(source, content, notes) {
1460
- return content === source ? Object.freeze({
1461
- content: source,
1462
- notes: Object.freeze([...notes]),
1463
- status: "unchanged"
1464
- }) : Object.freeze({
1465
- content,
1466
- notes: Object.freeze([...notes]),
1467
- status: "updated"
1468
- });
1012
+ function stripLegacyManagedBlock(source) {
1013
+ const parsed = readManagedBlock(source);
1014
+ if (parsed.status !== "present") return source;
1015
+ return source.slice(0, parsed.block.startOffset) + parsed.block.unmanagedContent + source.slice(parsed.block.endOffset);
1469
1016
  }
1470
1017
  //#endregion
1471
1018
  //#region ../core/src/managed-content/revision.ts
@@ -1596,210 +1143,6 @@ function convergedPlan$1(app, skillId) {
1596
1143
  };
1597
1144
  }
1598
1145
  //#endregion
1599
- //#region ../core/src/workspace/mcp-classify.ts
1600
- /**
1601
- * Splits desired servers into the ones Aura may write and the collisions a person has to settle.
1602
- *
1603
- * Scope is part of identity here. A server named `docs` in a project `.mcp.json` is not the `docs`
1604
- * the manifest wants in user-level configuration, and treating them as one either blocks a write
1605
- * that would not have collided or skips one that never happened.
1606
- */
1607
- function classifyDesired(desired, ledgerNames, state) {
1608
- const ledger = new Set(ledgerNames);
1609
- const owned = [];
1610
- const blockers = [];
1611
- for (const entry of desired) {
1612
- const blocker = collisionBlocker(entry, ledger, state);
1613
- if (blocker === "owned") owned.push(entry);
1614
- else if (blocker !== void 0) blockers.push(blocker);
1615
- }
1616
- return {
1617
- blockers,
1618
- owned
1619
- };
1620
- }
1621
- /** `owned` to write it, a blocker to refuse, `undefined` when the config already satisfies it. */
1622
- function collisionBlocker(entry, ledger, state) {
1623
- if (ledger.has(entry.name)) return "owned";
1624
- const sameName = (candidate) => candidate.name === entry.name && candidate.scope === entry.scope;
1625
- const unusable = state.unusable.find(sameName);
1626
- if (unusable !== void 0) return {
1627
- message: unusable.reason === "disabled" ? `MCP server ${entry.name} is already declared in this application's ${entry.scope} configuration but is turned off there. Remove or enable it, then run the fix again.` : `MCP server ${entry.name} is already declared in this application's ${entry.scope} configuration in a form Aura does not recognize, so Aura left it unchanged.`,
1628
- scope: entry.scope,
1629
- sourceId: unusable.sourceId
1630
- };
1631
- const existing = state.servers.filter(sameName);
1632
- if (existing.length === 0) return "owned";
1633
- const normalized = normalizeDesired(entry);
1634
- if ("message" in normalized) return normalized;
1635
- return existing.every((server) => isDeepStrictEqual(server.transport, normalized.transport)) ? void 0 : {
1636
- message: `MCP server ${entry.name} already exists outside Aura's ownership ledger and differs from the manifest.`,
1637
- scope: entry.scope
1638
- };
1639
- }
1640
- /**
1641
- * Normalizes one desired transport, reporting a manifest Aura refuses to write as a blocker.
1642
- *
1643
- * The manifest is a file a person can edit. One that has acquired a credential literal is not a
1644
- * crash in a check's `detect`; it is something to say out loud and decline to propagate.
1645
- */
1646
- function normalizeDesired(entry) {
1647
- try {
1648
- return { transport: normalizeMcpServerDefinition(entry.transport) };
1649
- } catch (error) {
1650
- return {
1651
- message: error instanceof McpWriteError ? `MCP server ${entry.name} cannot be written as the manifest defines it: ${error.message}` : `MCP server ${entry.name} has a manifest definition Aura cannot represent.`,
1652
- scope: entry.scope
1653
- };
1654
- }
1655
- }
1656
- //#endregion
1657
- //#region ../core/src/workspace/mcp-convergence.ts
1658
- /** Captures adapter source bytes behind a function, keeping them out of the model entirely. */
1659
- function createAppMcpConvergence(adapter, files, state) {
1660
- const writer = adapter.mcpWrite;
1661
- if (writer === void 0) return;
1662
- const targets = [...files.values()].filter((file) => file.spec.kind === "mcp");
1663
- return (desired, ledgerNames) => {
1664
- const classified = classifyDesired(desired, ledgerNames, state);
1665
- if (classified.blockers.length > 0) return blocked(classified.blockers);
1666
- const scopes = ["global", "project"].map((scope) => planScope(adapter, writer, targets, classified.owned, ledgerNames, scope, state.secretSightings));
1667
- const blockers = scopes.flatMap((result) => result.blockers);
1668
- const operations = scopes.flatMap((result) => result.operations);
1669
- return blockers.length > 0 ? blocked(blockers) : {
1670
- blockers: [],
1671
- operations,
1672
- ownedNames: [...new Set(classified.owned.map((entry) => entry.name))].sort()
1673
- };
1674
- };
1675
- }
1676
- function planScope(adapter, writer, targets, desired, ledgerNames, scope, sightings) {
1677
- const scopedDesired = desired.filter((entry) => entry.scope === scope);
1678
- const scopedTargets = targets.filter((target) => target.spec.scope === scope);
1679
- if (scopedDesired.length > 0 && scopedTargets.length === 0) return blockedScope(`${adapter.displayName} has no ${scope}-scope MCP configuration target.`, scope);
1680
- if (scopedTargets.length > 1) return blockedScope(`${adapter.displayName} declares more than one ${scope}-scope MCP write target.`, scope);
1681
- const target = scopedTargets[0];
1682
- return target === void 0 ? {
1683
- blockers: [],
1684
- operations: []
1685
- } : writeScopeTarget(adapter, writer, target, scopedDesired, ledgerNames, scope, sightings);
1686
- }
1687
- function writeScopeTarget(adapter, writer, target, desired, ledgerNames, scope, sightings) {
1688
- if (!needsTargetWrite(target, desired, ledgerNames)) return {
1689
- blockers: [],
1690
- operations: []
1691
- };
1692
- const unwritable = targetRefusal(target);
1693
- if (unwritable !== void 0) return {
1694
- blockers: [{
1695
- ...unwritable,
1696
- scope,
1697
- sourceId: target.spec.id
1698
- }],
1699
- operations: []
1700
- };
1701
- const written = runWriter(writer, target, desired, ledgerNames);
1702
- if ("refusal" in written) return {
1703
- blockers: [{
1704
- message: written.refusal,
1705
- path: target.spec.path,
1706
- scope,
1707
- sourceId: target.spec.id
1708
- }],
1709
- operations: []
1710
- };
1711
- if (Buffer.byteLength(written.content, "utf8") > 4194304) return {
1712
- blockers: [{
1713
- message: `${adapter.displayName}'s MCP configuration at ${target.spec.path} is larger than Aura will rewrite in one operation, so Aura left it unchanged.`,
1714
- path: target.spec.path,
1715
- scope,
1716
- sourceId: target.spec.id
1717
- }],
1718
- operations: []
1719
- };
1720
- const operation = {
1721
- content: written.content,
1722
- mode: scope === "global" ? 384 : 420,
1723
- path: target.spec.path,
1724
- precondition: targetPrecondition(target),
1725
- type: "write"
1726
- };
1727
- rememberMcpRedactor(adapter, operation, target, sightings);
1728
- return {
1729
- blockers: [],
1730
- operations: [operation]
1731
- };
1732
- }
1733
- /**
1734
- * Registers preview masking only for a target that actually holds an inline credential.
1735
- *
1736
- * Registering unconditionally would cost every convergence preview its diff, because masking has
1737
- * no previous side to project when the fix is creating the file — which is what most MCP-001 fixes
1738
- * do, on a file that by definition has no credentials in it yet.
1739
- */
1740
- function rememberMcpRedactor(adapter, operation, target, sightings) {
1741
- const transform = adapter.mcpSecrets;
1742
- const scoped = sightings.filter((sighting) => sighting.sourceId === target.spec.id);
1743
- if (transform === void 0 || scoped.length === 0) return;
1744
- rememberWriteRedactor(operation, mcpSecretRedactor(transform, scoped));
1745
- }
1746
- /** Runs a plugin serializer, treating a thrown error as the refusal it forgot to return. */
1747
- function runWriter(writer, target, desired, ledgerNames) {
1748
- try {
1749
- return writer({
1750
- desired,
1751
- ...target.content === void 0 ? {} : { existingContent: target.content },
1752
- ledgerNames
1753
- });
1754
- } catch {
1755
- return { refusal: "The MCP configuration serializer failed, so Aura left the file unchanged." };
1756
- }
1757
- }
1758
- /**
1759
- * What the file must still look like when the plan is applied.
1760
- *
1761
- * The rendered content is this file's own previous bytes with one section replaced, so applying it
1762
- * against a file the application has since rewritten would silently revert that work. Declaring
1763
- * what was read turns the race into a reported conflict.
1764
- */
1765
- function targetPrecondition(target) {
1766
- return target.content === void 0 ? { kind: "absent" } : {
1767
- digest: createHash("sha256").update(target.content, "utf8").digest("hex"),
1768
- kind: "sha256"
1769
- };
1770
- }
1771
- function needsTargetWrite(target, desired, ledgerNames) {
1772
- return (desired.length > 0 || ledgerNames.length > 0) && (target.exists || desired.length > 0);
1773
- }
1774
- /** Why this target cannot be rewritten safely, before a serializer is asked to try. */
1775
- function targetRefusal(target) {
1776
- const path = target.spec.path;
1777
- if (target.problem !== void 0 || target.exists && target.content === void 0) return {
1778
- message: `MCP configuration at ${path} could not be read safely, so Aura left it unchanged.`,
1779
- path
1780
- };
1781
- return (target.size ?? 0) > 4194304 ? {
1782
- message: `MCP configuration at ${path} is larger than Aura will rewrite in one operation, so Aura left it unchanged.`,
1783
- path
1784
- } : void 0;
1785
- }
1786
- function blockedScope(message, scope) {
1787
- return {
1788
- blockers: [{
1789
- message,
1790
- scope
1791
- }],
1792
- operations: []
1793
- };
1794
- }
1795
- function blocked(blockers) {
1796
- return {
1797
- blockers,
1798
- operations: [],
1799
- ownedNames: []
1800
- };
1801
- }
1802
- //#endregion
1803
1146
  //#region ../core/src/workspace/mcp-plan-manifest.ts
1804
1147
  /**
1805
1148
  * Merges preset-required servers under the manifest's own, keyed by scope and name.
@@ -1858,12 +1201,16 @@ function withoutServer(manifest, server) {
1858
1201
  * association lives here instead of on the object: reaching a planner takes this module, which
1859
1202
  * plugin code cannot import.
1860
1203
  */
1861
- const PLANNERS$1 = /* @__PURE__ */ new WeakMap();
1204
+ const PLANNERS = /* @__PURE__ */ new WeakMap();
1862
1205
  /** Per-scan memo of what convergence planning produced, keyed by the model it was computed from. */
1863
- const PLANS$1 = /* @__PURE__ */ new WeakMap();
1206
+ const PLANS = /* @__PURE__ */ new WeakMap();
1864
1207
  /** Associates a planner with the app model core just built for it. */
1865
1208
  function rememberMcpConvergence(app, convergence) {
1866
- PLANNERS$1.set(app, convergence);
1209
+ PLANNERS.set(app, convergence);
1210
+ }
1211
+ /** Forgets one scan's memoized plans, after a refresh replaced the bytes they were built from. */
1212
+ function forgetMcpPlans(model) {
1213
+ PLANS.delete(model);
1867
1214
  }
1868
1215
  /**
1869
1216
  * Reports only why convergence is impossible, without rendering any file.
@@ -1877,8 +1224,8 @@ function mcpConvergenceBlockers(model, appId) {
1877
1224
  }
1878
1225
  /** Builds application writes and the ownership-ledger update as one atomic fix plan. */
1879
1226
  function planManifestMcpConvergence(model, appId) {
1880
- const memo = PLANS$1.get(model) ?? /* @__PURE__ */ new Map();
1881
- PLANS$1.set(model, memo);
1227
+ const memo = PLANS.get(model) ?? /* @__PURE__ */ new Map();
1228
+ PLANS.set(model, memo);
1882
1229
  const cached = memo.get(appId);
1883
1230
  if (cached !== void 0) return cached;
1884
1231
  const computed = computeConvergence(model, appId);
@@ -1994,103 +1341,53 @@ function ambiguousRemoval(model, server) {
1994
1341
  function resolvePlanner(model, appId) {
1995
1342
  const app = model.apps.find((candidate) => candidate.adapterId === appId);
1996
1343
  if (app === void 0) return { blockers: [{ message: `Application ${appId} was not detected.` }] };
1997
- const convergence = PLANNERS$1.get(app);
1344
+ const convergence = PLANNERS.get(app);
1998
1345
  return convergence === void 0 ? { blockers: [{ message: `${app.displayName}'s adapter cannot write MCP configuration.` }] } : {
1999
1346
  app,
2000
1347
  convergence
2001
1348
  };
2002
1349
  }
2003
1350
  //#endregion
2004
- //#region ../core/src/workspace/mcp-secret-plan.ts
2005
- const PLANNERS = /* @__PURE__ */ new WeakMap();
2006
- const PLANS = /* @__PURE__ */ new WeakMap();
2007
- /** Captures raw MCP bytes for remediation behind the same private boundary as convergence. */
2008
- function createAppMcpSecretPlanner(adapter, files, sightings) {
2009
- const transform = adapter.mcpSecrets;
2010
- if (transform === void 0) return;
2011
- return {
2012
- plan: (sourceId) => {
2013
- const target = files.get(sourceId);
2014
- const sourceSightings = sightings.filter((sighting) => sighting.sourceId === sourceId);
2015
- const supported = sourceSightings.filter(transform.supports);
2016
- if (target === void 0 || target.spec.kind !== "mcp" || target.content === void 0 || targetRefusal(target) !== void 0 || supported.length === 0) return;
2017
- let rewritten;
2018
- try {
2019
- rewritten = transform.rewrite({
2020
- content: target.content,
2021
- sightings: supported
2022
- });
2023
- } catch {
2024
- return;
2025
- }
2026
- if ("refusal" in rewritten || rewritten.rewrittenFields.length !== supported.length) return;
2027
- if (Buffer.byteLength(rewritten.content, "utf8") > 4194304) return;
2028
- const operation = {
2029
- content: rewritten.content,
2030
- mode: target.spec.scope === "global" ? 384 : 420,
2031
- path: target.spec.path,
2032
- precondition: targetPrecondition(target),
2033
- type: "write"
2034
- };
2035
- rememberWriteRedactor(operation, mcpSecretRedactor(transform, sourceSightings));
2036
- return {
2037
- manualSteps: remediationSteps(target.spec.path, supported),
2038
- operations: [operation],
2039
- summary: `Replace inline MCP credentials in ${target.spec.path} with environment references.`
2040
- };
2041
- },
2042
- supports: transform.supports
2043
- };
2044
- }
2045
- /** Associates a private planner with the app model that exposes only safe sightings. */
2046
- function rememberMcpSecretPlanner(app, planner) {
2047
- PLANNERS.set(app, planner);
2048
- }
2049
- /**
2050
- * Whether this occurrence has a behavior-preserving adapter rewrite that a plan can actually carry.
2051
- *
2052
- * `supports` only reads the locator's shape, so it answers "could this kind of field be rewritten",
2053
- * not "was this one". Planning is what discovers that the document is spelled in a form the writer
2054
- * cannot edit, and a finding that advertises a guided fix the planner then declines to produce is
2055
- * a dead end for the user. The plan is memoized, so asking it here costs nothing.
2056
- */
2057
- function canPlanMcpSecretRemediation(model, sighting) {
2058
- const app = model.apps.find((candidate) => candidate.adapterId === sighting.appId);
2059
- if ((app === void 0 ? void 0 : PLANNERS.get(app))?.supports(sighting) !== true) return false;
2060
- return planMcpSecretRemediation(model, sighting) !== void 0;
2061
- }
2062
- /** Builds the one whole-file plan shared by every representable sighting in a source file. */
2063
- function planMcpSecretRemediation(model, sighting) {
2064
- const key = `${sighting.appId}:${sighting.sourceId}`;
2065
- const memo = PLANS.get(model) ?? /* @__PURE__ */ new Map();
2066
- PLANS.set(model, memo);
2067
- if (memo.has(key)) return memo.get(key);
2068
- const app = model.apps.find((candidate) => candidate.adapterId === sighting.appId);
2069
- const plan = (app === void 0 ? void 0 : PLANNERS.get(app))?.plan(sighting.sourceId);
2070
- memo.set(key, plan);
2071
- return plan;
2072
- }
1351
+ //#region ../core/src/workspace/append-instructions.ts
2073
1352
  /**
2074
- * What the user has to do around the rewrite, in the order the rewrite makes them possible.
1353
+ * Appends Markdown fragments while preserving every existing byte.
2075
1354
  *
2076
- * Rotation leads the list because moving a credential out of the file does not undo where it has
2077
- * already been. A value that sat inline in configuration is a value that may have reached source
2078
- * control, a shell history, a log, or a copied snippet which is the reason this check exists, and
2079
- * the reason the same value put behind an environment reference is still the wrong value to keep.
1355
+ * Only appended fragments are canonicalized, and by the same normalization the content hash
1356
+ * covers that is what lets a recorded fingerprint answer for text already in the file. They use
1357
+ * the target's line ending and end in one line ending, with one blank-line seam between
1358
+ * consecutive fragments.
2080
1359
  */
2081
- function remediationSteps(path, sightings) {
2082
- const copies = sightings.map((sighting) => `Copy the current value for ${sighting.serverName}.${sighting.field} from ${path} now; Aura will replace it with ${sighting.suggestedEnvName}.`);
2083
- const exports = [...new Set(sightings.map((sighting) => sighting.suggestedEnvName))].sort().map((name) => `Run \`export ${name}=…\` in the environment that starts the MCP client.`);
2084
- return [
2085
- ...copies,
2086
- ...exports,
2087
- "Rotate each credential at its issuer: an inline value may already have been copied elsewhere.",
2088
- "Run `aura check` again."
2089
- ];
1360
+ function appendInstructionFragments(source, fragments) {
1361
+ if (fragments.length === 0) return source;
1362
+ const lineEnding = source.includes("\r\n") ? "\r\n" : "\n";
1363
+ let result = source;
1364
+ for (const fragment of fragments) {
1365
+ const canonical = canonicalizeContent(fragment).replaceAll("\n", lineEnding);
1366
+ if (result.length > 0) result += blankLineSeparator(result, lineEnding);
1367
+ result += canonical;
1368
+ }
1369
+ return result;
1370
+ }
1371
+ function blankLineSeparator(source, lineEnding) {
1372
+ let trailingLineEndings = 0;
1373
+ let end = source.length;
1374
+ while (end > 0 && trailingLineEndings < 2) {
1375
+ if (source.endsWith("\r\n", end)) {
1376
+ end -= 2;
1377
+ trailingLineEndings += 1;
1378
+ continue;
1379
+ }
1380
+ if (source[end - 1] === "\n" || source[end - 1] === "\r") {
1381
+ end -= 1;
1382
+ trailingLineEndings += 1;
1383
+ continue;
1384
+ }
1385
+ break;
1386
+ }
1387
+ return lineEnding.repeat(2 - trailingLineEndings);
2090
1388
  }
2091
1389
  //#endregion
2092
1390
  //#region ../core/src/workspace/shared-link-plan.ts
2093
- const SHARED_LINK_SNIPPET_ID = "shared-instructions";
2094
1391
  /** Builds the same safe shared-instruction link plan for checks and setup. */
2095
1392
  function planSharedInstructionLink(app, model, options = {}) {
2096
1393
  const link = options.link ?? app.sharedLink;
@@ -2099,21 +1396,24 @@ function planSharedInstructionLink(app, model, options = {}) {
2099
1396
  const refusal = refuseBefore(app, status);
2100
1397
  if (refusal !== void 0) return { blocked: refusal };
2101
1398
  switch (link.kind) {
2102
- case "import-line": return planImportLine(app, model, link, status, options.sourceContent);
2103
- case "native-copy": return planNativeCopy(app, model, link, status, options.sourceContent);
1399
+ case "import-line": return planImportLine(app, link, status, options.sourceContent);
1400
+ case "native-copy": return planNativeCopy(app, link, status, options.sourceContent);
2104
1401
  case "symlink": return planSymlink(app, model, link, status, options);
2105
1402
  }
2106
1403
  }
2107
- function planImportLine(app, model, link, status, sourceContent) {
1404
+ function planImportLine(app, link, status, sourceContent) {
2108
1405
  const source = instructionEntry(app, link.entryPath);
2109
1406
  if (unreadableInstructionEntry(status, source, sourceContent)) return { blocked: `Something is at ${link.entryPath} that the adapter could not read as an instruction file, so Aura will not replace it. Check whether it is a broken symbolic link.` };
2110
- const reconciled = reconcileManagedBlock(selectedContent(sourceContent, source), [{
2111
- content: link.content ?? "",
2112
- id: SHARED_LINK_SNIPPET_ID
2113
- }]);
2114
- if (reconciled.status === "invalid") return { blocked: `The Aura-managed block in ${link.entryPath} is malformed, so Aura will not rewrite the file. Repair or delete the block and run check --fix again.` };
2115
- if (reconciled.status === "unchanged" && observedStateHolds(sourceContent)) return { plan: convergedPlan(app) };
2116
- return { plan: writePlan(app, link, reconciled.content, model.homeDir) };
1407
+ const current = selectedContent(sourceContent, source);
1408
+ const desired = link.content ?? "";
1409
+ if (observedStateHolds(sourceContent) && containsImport(current, desired)) return { plan: convergedPlan(app) };
1410
+ return { plan: writePlan(app, link, appendInstructionFragments(current, [desired])) };
1411
+ }
1412
+ /** Legacy marked imports and new plain imports both contain the exact adapter-rendered line. */
1413
+ function containsImport(source, desired) {
1414
+ const normalized = desired.replace(/\r\n?/gu, "\n");
1415
+ const normalizedDesired = normalized.endsWith("\n") ? normalized.slice(0, -1) : normalized;
1416
+ return source.replace(/\r\n?/gu, "\n").split("\n").some((line) => line === normalizedDesired);
2117
1417
  }
2118
1418
  /**
2119
1419
  * Whether what the scan saw still describes the file when this plan is applied.
@@ -2132,19 +1432,19 @@ function unreadableInstructionEntry(status, source, sourceContent) {
2132
1432
  function selectedContent(sourceContent, source) {
2133
1433
  return sourceContent ?? source?.content ?? "";
2134
1434
  }
2135
- function planNativeCopy(app, model, link, status, sourceContent) {
1435
+ function planNativeCopy(app, link, status, sourceContent) {
2136
1436
  const content = sourceContent ?? instructionEntry(app, link.entryPath)?.content;
2137
1437
  const refusal = nativeCopyRefusal(status, sourceContent, content, link.content);
2138
1438
  if (refusal !== void 0) return { blocked: refusal };
2139
1439
  if (status?.exists === true && content === link.content && observedStateHolds(sourceContent)) return { plan: convergedPlan(app) };
2140
- return { plan: writePlan(app, link, link.content ?? "", model.homeDir) };
1440
+ return { plan: writePlan(app, link, link.content ?? "") };
2141
1441
  }
2142
1442
  function nativeCopyRefusal(status, sourceContent, content, desired) {
2143
1443
  return status?.exists === true && sourceContent === void 0 && content !== desired ? "The Aura wrapper path contains different content, so it is treated as user-owned and preserved." : void 0;
2144
1444
  }
2145
1445
  function planSymlink(app, model, link, status, options) {
2146
- if (status?.exists === true && status.pathKind !== "symlink" && options.sourceContent === void 0) return { blocked: "The existing file is user-owned. Consolidate its content before replacing it with a symlink." };
2147
1446
  const target = options.symlinkTarget ?? model.sharedInstructions.path;
1447
+ if (status?.exists === true && status.pathKind !== "symlink" && options.sourceContent === void 0 && !holdsOnlySharedReference(app, link.entryPath, target)) return { blocked: "The existing file is user-owned and is never replaced automatically. Run `aura setup` and choose instruction consolidation to merge its content into the shared source." };
2148
1448
  if (observedStateHolds(options.sourceContent) && pointsAt(status, target)) return { plan: convergedPlan(app) };
2149
1449
  return { plan: {
2150
1450
  operations: [{
@@ -2178,9 +1478,16 @@ function refuseBefore(app, status) {
2178
1478
  if (status?.problem !== void 0) return `Aura could not safely read the instruction entry (${status.problem}) and will not overwrite it.`;
2179
1479
  return status?.pathKind === "directory" ? "The instruction entry is a directory and cannot be replaced automatically." : void 0;
2180
1480
  }
2181
- function writePlan(app, link, content, homeDir) {
1481
+ /**
1482
+ * Writes one entry, with nothing to warn the user about afterwards.
1483
+ *
1484
+ * There used to be a manual step here telling the user to keep a project entry out of version
1485
+ * control, because a project entry could name the shared source by absolute path. It cannot any
1486
+ * more: a project entry names it relatively, and a home entry through `~`. Both are committable or
1487
+ * personal by construction rather than by advice the user has to remember to follow.
1488
+ */
1489
+ function writePlan(app, link, content) {
2182
1490
  return {
2183
- ...link.scope === "project" && content.includes(homeDir) ? { manualSteps: [`${link.entryPath} points at the shared source by absolute path, which is specific to this machine and this user. Keep it out of version control — add it to .gitignore or .git/info/exclude.`] } : {},
2184
1491
  operations: [{
2185
1492
  content,
2186
1493
  mode: 420,
@@ -2190,6 +1497,19 @@ function writePlan(app, link, content, homeDir) {
2190
1497
  summary: `Link ${app.displayName} to the shared instruction source.`
2191
1498
  };
2192
1499
  }
1500
+ /**
1501
+ * Whether a real file at this entry holds nothing but a legacy block or plain link Aura wrote.
1502
+ *
1503
+ * The refusal above guards the user's own guidance, and an entry Aura wired as an import line holds
1504
+ * none. Without this, an app switching from an import line to a link strands every machine already
1505
+ * wired the old way: consolidation has no source to offer for a file that is Aura's.
1506
+ */
1507
+ function holdsOnlySharedReference(app, path, target) {
1508
+ const document = instructionEntry(app, path);
1509
+ if (document === void 0) return false;
1510
+ if (stripLegacyManagedBlock(document.content).trim().length === 0) return true;
1511
+ return document.content.replace(/\r\n?/gu, "\n").split("\n").filter((line) => line.trim().length > 0).length === 1 && document.links.some((candidate) => resolve(candidate.targetPath) === resolve(target));
1512
+ }
2193
1513
  function instructionEntry(app, path) {
2194
1514
  return app.instructionFiles.find((document) => resolve(document.path) === resolve(path));
2195
1515
  }
@@ -2197,4 +1517,4 @@ function entryStatus(app, path) {
2197
1517
  return app.sourceFiles.find((file) => resolve(file.spec.path) === resolve(path));
2198
1518
  }
2199
1519
  //#endregion
2200
- export { renderRemoveDiff as A, errorMessage as B, canonicalizeManagedSnippet as C, renderArchiveDiff as D, renderRedactedWriteDiff as E, assertAuraManifestWritable as F, src_default as G, resolveAuraManifestPath as H, createAuraManifestWriteOperation as I, pluralize as K, createEmptyAuraManifest as L, FILE_MODES as M, MAX_MUTABLE_FILE_BYTES as N, renderConflict as O, MAX_RETAINED_PLAN_BYTES as P, parseAuraManifest as R, readManagedBlock as S, hashManagedSnippet as T, AuraManifestError as U, isRecord as V, SHARED_INSTRUCTIONS_TEMPLATE as W, planSharedSkillTreeUpdate as _, rememberMcpSecretPlanner as a, reconcileManagedSnippet as b, planManifestMcpConvergence as c, createAppMcpConvergence as d, isAuraOwnedSkillTarget as f, managedContentRevisionStatus as g, skillDeploymentStatus as h, planMcpSecretRemediation as i, renderSymlinkDiff as j, renderMoveDiff as k, planMcpServerRemoval as l, sharedSkillsRoot as m, canPlanMcpSecretRemediation as n, mcpConvergenceBlockers as o, planSkillDeployment as p, displayPath as q, createAppMcpSecretPlanner as r, planDesiredMcpConvergence as s, planSharedInstructionLink as t, rememberMcpConvergence as u, reconcileParsedManagedBlock as v, hashCanonicalManagedSnippet as w, managedSnippetContentProblems as x, diffManagedSnippet as y, errorCode as z };
1520
+ export { displayPath as A, isRecord as C, SHARED_INSTRUCTIONS_TEMPLATE as D, AuraManifestError as E, src_default as O, errorMessage as S, resolveAuraManifestPath as T, assertAuraManifestWritable as _, planDesiredMcpConvergence as a, parseAuraManifest as b, rememberMcpConvergence as c, sharedSkillsRoot as d, skillDeploymentStatus as f, hashContent as g, stripLegacyManagedBlock as h, mcpConvergenceBlockers as i, pluralize as k, isAuraOwnedSkillTarget as l, planSharedSkillTreeUpdate as m, appendInstructionFragments as n, planManifestMcpConvergence as o, managedContentRevisionStatus as p, forgetMcpPlans as r, planMcpServerRemoval as s, planSharedInstructionLink as t, planSkillDeployment as u, createAuraManifestWriteOperation as v, AURA_MANIFEST_PATH as w, errorCode as x, createEmptyAuraManifest as y };