@tryaura/aura-cli 0.2.0 → 0.3.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.
@@ -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.
@@ -360,28 +402,44 @@ const MAX_TRUSTED_PATH_LENGTH = 1024;
360
402
  * Each entry binds an absolute preset path to a hash of the exact contents that were reviewed, so
361
403
  * a file edited after acceptance is untrusted again until someone looks at the new contents. Only
362
404
  * acceptances appear here: declining records nothing, and the next interactive setup asks again.
405
+ *
406
+ * `mainWorktreePath` names the same file in the repository's primary Git checkout and carries no
407
+ * uniqueness of its own: every worktree of one repository shares it, so a repository accumulates
408
+ * one entry per distinct set of contents its user accepted rather than one per working directory.
363
409
  */
364
410
  function optionalTrustedRepoPresets(value) {
365
411
  if (value === void 0) return {};
366
412
  if (!Array.isArray(value)) throw invalid("$.trustedRepoPresets", "must be an array");
367
413
  if (value.length > 64) throw invalid("$.trustedRepoPresets", `must contain at most ${String(64)} entries`);
368
- const paths = /* @__PURE__ */ new Set();
414
+ const records = /* @__PURE__ */ new Set();
369
415
  return { trustedRepoPresets: Object.freeze(value.map((candidate, index) => {
370
416
  const path = `$.trustedRepoPresets[${String(index)}]`;
371
417
  const entry = requiredObject(candidate, path);
372
- const presetPath = requiredString(entry, "path", path);
373
- if (presetPath.length === 0 || presetPath.length > MAX_TRUSTED_PATH_LENGTH) throw invalid(`${path}.path`, `must be a non-empty path of at most ${String(MAX_TRUSTED_PATH_LENGTH)} characters`);
374
- if (paths.has(presetPath)) throw invalid(`${path}.path`, "must not duplicate another trusted preset path");
375
- paths.add(presetPath);
418
+ const presetPath = trustedPath(requiredString(entry, "path", path), `${path}.path`);
376
419
  const hash = requiredString(entry, "hash", path);
377
420
  if (!SHA256_PATTERN.test(hash)) throw invalid(`${path}.hash`, "must be a lowercase SHA-256 hash");
421
+ const record = `${presetPath}\0${hash}`;
422
+ if (records.has(record)) throw invalid(`${path}.hash`, "must not duplicate another trusted preset path and hash");
423
+ records.add(record);
424
+ const mainWorktree = entry["mainWorktreePath"];
425
+ if (mainWorktree === void 0) return Object.freeze({
426
+ ...entry,
427
+ hash,
428
+ path: presetPath
429
+ });
430
+ if (typeof mainWorktree !== "string") throw invalid(`${path}.mainWorktreePath`, "must be a string");
378
431
  return Object.freeze({
379
432
  ...entry,
380
433
  hash,
434
+ mainWorktreePath: trustedPath(mainWorktree, `${path}.mainWorktreePath`),
381
435
  path: presetPath
382
436
  });
383
437
  })) };
384
438
  }
439
+ function trustedPath(value, jsonPath) {
440
+ if (value.length === 0 || value.length > MAX_TRUSTED_PATH_LENGTH) throw invalid(jsonPath, `must be a non-empty path of at most ${String(MAX_TRUSTED_PATH_LENGTH)} characters`);
441
+ return value;
442
+ }
385
443
  //#endregion
386
444
  //#region ../core/src/manifest/schema.ts
387
445
  const APP_ID_PATTERN = /^[a-z0-9][a-z0-9._-]*$/u;
@@ -505,22 +563,6 @@ function apps(value) {
505
563
  }
506
564
  return Object.freeze(result);
507
565
  }
508
- function snippets(value) {
509
- if (!Array.isArray(value)) throw invalid("$.snippets", "must be an array");
510
- return Object.freeze(value.map((candidate, index) => {
511
- const path = `$.snippets[${String(index)}]`;
512
- const snippet = requiredObject(candidate, path);
513
- const hash = requiredString(snippet, "hash", path);
514
- if (!SHA256_PATTERN.test(hash)) throw invalid(`${path}.hash`, "must be a lowercase SHA-256 hash");
515
- return Object.freeze({
516
- ...snippet,
517
- hash,
518
- id: requiredString(snippet, "id", path),
519
- pinned: requiredBoolean(snippet, "pinned", path),
520
- version: requiredString(snippet, "version", path)
521
- });
522
- }));
523
- }
524
566
  function ownership(value) {
525
567
  const source = requiredObject(value, "$.ownership");
526
568
  const result = {};
@@ -689,228 +731,49 @@ function createAuraManifestWriteOperation(state, manifest) {
689
731
  });
690
732
  }
691
733
  //#endregion
692
- //#region ../core/src/fix-plan/limits.ts
693
- /**
694
- * The largest file Aura will write over, remove, or replace with a link.
695
- *
696
- * A mutation is only reversible if its previous contents were captured, so this doubles as the
697
- * ceiling on what one operation retains in memory. Agent configuration is measured in kilobytes;
698
- * anything approaching this size is not something a fix should be rewriting unattended.
699
- */
700
- const MAX_MUTABLE_FILE_BYTES = 4194304;
701
- /**
702
- * The most file content Aura retains across a whole plan.
703
- *
704
- * {@link MAX_MUTABLE_FILE_BYTES} bounds one operation; without a plan-wide budget a long plan still
705
- * multiplies it by the operation count.
706
- */
707
- const MAX_RETAINED_PLAN_BYTES = 67108864;
708
- /**
709
- * The combined before/after size above which a preview summarizes instead of rendering a patch.
710
- *
711
- * A unified diff is larger than the inputs that produced it, and a preview no human can read is not
712
- * worth the memory it costs.
713
- */
714
- const MAX_DIFF_BYTES = 262144;
715
- /**
716
- * Every mode a plan may request.
717
- *
718
- * `FileMode` already closes this set, but a type is erased at runtime and plugins ship as compiled
719
- * JavaScript. Since the mode reaches `chmod` unchanged, checking it here is what actually keeps a
720
- * plan from asking for a world-writable or setuid file.
721
- */
722
- const FILE_MODES = /* @__PURE__ */ new Set([
723
- 384,
724
- 420,
725
- 448,
726
- 493
727
- ]);
728
- //#endregion
729
- //#region ../core/src/fix-plan/diff.ts
730
- const NULL_PATH = "/dev/null";
731
- function renderWriteDiff(path, before, content, requestedMode, mode) {
732
- const note = modeNote(before, requestedMode, mode);
733
- const previous = textOf(before);
734
- if (previous === void 0) return renderSummary("write", path, "Binary or oversized file would change.") + note;
735
- if (exceedsDiffBudget(previous, content)) return renderSummary("write", path, "File is too large to diff; contents would change.") + note;
736
- return renderPatch("write", path, before.kind === "missing" ? NULL_PATH : path, path, previous, content) + note;
737
- }
738
- /** Fail-closed write preview used when a semantic redactor cannot safely project both sides. */
739
- function renderWriteSummary(path, before, requestedMode, mode) {
740
- return renderSummary("write", path, "Sensitive MCP configuration would change.") + modeNote(before, requestedMode, mode);
741
- }
742
- function renderRemoveDiff(path, before) {
743
- if (before.kind === "directory") return renderSummary("remove", path, "Remove empty directory.");
744
- const previous = textOf(before);
745
- if (previous === void 0) return renderSummary("remove", path, "Binary or oversized file would be removed.");
746
- if (exceedsDiffBudget(previous, "")) return renderSummary("remove", path, "File is too large to diff; it would be removed.");
747
- return renderPatch("remove", path, path, NULL_PATH, previous, "");
748
- }
749
- function renderMoveDiff(sourcePath, destinationPath) {
750
- return [
751
- `diff --aura move ${sourcePath} ${destinationPath}`,
752
- `rename from ${sourcePath}`,
753
- `rename to ${destinationPath}`,
754
- ""
755
- ].join("\n");
756
- }
757
- function renderArchiveDiff(path, relativePath, before, replacement, replacementMode) {
758
- const archive = renderSummary("archive", path, `Preserve the original at <backup>/consolidation/${relativePath}.`);
759
- if (replacement === void 0) return `${archive}${renderRemoveDiff(path, before)}`;
760
- return replacement.type === "symlink" ? `${archive}${renderSymlinkDiff(path, before, replacement.target)}` : `${archive}${renderWriteDiff(path, before, replacement.content, replacement.mode, replacementMode ?? 420)}`;
761
- }
762
- function renderSymlinkDiff(path, before, target) {
763
- const previous = textOf(before);
764
- if (previous === void 0) return renderSummary("symlink", path, `Binary or oversized file would become a link.`);
765
- if (exceedsDiffBudget(previous, target)) return renderSummary("symlink", path, "File is too large to diff; it would become a link.");
766
- return `${renderPatch("symlink", path, before.kind === "missing" ? NULL_PATH : path, path, previous, `${target}\n`)}link target ${target}\n`;
767
- }
768
- /** Describes a conflict in the same shape as a diff, so a renderer can treat previews uniformly. */
769
- function renderConflict(operation, path, reason) {
770
- return renderSummary(operation, path, `Blocked: ${reason}.`);
771
- }
772
- function renderPatch(operation, displayPath, oldPath, newPath, oldContent, newContent) {
773
- return `diff --aura ${operation} ${displayPath}\n${createTwoFilesPatch(oldPath, newPath, oldContent, newContent, "before", "after", { context: 3 })}`;
774
- }
775
- function renderSummary(operation, path, detail) {
776
- return [
777
- `diff --aura ${operation} ${path}`,
778
- detail,
779
- ""
780
- ].join("\n");
781
- }
782
- /**
783
- * Says what happens to an existing file's permissions.
784
- *
785
- * Two cases are worth a line. A mode that changes is a change the diff itself cannot show, and core
786
- * only does that to files it owns as protocol. A `mode` a plan asked for and will not get is the
787
- * commoner one: an existing file keeps whatever the user set, and saying so is the difference
788
- * between a deliberate choice and a silent one.
789
- */
790
- function modeNote(before, requestedMode, mode) {
791
- if (before.kind !== "file") return "";
792
- if (before.mode !== mode) return `mode ${formatMode(before.mode)} changes to ${formatMode(mode)}\n`;
793
- if (requestedMode !== void 0 && requestedMode !== before.mode) return `mode ${formatMode(requestedMode)} requested; existing mode ${formatMode(before.mode)} is preserved\n`;
794
- return "";
795
- }
796
- function formatMode(mode) {
797
- return `0o${mode.toString(8).padStart(3, "0")}`;
798
- }
799
- function exceedsDiffBudget(before, after) {
800
- return Buffer.byteLength(before, "utf8") + Buffer.byteLength(after, "utf8") > MAX_DIFF_BYTES;
801
- }
802
- function textOf(state) {
803
- switch (state.kind) {
804
- case "directory":
805
- case "unsupported": return;
806
- case "file": return state.content !== void 0 && isUtf8(state.content) ? state.content.toString("utf8") : void 0;
807
- case "missing": return "";
808
- case "symlink": return `${state.target}\n`;
809
- }
810
- }
811
- //#endregion
812
- //#region ../core/src/fix-plan/write-redaction.ts
813
- const REDACTORS = /* @__PURE__ */ new WeakMap();
734
+ //#region ../core/src/content-hash.ts
735
+ /** Shape of every hash this module produces, for validators that must recognize one. */
736
+ const CONTENT_HASH_PATTERN = /^[0-9a-f]{64}$/u;
814
737
  /**
815
- * Paths some operation has asked to have redacted, kept beside the identity-keyed registry above.
738
+ * Normalizes Markdown to LF and exactly one trailing newline.
816
739
  *
817
- * The registry is keyed on the operation object, so anything that copies an operation between
818
- * planning and preview silently drops its masker and the symptom of that would be a credential
819
- * rendered into a diff rather than an error. Remembering the path as well turns the silent case
820
- * into the conservative one: a write to a path that has ever needed masking and arrives without a
821
- * masker gets a summary, not a patch.
740
+ * Line endings and trailing blank lines are what a checkout, an editor, or an append seam change
741
+ * without changing what the text says, so a hash that counted them would report drift on every
742
+ * machine that rewrote them — and a fragment appended in this form is the same text the hash
743
+ * covers. Trailing newlines are trimmed by scanning rather than with `/\n+$/`, whose backtracking
744
+ * is quadratic when a long newline run does not reach the end of the string.
822
745
  */
823
- const REDACTED_PATHS = /* @__PURE__ */ new Set();
824
- /** Associates a semantic content masker with a write without changing the public operation schema. */
825
- function rememberWriteRedactor(operation, redactor) {
826
- const existing = REDACTORS.get(operation) ?? [];
827
- REDACTORS.set(operation, [...existing, redactor]);
828
- REDACTED_PATHS.add(operation.path);
746
+ function canonicalizeContent(content) {
747
+ const normalized = content.replace(/\r\n?/gu, "\n");
748
+ let end = normalized.length;
749
+ while (end > 0 && normalized[end - 1] === "\n") end -= 1;
750
+ return `${normalized.slice(0, end)}\n`;
829
751
  }
830
752
  /**
831
- * Wraps an adapter's transform so a projection that could not account for every field fails closed.
753
+ * Fingerprints Markdown Aura records but does not own: an installed snippet, a trusted preset.
832
754
  *
833
- * `unresolved` names the fields whose server entry this content does not contain. That is not the
834
- * same as "nothing to mask": a rewritten side legitimately has nothing left to mask and reports no
835
- * unresolved fields, while a document shaped in a way the adapter cannot navigate reports them all
836
- * and still holds the credential.
755
+ * One function for both because the two records answer the same question "is this still the text
756
+ * the user accepted?" and two spellings of the canonical form would answer it differently on the
757
+ * first file whose line endings a checkout rewrote.
837
758
  */
838
- function mcpSecretRedactor(transform, sightings) {
839
- return (content) => {
840
- const redaction = transform.redact({
841
- content,
842
- sightings
843
- });
844
- return redaction === void 0 || redaction.unresolved.length > 0 ? void 0 : redaction.content;
845
- };
846
- }
847
- /** Renders a write only after every registered semantic masker succeeds on both diff sides. */
848
- function renderRedactedWriteDiff(operations, path, before, content, requestedMode, mode) {
849
- const redactors = operations.flatMap((operation) => REDACTORS.get(operation) ?? []);
850
- if (redactors.length === 0) return REDACTED_PATHS.has(path) ? renderWriteSummary(path, before, requestedMode, mode) : renderWriteDiff(path, before, content, requestedMode, mode);
851
- const next = redactAll(redactors, content);
852
- if (next === void 0) return renderWriteSummary(path, before, requestedMode, mode);
853
- if (before.kind !== "file" || before.content === void 0) return renderWriteDiff(path, before, next, requestedMode, mode);
854
- const previous = redactAll(redactors, before.content.toString("utf8"));
855
- if (previous === void 0) return renderWriteSummary(path, before, requestedMode, mode);
856
- return renderWriteDiff(path, {
857
- ...before,
858
- content: Buffer.from(previous, "utf8")
859
- }, next, requestedMode, mode);
860
- }
861
- function redactAll(redactors, content) {
862
- let projected = content;
863
- for (const redact of redactors) {
864
- const result = safeRedact(redact, projected);
865
- if (result === void 0) return;
866
- projected = result;
867
- }
868
- return projected;
759
+ function hashContent(content) {
760
+ return createHash("sha256").update(canonicalizeContent(content), "utf8").digest("hex");
869
761
  }
870
- function safeRedact(redact, content) {
871
- try {
872
- return redact(content);
873
- } catch {
874
- return;
875
- }
876
- }
877
- //#endregion
878
- //#region ../core/src/managed-block/protocol.ts
879
- /** Distribution-independent outer marker opening Aura-managed content. */
880
- const AURA_MANAGED_BLOCK_BEGIN = "<!-- aura:begin -->";
881
- /** Distribution-independent outer marker closing Aura-managed content. */
882
- const AURA_MANAGED_BLOCK_END = "<!-- aura:end -->";
883
- /** Prefix shared by every Aura snippet opening marker. */
884
- const AURA_MANAGED_SNIPPET_BEGIN_PREFIX = "<!-- aura:begin id=";
885
- /** Prefix shared by every Aura snippet closing marker. */
886
- const AURA_MANAGED_SNIPPET_END_PREFIX = "<!-- aura:end id=";
887
- /** Human-facing ownership warning rendered once inside the outer block. */
888
- const AURA_MANAGED_BLOCK_NOTICE = "Managed by Aura. Edit via the Aura CLI; manual edits to this block are overwritten.";
889
762
  const MANAGED_SNIPPET_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/;
890
- const MANAGED_SNIPPET_HASH_PATTERN = /^[0-9a-f]{64}$/;
763
+ const MANAGED_SNIPPET_HASH_PATTERN = CONTENT_HASH_PATTERN;
891
764
  /** Whether an id is both line-safe and legal inside an HTML comment. */
892
765
  function isManagedSnippetId(id) {
893
766
  return MANAGED_SNIPPET_ID_PATTERN.test(id) && !id.includes("--");
894
767
  }
895
768
  /**
896
- * Normalizes snippet text to the bytes covered by the managed-block hash protocol.
769
+ * Computes the protocol hash for a snippet's canonical UTF-8 contents.
897
770
  *
898
- * Trailing newlines are trimmed by scanning rather than with `/\n+$/`, whose backtracking is
899
- * quadratic when a long newline run does not reach the end of the string.
771
+ * The legacy block's hash is {@link hashContent} under another name: a marked section read back
772
+ * from an older file has to compare equal to the same text hashed anywhere else in Aura, or a
773
+ * migration would report drift on content nobody touched.
900
774
  */
901
- function canonicalizeManagedSnippet(content) {
902
- const normalized = content.replace(/\r\n?/g, "\n");
903
- let end = normalized.length;
904
- while (end > 0 && normalized[end - 1] === "\n") end -= 1;
905
- return `${normalized.slice(0, end)}\n`;
906
- }
907
- /** Computes the protocol hash for text already in {@link canonicalizeManagedSnippet} form. */
908
- function hashCanonicalManagedSnippet(canonical) {
909
- return createHash("sha256").update(canonical, "utf8").digest("hex");
910
- }
911
- /** Computes the protocol hash for a snippet's canonical UTF-8 contents. */
912
775
  function hashManagedSnippet(content) {
913
- return hashCanonicalManagedSnippet(canonicalizeManagedSnippet(content));
776
+ return hashContent(content);
914
777
  }
915
778
  //#endregion
916
779
  //#region ../core/src/managed-block/parse-state.ts
@@ -1132,324 +995,23 @@ function collectPlainLine(source, line, protectedByFence, block, snippet, notes)
1132
995
  }));
1133
996
  }
1134
997
  //#endregion
1135
- //#region ../core/src/managed-block/scan.ts
1136
- /**
1137
- * Finds protocol markers the reader would honour, applying the same fence rules as
1138
- * {@link readManagedBlock} so fenced marker examples stay ordinary text.
1139
- */
1140
- function scanForMarkers(text) {
1141
- let fence;
1142
- let markerLine;
1143
- for (const line of splitSourceLines$1(text)) {
1144
- const previousFence = fence;
1145
- fence = advanceMarkdownFence(line.text, fence);
1146
- if (previousFence !== void 0 || fence !== void 0) continue;
1147
- if (markerLine === void 0 && parseMarker(line.text) !== void 0) markerLine = line.number;
1148
- }
1149
- return {
1150
- markerLine,
1151
- unterminatedFence: fence !== void 0
1152
- };
1153
- }
998
+ //#region ../core/src/managed-block/strip.ts
1154
999
  /**
1155
- * Rejects content that would escape its own snippet. Written verbatim, a marker inside a snippet
1156
- * body reopens or closes the protocol, and the resulting file parses as invalid forever — so the
1157
- * damage has to be caught before the write, not after.
1000
+ * Removes a legacy Aura-managed block from a source string, keeping everything else byte-for-byte.
1158
1001
  *
1159
- * Every write path shares this guard. A narrower path that skipped it would be a way to write the
1160
- * exact bytes the wider one refuses, and the file it corrupts is the same file either way.
1161
- */
1162
- function managedSnippetContentProblems(id, canonical) {
1163
- const scan = scanForMarkers(canonical);
1164
- if (scan.markerLine !== void 0) return Object.freeze([Object.freeze({
1165
- code: "invalid-snippet-content",
1166
- message: `Snippet "${id}" declares an Aura marker on content line ${String(scan.markerLine)}. Wrap marker examples in a Markdown fence.`
1167
- })]);
1168
- if (scan.unterminatedFence) return Object.freeze([Object.freeze({
1169
- code: "invalid-snippet-content",
1170
- message: `Snippet "${id}" ends inside an unclosed Markdown fence, which would hide every marker after it. Close the fence.`
1171
- })]);
1172
- return Object.freeze([]);
1173
- }
1174
- /**
1175
- * Drops every protocol line the reader honours, plus the notice that follows an opening marker,
1176
- * leaving handwritten text byte-for-byte. Fenced marker examples survive because the reader never
1177
- * treated them as protocol in the first place.
1178
- */
1179
- function stripManagedMarkers(source) {
1180
- const kept = [];
1181
- let fence;
1182
- let afterBlockBegin = false;
1183
- for (const line of splitSourceLines$1(source)) {
1184
- const previousFence = fence;
1185
- fence = advanceMarkdownFence(line.text, fence);
1186
- const marker = previousFence !== void 0 || fence !== void 0 ? void 0 : parseMarker(line.text);
1187
- if (marker !== void 0) {
1188
- afterBlockBegin = marker.kind === "block-begin";
1189
- continue;
1190
- }
1191
- if (afterBlockBegin && line.text === "Managed by Aura. Edit via the Aura CLI; manual edits to this block are overwritten.") {
1192
- afterBlockBegin = false;
1193
- continue;
1194
- }
1195
- afterBlockBegin = false;
1196
- kept.push(source.slice(line.start, line.end));
1197
- }
1198
- return kept.join("");
1199
- }
1200
- //#endregion
1201
- //#region ../core/src/managed-block/reconcile-snippet.ts
1202
- /** Reconciles exactly one snippet while preserving every other source byte. */
1203
- function reconcileManagedSnippet(source, snippetId, resolution) {
1204
- const current = readManagedBlock(source);
1205
- if (current.status === "invalid") return invalidResult$1(source, current.notes, current.problems);
1206
- if (current.status === "absent") return missingSnippet(source, current.notes, snippetId, "an Aura-managed block");
1207
- const snippet = current.block.snippets.find((candidate) => candidate.id === snippetId);
1208
- if (snippet === void 0) return missingSnippet(source, current.notes, snippetId, "the Aura-managed block");
1209
- const lineEnding = markerLineEnding(source, snippet.startOffset, snippet.contentStartOffset);
1210
- const canonical = resolution.kind === "restore" ? canonicalizeManagedSnippet(resolution.content) : void 0;
1211
- if (canonical !== void 0) {
1212
- const problems = managedSnippetContentProblems(snippet.id, canonical);
1213
- if (problems.length > 0) return invalidResult$1(source, current.notes, problems);
1214
- }
1215
- const content = canonical === void 0 ? snippet.content : withLineEnding(canonical, lineEnding);
1216
- const hash = canonical === void 0 ? snippet.computedHash : hashCanonicalManagedSnippet(canonical);
1217
- const opening = `${AURA_MANAGED_SNIPPET_BEGIN_PREFIX}${snippet.id} sha256=${hash} -->${lineEnding}`;
1218
- const updated = source.slice(0, snippet.startOffset) + opening + content + source.slice(snippet.contentEndOffset);
1219
- return updated === source ? Object.freeze({
1220
- content: source,
1221
- notes: current.notes,
1222
- status: "unchanged"
1223
- }) : Object.freeze({
1224
- content: updated,
1225
- notes: current.notes,
1226
- status: "updated"
1227
- });
1228
- }
1229
- /** Creates the opt-in edited-versus-canonical detail used by the merge choice. */
1230
- function diffManagedSnippet(path, editedContent, canonicalContent) {
1231
- return createTwoFilesPatch(`${path} (edited)`, `${path} (canonical)`, canonicalizeManagedSnippet(editedContent), canonicalizeManagedSnippet(canonicalContent), "edited", "canonical", { context: 3 });
1232
- }
1233
- function missingSnippet(source, notes, snippetId, location) {
1234
- return invalidResult$1(source, notes, [Object.freeze({
1235
- code: "missing-snippet",
1236
- message: `Snippet "${snippetId}" does not exist in ${location}.`
1237
- })]);
1238
- }
1239
- function invalidResult$1(source, notes, problems) {
1240
- return Object.freeze({
1241
- content: source,
1242
- notes,
1243
- problems,
1244
- status: "invalid"
1245
- });
1246
- }
1247
- function markerLineEnding(source, startOffset, contentStartOffset) {
1248
- return source.slice(startOffset, contentStartOffset).endsWith("\r\n") ? "\r\n" : "\n";
1249
- }
1250
- function withLineEnding(content, lineEnding) {
1251
- return lineEnding === "\n" ? content : content.replaceAll("\n", lineEnding);
1252
- }
1253
- //#endregion
1254
- //#region ../core/src/managed-block/reconcile-desired.ts
1255
- function prepareDesiredSnippets(snippets, options) {
1256
- const ids = /* @__PURE__ */ new Set();
1257
- const preserved = new Set(options.preserveSnippetIds ?? []);
1258
- const prepared = [];
1259
- const problems = [];
1260
- for (const snippet of snippets) {
1261
- if (!isManagedSnippetId(snippet.id)) problems.push(Object.freeze({
1262
- code: "invalid-snippet-id",
1263
- message: `Snippet ID "${snippet.id}" is not safe inside an HTML comment marker.`
1264
- }));
1265
- if (ids.has(snippet.id)) problems.push(Object.freeze({
1266
- code: "duplicate-snippet",
1267
- message: `Desired snippet ID "${snippet.id}" appears more than once.`
1268
- }));
1269
- if (preserved.has(snippet.id)) problems.push(Object.freeze({
1270
- code: "duplicate-snippet",
1271
- message: `Snippet ID "${snippet.id}" is both desired and preserved; writing both would duplicate it.`
1272
- }));
1273
- ids.add(snippet.id);
1274
- const canonical = canonicalizeManagedSnippet(snippet.content);
1275
- problems.push(...managedSnippetContentProblems(snippet.id, canonical));
1276
- prepared.push({
1277
- canonical,
1278
- hash: hashCanonicalManagedSnippet(canonical),
1279
- id: snippet.id,
1280
- kind: "desired"
1281
- });
1282
- }
1283
- return {
1284
- prepared,
1285
- problems: Object.freeze(problems)
1286
- };
1287
- }
1288
- //#endregion
1289
- //#region ../core/src/managed-block/reconcile-ledger.ts
1290
- function renderLedgerSnippets(source, block, desired, options) {
1291
- if (block === void 0 || options.ownedSnippetIds === void 0) return desired;
1292
- const controlled = controlledSnippetIds(desired, options);
1293
- const preserved = new Set(options.preserveSnippetIds ?? []);
1294
- const remaining = [...desired];
1295
- const rendered = [];
1296
- for (const snippet of block.snippets) {
1297
- if (preserved.has(snippet.id) || !controlled.has(snippet.id)) {
1298
- rendered.push({
1299
- id: snippet.id,
1300
- kind: "preserved",
1301
- raw: source.slice(snippet.startOffset, snippet.endOffset)
1302
- });
1303
- continue;
1304
- }
1305
- const replacement = remaining.shift();
1306
- if (replacement !== void 0) rendered.push(replacement);
1307
- }
1308
- rendered.push(...remaining);
1309
- return rendered;
1310
- }
1311
- function controlledSnippetIds(desired, options) {
1312
- return /* @__PURE__ */ new Set([...options.ownedSnippetIds ?? [], ...desired.map((snippet) => snippet.id)]);
1313
- }
1314
- function preservedUnownedNotes(block, desired, options) {
1315
- if (block === void 0 || options.ownedSnippetIds === void 0) return [];
1316
- const controlled = controlledSnippetIds(desired, options);
1317
- return block.snippets.filter((snippet) => !controlled.has(snippet.id)).map((snippet) => Object.freeze({
1318
- code: "preserved-unowned-snippet",
1319
- line: snippet.startLine,
1320
- message: `Snippet "${snippet.id}" is not recorded in Aura's manifest; its section was preserved.`
1321
- }));
1322
- }
1323
- //#endregion
1324
- //#region ../core/src/managed-block/reconcile.ts
1325
- /**
1326
- * Reconciles one source string against the complete ordered set of desired snippets.
1002
+ * For a consumer deciding what a file's *user-authored* content is instruction consolidation
1003
+ * above all the managed block is Aura's own artifact: merging it elsewhere plants links and
1004
+ * legacy ledger sections in files that must not carry them. Unmanaged lines found inside the block
1005
+ * are user text and are kept.
1327
1006
  *
1328
- * Existing hash mismatches remain observable through {@link readManagedBlock}, but reconciliation
1329
- * deliberately replaces managed content with the desired canonical version, reporting each
1330
- * discarded hand edit as an `overwritten-snippet` note. Invalid structures fail closed and return
1331
- * the original source unchanged unless the caller opts into `onInvalid: "repair"`.
1007
+ * A source with no block, and one whose block does not parse, are returned unchanged: a malformed
1008
+ * block cannot be attributed to Aura with confidence, and dropping bytes on that guess would lose
1009
+ * user content.
1332
1010
  */
1333
- function reconcileManagedBlock(source, desiredSnippets, options = {}) {
1334
- return reconcileParsedManagedBlock(source, readManagedBlock(source), desiredSnippets, options);
1335
- }
1336
- /**
1337
- * {@link reconcileManagedBlock}, for a caller that already parsed `source`.
1338
- *
1339
- * `current` must be `readManagedBlock(source)` for the same string; passing a result read from
1340
- * anything else splices content at offsets that no longer describe the source.
1341
- */
1342
- function reconcileParsedManagedBlock(source, current, desiredSnippets, options = {}) {
1343
- const desired = prepareDesiredSnippets(desiredSnippets, options);
1344
- if (desired.problems.length > 0) return invalidResult(source, current.notes, desired.problems);
1345
- const hiddenMarker = current.notes.find((note) => note.code === "unterminated-fence");
1346
- if (hiddenMarker !== void 0) return invalidResult(source, current.notes, [Object.freeze({
1347
- code: "unterminated-fence",
1348
- line: hiddenMarker.line,
1349
- message: hiddenMarker.message
1350
- })]);
1351
- if (current.status !== "invalid") {
1352
- const block = current.status === "present" ? current.block : void 0;
1353
- const rendered = renderLedgerSnippets(source, block, desired.prepared, options);
1354
- const notes = [
1355
- ...current.notes,
1356
- ...overwrittenNotes(current, desired.prepared, options),
1357
- ...preservedUnownedNotes(block, desired.prepared, options)
1358
- ];
1359
- return settle(source, buildContent(source, rendered, current), notes);
1360
- }
1361
- if (options.onInvalid !== "repair") return invalidResult(source, current.notes, current.problems);
1362
- if (options.ownedSnippetIds !== void 0) return invalidResult(source, current.notes, current.problems);
1363
- const stripped = stripManagedMarkers(source);
1364
- const repaired = readManagedBlock(stripped);
1365
- if (repaired.status === "invalid") return invalidResult(source, current.notes, current.problems);
1366
- const notes = [...current.notes, ...repairNotes(current.problems)];
1367
- return settle(source, buildContent(stripped, desired.prepared, repaired), notes);
1368
- }
1369
- function buildContent(source, prepared, current) {
1370
- if (prepared.length === 0) return current.status === "absent" ? source : source.slice(0, current.block.startOffset) + current.block.unmanagedContent + source.slice(current.block.endOffset);
1371
- const lineEnding = detectLineEnding$1(source);
1372
- if (current.status === "absent") return `${source}${source.length === 0 || source.endsWith("\n") ? "" : lineEnding}${renderManagedBlock(prepared, lineEnding, true)}`;
1373
- const replacement = renderManagedBlock(prepared, lineEnding, source[current.block.endOffset - 1] === "\n" || current.block.unmanagedContent.length > 0);
1374
- if (current.block.unmanagedContent.length === 0 && replacement === source.slice(current.block.startOffset, current.block.endOffset)) return source;
1375
- return source.slice(0, current.block.startOffset) + replacement + current.block.unmanagedContent + source.slice(current.block.endOffset);
1376
- }
1377
- function renderManagedBlock(snippets, lineEnding, endsWithLineEnding) {
1378
- const parts = [
1379
- AURA_MANAGED_BLOCK_BEGIN,
1380
- lineEnding,
1381
- AURA_MANAGED_BLOCK_NOTICE,
1382
- lineEnding
1383
- ];
1384
- for (const snippet of snippets) {
1385
- if (snippet.kind === "preserved") {
1386
- parts.push(snippet.raw);
1387
- continue;
1388
- }
1389
- const content = lineEnding === "\n" ? snippet.canonical : snippet.canonical.replaceAll("\n", lineEnding);
1390
- parts.push(`${AURA_MANAGED_SNIPPET_BEGIN_PREFIX}${snippet.id} sha256=${snippet.hash} -->`, lineEnding, content, `${AURA_MANAGED_SNIPPET_END_PREFIX}${snippet.id} -->`, lineEnding);
1391
- }
1392
- parts.push(AURA_MANAGED_BLOCK_END);
1393
- if (endsWithLineEnding) parts.push(lineEnding);
1394
- return parts.join("");
1395
- }
1396
- function overwrittenNotes(current, desired, options) {
1397
- if (current.status === "absent") return [];
1398
- const controlled = controlledSnippetIds(desired, options);
1399
- const desiredById = new Map(desired.map((snippet) => [snippet.id, snippet]));
1400
- const preserved = new Set(options.preserveSnippetIds ?? []);
1401
- return current.block.snippets.flatMap((snippet) => {
1402
- if (options.ownedSnippetIds !== void 0 && !controlled.has(snippet.id) || preserved.has(snippet.id) || !handEdited(snippet, options)) return [];
1403
- const replacement = desiredById.get(snippet.id);
1404
- if (replacement === void 0) return [Object.freeze({
1405
- code: "removed-snippet",
1406
- line: snippet.startLine,
1407
- message: `Snippet "${snippet.id}" was edited by hand since Aura wrote it; the edit is being removed.`
1408
- })];
1409
- if (replacement.hash === snippet.computedHash) return [];
1410
- return [Object.freeze({
1411
- code: "overwritten-snippet",
1412
- line: snippet.startLine,
1413
- message: `Snippet "${snippet.id}" was edited by hand since Aura wrote it; the edit is being replaced.`
1414
- })];
1415
- });
1416
- }
1417
- /**
1418
- * Whether a section differs from what Aura last recorded for it.
1419
- *
1420
- * The marker hash is written by whoever wrote the marker, so a re-stamped section certifies itself
1421
- * and `hashMatches` alone cannot tell a hand edit from a catalog upgrade. The manifest is the only
1422
- * record the editor did not control; fall back to the marker only when there is no manifest entry.
1423
- */
1424
- function handEdited(snippet, options) {
1425
- const previousHash = options.previousSnippetHashes?.get(snippet.id);
1426
- return previousHash === void 0 ? !snippet.hashMatches : previousHash !== snippet.computedHash;
1427
- }
1428
- function repairNotes(problems) {
1429
- return problems.map((problem) => Object.freeze({
1430
- code: "repaired-invalid-block",
1431
- line: problem.line,
1432
- message: `Rebuilt the managed block to repair: ${problem.message}`
1433
- }));
1434
- }
1435
- function invalidResult(source, notes, problems) {
1436
- return Object.freeze({
1437
- content: source,
1438
- notes: Object.freeze([...notes]),
1439
- problems,
1440
- status: "invalid"
1441
- });
1442
- }
1443
- function settle(source, content, notes) {
1444
- return content === source ? Object.freeze({
1445
- content: source,
1446
- notes: Object.freeze([...notes]),
1447
- status: "unchanged"
1448
- }) : Object.freeze({
1449
- content,
1450
- notes: Object.freeze([...notes]),
1451
- status: "updated"
1452
- });
1011
+ function stripLegacyManagedBlock(source) {
1012
+ const parsed = readManagedBlock(source);
1013
+ if (parsed.status !== "present") return source;
1014
+ return source.slice(0, parsed.block.startOffset) + parsed.block.unmanagedContent + source.slice(parsed.block.endOffset);
1453
1015
  }
1454
1016
  //#endregion
1455
1017
  //#region ../core/src/managed-content/revision.ts
@@ -1580,210 +1142,6 @@ function convergedPlan$1(app, skillId) {
1580
1142
  };
1581
1143
  }
1582
1144
  //#endregion
1583
- //#region ../core/src/workspace/mcp-classify.ts
1584
- /**
1585
- * Splits desired servers into the ones Aura may write and the collisions a person has to settle.
1586
- *
1587
- * Scope is part of identity here. A server named `docs` in a project `.mcp.json` is not the `docs`
1588
- * the manifest wants in user-level configuration, and treating them as one either blocks a write
1589
- * that would not have collided or skips one that never happened.
1590
- */
1591
- function classifyDesired(desired, ledgerNames, state) {
1592
- const ledger = new Set(ledgerNames);
1593
- const owned = [];
1594
- const blockers = [];
1595
- for (const entry of desired) {
1596
- const blocker = collisionBlocker(entry, ledger, state);
1597
- if (blocker === "owned") owned.push(entry);
1598
- else if (blocker !== void 0) blockers.push(blocker);
1599
- }
1600
- return {
1601
- blockers,
1602
- owned
1603
- };
1604
- }
1605
- /** `owned` to write it, a blocker to refuse, `undefined` when the config already satisfies it. */
1606
- function collisionBlocker(entry, ledger, state) {
1607
- if (ledger.has(entry.name)) return "owned";
1608
- const sameName = (candidate) => candidate.name === entry.name && candidate.scope === entry.scope;
1609
- const unusable = state.unusable.find(sameName);
1610
- if (unusable !== void 0) return {
1611
- 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.`,
1612
- scope: entry.scope,
1613
- sourceId: unusable.sourceId
1614
- };
1615
- const existing = state.servers.filter(sameName);
1616
- if (existing.length === 0) return "owned";
1617
- const normalized = normalizeDesired(entry);
1618
- if ("message" in normalized) return normalized;
1619
- return existing.every((server) => isDeepStrictEqual(server.transport, normalized.transport)) ? void 0 : {
1620
- message: `MCP server ${entry.name} already exists outside Aura's ownership ledger and differs from the manifest.`,
1621
- scope: entry.scope
1622
- };
1623
- }
1624
- /**
1625
- * Normalizes one desired transport, reporting a manifest Aura refuses to write as a blocker.
1626
- *
1627
- * The manifest is a file a person can edit. One that has acquired a credential literal is not a
1628
- * crash in a check's `detect`; it is something to say out loud and decline to propagate.
1629
- */
1630
- function normalizeDesired(entry) {
1631
- try {
1632
- return { transport: normalizeMcpServerDefinition(entry.transport) };
1633
- } catch (error) {
1634
- return {
1635
- 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.`,
1636
- scope: entry.scope
1637
- };
1638
- }
1639
- }
1640
- //#endregion
1641
- //#region ../core/src/workspace/mcp-convergence.ts
1642
- /** Captures adapter source bytes behind a function, keeping them out of the model entirely. */
1643
- function createAppMcpConvergence(adapter, files, state) {
1644
- const writer = adapter.mcpWrite;
1645
- if (writer === void 0) return;
1646
- const targets = [...files.values()].filter((file) => file.spec.kind === "mcp");
1647
- return (desired, ledgerNames) => {
1648
- const classified = classifyDesired(desired, ledgerNames, state);
1649
- if (classified.blockers.length > 0) return blocked(classified.blockers);
1650
- const scopes = ["global", "project"].map((scope) => planScope(adapter, writer, targets, classified.owned, ledgerNames, scope, state.secretSightings));
1651
- const blockers = scopes.flatMap((result) => result.blockers);
1652
- const operations = scopes.flatMap((result) => result.operations);
1653
- return blockers.length > 0 ? blocked(blockers) : {
1654
- blockers: [],
1655
- operations,
1656
- ownedNames: [...new Set(classified.owned.map((entry) => entry.name))].sort()
1657
- };
1658
- };
1659
- }
1660
- function planScope(adapter, writer, targets, desired, ledgerNames, scope, sightings) {
1661
- const scopedDesired = desired.filter((entry) => entry.scope === scope);
1662
- const scopedTargets = targets.filter((target) => target.spec.scope === scope);
1663
- if (scopedDesired.length > 0 && scopedTargets.length === 0) return blockedScope(`${adapter.displayName} has no ${scope}-scope MCP configuration target.`, scope);
1664
- if (scopedTargets.length > 1) return blockedScope(`${adapter.displayName} declares more than one ${scope}-scope MCP write target.`, scope);
1665
- const target = scopedTargets[0];
1666
- return target === void 0 ? {
1667
- blockers: [],
1668
- operations: []
1669
- } : writeScopeTarget(adapter, writer, target, scopedDesired, ledgerNames, scope, sightings);
1670
- }
1671
- function writeScopeTarget(adapter, writer, target, desired, ledgerNames, scope, sightings) {
1672
- if (!needsTargetWrite(target, desired, ledgerNames)) return {
1673
- blockers: [],
1674
- operations: []
1675
- };
1676
- const unwritable = targetRefusal(target);
1677
- if (unwritable !== void 0) return {
1678
- blockers: [{
1679
- ...unwritable,
1680
- scope,
1681
- sourceId: target.spec.id
1682
- }],
1683
- operations: []
1684
- };
1685
- const written = runWriter(writer, target, desired, ledgerNames);
1686
- if ("refusal" in written) return {
1687
- blockers: [{
1688
- message: written.refusal,
1689
- path: target.spec.path,
1690
- scope,
1691
- sourceId: target.spec.id
1692
- }],
1693
- operations: []
1694
- };
1695
- if (Buffer.byteLength(written.content, "utf8") > 4194304) return {
1696
- blockers: [{
1697
- message: `${adapter.displayName}'s MCP configuration at ${target.spec.path} is larger than Aura will rewrite in one operation, so Aura left it unchanged.`,
1698
- path: target.spec.path,
1699
- scope,
1700
- sourceId: target.spec.id
1701
- }],
1702
- operations: []
1703
- };
1704
- const operation = {
1705
- content: written.content,
1706
- mode: scope === "global" ? 384 : 420,
1707
- path: target.spec.path,
1708
- precondition: targetPrecondition(target),
1709
- type: "write"
1710
- };
1711
- rememberMcpRedactor(adapter, operation, target, sightings);
1712
- return {
1713
- blockers: [],
1714
- operations: [operation]
1715
- };
1716
- }
1717
- /**
1718
- * Registers preview masking only for a target that actually holds an inline credential.
1719
- *
1720
- * Registering unconditionally would cost every convergence preview its diff, because masking has
1721
- * no previous side to project when the fix is creating the file — which is what most MCP-001 fixes
1722
- * do, on a file that by definition has no credentials in it yet.
1723
- */
1724
- function rememberMcpRedactor(adapter, operation, target, sightings) {
1725
- const transform = adapter.mcpSecrets;
1726
- const scoped = sightings.filter((sighting) => sighting.sourceId === target.spec.id);
1727
- if (transform === void 0 || scoped.length === 0) return;
1728
- rememberWriteRedactor(operation, mcpSecretRedactor(transform, scoped));
1729
- }
1730
- /** Runs a plugin serializer, treating a thrown error as the refusal it forgot to return. */
1731
- function runWriter(writer, target, desired, ledgerNames) {
1732
- try {
1733
- return writer({
1734
- desired,
1735
- ...target.content === void 0 ? {} : { existingContent: target.content },
1736
- ledgerNames
1737
- });
1738
- } catch {
1739
- return { refusal: "The MCP configuration serializer failed, so Aura left the file unchanged." };
1740
- }
1741
- }
1742
- /**
1743
- * What the file must still look like when the plan is applied.
1744
- *
1745
- * The rendered content is this file's own previous bytes with one section replaced, so applying it
1746
- * against a file the application has since rewritten would silently revert that work. Declaring
1747
- * what was read turns the race into a reported conflict.
1748
- */
1749
- function targetPrecondition(target) {
1750
- return target.content === void 0 ? { kind: "absent" } : {
1751
- digest: createHash("sha256").update(target.content, "utf8").digest("hex"),
1752
- kind: "sha256"
1753
- };
1754
- }
1755
- function needsTargetWrite(target, desired, ledgerNames) {
1756
- return (desired.length > 0 || ledgerNames.length > 0) && (target.exists || desired.length > 0);
1757
- }
1758
- /** Why this target cannot be rewritten safely, before a serializer is asked to try. */
1759
- function targetRefusal(target) {
1760
- const path = target.spec.path;
1761
- if (target.problem !== void 0 || target.exists && target.content === void 0) return {
1762
- message: `MCP configuration at ${path} could not be read safely, so Aura left it unchanged.`,
1763
- path
1764
- };
1765
- return (target.size ?? 0) > 4194304 ? {
1766
- message: `MCP configuration at ${path} is larger than Aura will rewrite in one operation, so Aura left it unchanged.`,
1767
- path
1768
- } : void 0;
1769
- }
1770
- function blockedScope(message, scope) {
1771
- return {
1772
- blockers: [{
1773
- message,
1774
- scope
1775
- }],
1776
- operations: []
1777
- };
1778
- }
1779
- function blocked(blockers) {
1780
- return {
1781
- blockers,
1782
- operations: [],
1783
- ownedNames: []
1784
- };
1785
- }
1786
- //#endregion
1787
1145
  //#region ../core/src/workspace/mcp-plan-manifest.ts
1788
1146
  /**
1789
1147
  * Merges preset-required servers under the manifest's own, keyed by scope and name.
@@ -1842,12 +1200,16 @@ function withoutServer(manifest, server) {
1842
1200
  * association lives here instead of on the object: reaching a planner takes this module, which
1843
1201
  * plugin code cannot import.
1844
1202
  */
1845
- const PLANNERS$1 = /* @__PURE__ */ new WeakMap();
1203
+ const PLANNERS = /* @__PURE__ */ new WeakMap();
1846
1204
  /** Per-scan memo of what convergence planning produced, keyed by the model it was computed from. */
1847
- const PLANS$1 = /* @__PURE__ */ new WeakMap();
1205
+ const PLANS = /* @__PURE__ */ new WeakMap();
1848
1206
  /** Associates a planner with the app model core just built for it. */
1849
1207
  function rememberMcpConvergence(app, convergence) {
1850
- PLANNERS$1.set(app, convergence);
1208
+ PLANNERS.set(app, convergence);
1209
+ }
1210
+ /** Forgets one scan's memoized plans, after a refresh replaced the bytes they were built from. */
1211
+ function forgetMcpPlans(model) {
1212
+ PLANS.delete(model);
1851
1213
  }
1852
1214
  /**
1853
1215
  * Reports only why convergence is impossible, without rendering any file.
@@ -1861,8 +1223,8 @@ function mcpConvergenceBlockers(model, appId) {
1861
1223
  }
1862
1224
  /** Builds application writes and the ownership-ledger update as one atomic fix plan. */
1863
1225
  function planManifestMcpConvergence(model, appId) {
1864
- const memo = PLANS$1.get(model) ?? /* @__PURE__ */ new Map();
1865
- PLANS$1.set(model, memo);
1226
+ const memo = PLANS.get(model) ?? /* @__PURE__ */ new Map();
1227
+ PLANS.set(model, memo);
1866
1228
  const cached = memo.get(appId);
1867
1229
  if (cached !== void 0) return cached;
1868
1230
  const computed = computeConvergence(model, appId);
@@ -1978,103 +1340,53 @@ function ambiguousRemoval(model, server) {
1978
1340
  function resolvePlanner(model, appId) {
1979
1341
  const app = model.apps.find((candidate) => candidate.adapterId === appId);
1980
1342
  if (app === void 0) return { blockers: [{ message: `Application ${appId} was not detected.` }] };
1981
- const convergence = PLANNERS$1.get(app);
1343
+ const convergence = PLANNERS.get(app);
1982
1344
  return convergence === void 0 ? { blockers: [{ message: `${app.displayName}'s adapter cannot write MCP configuration.` }] } : {
1983
1345
  app,
1984
1346
  convergence
1985
1347
  };
1986
1348
  }
1987
1349
  //#endregion
1988
- //#region ../core/src/workspace/mcp-secret-plan.ts
1989
- const PLANNERS = /* @__PURE__ */ new WeakMap();
1990
- const PLANS = /* @__PURE__ */ new WeakMap();
1991
- /** Captures raw MCP bytes for remediation behind the same private boundary as convergence. */
1992
- function createAppMcpSecretPlanner(adapter, files, sightings) {
1993
- const transform = adapter.mcpSecrets;
1994
- if (transform === void 0) return;
1995
- return {
1996
- plan: (sourceId) => {
1997
- const target = files.get(sourceId);
1998
- const sourceSightings = sightings.filter((sighting) => sighting.sourceId === sourceId);
1999
- const supported = sourceSightings.filter(transform.supports);
2000
- if (target === void 0 || target.spec.kind !== "mcp" || target.content === void 0 || targetRefusal(target) !== void 0 || supported.length === 0) return;
2001
- let rewritten;
2002
- try {
2003
- rewritten = transform.rewrite({
2004
- content: target.content,
2005
- sightings: supported
2006
- });
2007
- } catch {
2008
- return;
2009
- }
2010
- if ("refusal" in rewritten || rewritten.rewrittenFields.length !== supported.length) return;
2011
- if (Buffer.byteLength(rewritten.content, "utf8") > 4194304) return;
2012
- const operation = {
2013
- content: rewritten.content,
2014
- mode: target.spec.scope === "global" ? 384 : 420,
2015
- path: target.spec.path,
2016
- precondition: targetPrecondition(target),
2017
- type: "write"
2018
- };
2019
- rememberWriteRedactor(operation, mcpSecretRedactor(transform, sourceSightings));
2020
- return {
2021
- manualSteps: remediationSteps(target.spec.path, supported),
2022
- operations: [operation],
2023
- summary: `Replace inline MCP credentials in ${target.spec.path} with environment references.`
2024
- };
2025
- },
2026
- supports: transform.supports
2027
- };
2028
- }
2029
- /** Associates a private planner with the app model that exposes only safe sightings. */
2030
- function rememberMcpSecretPlanner(app, planner) {
2031
- PLANNERS.set(app, planner);
2032
- }
2033
- /**
2034
- * Whether this occurrence has a behavior-preserving adapter rewrite that a plan can actually carry.
2035
- *
2036
- * `supports` only reads the locator's shape, so it answers "could this kind of field be rewritten",
2037
- * not "was this one". Planning is what discovers that the document is spelled in a form the writer
2038
- * cannot edit, and a finding that advertises a guided fix the planner then declines to produce is
2039
- * a dead end for the user. The plan is memoized, so asking it here costs nothing.
2040
- */
2041
- function canPlanMcpSecretRemediation(model, sighting) {
2042
- const app = model.apps.find((candidate) => candidate.adapterId === sighting.appId);
2043
- if ((app === void 0 ? void 0 : PLANNERS.get(app))?.supports(sighting) !== true) return false;
2044
- return planMcpSecretRemediation(model, sighting) !== void 0;
2045
- }
2046
- /** Builds the one whole-file plan shared by every representable sighting in a source file. */
2047
- function planMcpSecretRemediation(model, sighting) {
2048
- const key = `${sighting.appId}:${sighting.sourceId}`;
2049
- const memo = PLANS.get(model) ?? /* @__PURE__ */ new Map();
2050
- PLANS.set(model, memo);
2051
- if (memo.has(key)) return memo.get(key);
2052
- const app = model.apps.find((candidate) => candidate.adapterId === sighting.appId);
2053
- const plan = (app === void 0 ? void 0 : PLANNERS.get(app))?.plan(sighting.sourceId);
2054
- memo.set(key, plan);
2055
- return plan;
2056
- }
1350
+ //#region ../core/src/workspace/append-instructions.ts
2057
1351
  /**
2058
- * What the user has to do around the rewrite, in the order the rewrite makes them possible.
1352
+ * Appends Markdown fragments while preserving every existing byte.
2059
1353
  *
2060
- * Rotation leads the list because moving a credential out of the file does not undo where it has
2061
- * already been. A value that sat inline in configuration is a value that may have reached source
2062
- * control, a shell history, a log, or a copied snippet which is the reason this check exists, and
2063
- * the reason the same value put behind an environment reference is still the wrong value to keep.
1354
+ * Only appended fragments are canonicalized, and by the same normalization the content hash
1355
+ * covers that is what lets a recorded fingerprint answer for text already in the file. They use
1356
+ * the target's line ending and end in one line ending, with one blank-line seam between
1357
+ * consecutive fragments.
2064
1358
  */
2065
- function remediationSteps(path, sightings) {
2066
- const copies = sightings.map((sighting) => `Copy the current value for ${sighting.serverName}.${sighting.field} from ${path} now; Aura will replace it with ${sighting.suggestedEnvName}.`);
2067
- const exports = [...new Set(sightings.map((sighting) => sighting.suggestedEnvName))].sort().map((name) => `Run \`export ${name}=…\` in the environment that starts the MCP client.`);
2068
- return [
2069
- ...copies,
2070
- ...exports,
2071
- "Rotate each credential at its issuer: an inline value may already have been copied elsewhere.",
2072
- "Run `aura check` again."
2073
- ];
1359
+ function appendInstructionFragments(source, fragments) {
1360
+ if (fragments.length === 0) return source;
1361
+ const lineEnding = source.includes("\r\n") ? "\r\n" : "\n";
1362
+ let result = source;
1363
+ for (const fragment of fragments) {
1364
+ const canonical = canonicalizeContent(fragment).replaceAll("\n", lineEnding);
1365
+ if (result.length > 0) result += blankLineSeparator(result, lineEnding);
1366
+ result += canonical;
1367
+ }
1368
+ return result;
1369
+ }
1370
+ function blankLineSeparator(source, lineEnding) {
1371
+ let trailingLineEndings = 0;
1372
+ let end = source.length;
1373
+ while (end > 0 && trailingLineEndings < 2) {
1374
+ if (source.endsWith("\r\n", end)) {
1375
+ end -= 2;
1376
+ trailingLineEndings += 1;
1377
+ continue;
1378
+ }
1379
+ if (source[end - 1] === "\n" || source[end - 1] === "\r") {
1380
+ end -= 1;
1381
+ trailingLineEndings += 1;
1382
+ continue;
1383
+ }
1384
+ break;
1385
+ }
1386
+ return lineEnding.repeat(2 - trailingLineEndings);
2074
1387
  }
2075
1388
  //#endregion
2076
1389
  //#region ../core/src/workspace/shared-link-plan.ts
2077
- const SHARED_LINK_SNIPPET_ID = "shared-instructions";
2078
1390
  /** Builds the same safe shared-instruction link plan for checks and setup. */
2079
1391
  function planSharedInstructionLink(app, model, options = {}) {
2080
1392
  const link = options.link ?? app.sharedLink;
@@ -2083,21 +1395,24 @@ function planSharedInstructionLink(app, model, options = {}) {
2083
1395
  const refusal = refuseBefore(app, status);
2084
1396
  if (refusal !== void 0) return { blocked: refusal };
2085
1397
  switch (link.kind) {
2086
- case "import-line": return planImportLine(app, model, link, status, options.sourceContent);
2087
- case "native-copy": return planNativeCopy(app, model, link, status, options.sourceContent);
1398
+ case "import-line": return planImportLine(app, link, status, options.sourceContent);
1399
+ case "native-copy": return planNativeCopy(app, link, status, options.sourceContent);
2088
1400
  case "symlink": return planSymlink(app, model, link, status, options);
2089
1401
  }
2090
1402
  }
2091
- function planImportLine(app, model, link, status, sourceContent) {
1403
+ function planImportLine(app, link, status, sourceContent) {
2092
1404
  const source = instructionEntry(app, link.entryPath);
2093
1405
  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.` };
2094
- const reconciled = reconcileManagedBlock(selectedContent(sourceContent, source), [{
2095
- content: link.content ?? "",
2096
- id: SHARED_LINK_SNIPPET_ID
2097
- }]);
2098
- 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.` };
2099
- if (reconciled.status === "unchanged" && observedStateHolds(sourceContent)) return { plan: convergedPlan(app) };
2100
- return { plan: writePlan(app, link, reconciled.content, model.homeDir) };
1406
+ const current = selectedContent(sourceContent, source);
1407
+ const desired = link.content ?? "";
1408
+ if (observedStateHolds(sourceContent) && containsImport(current, desired)) return { plan: convergedPlan(app) };
1409
+ return { plan: writePlan(app, link, appendInstructionFragments(current, [desired])) };
1410
+ }
1411
+ /** Legacy marked imports and new plain imports both contain the exact adapter-rendered line. */
1412
+ function containsImport(source, desired) {
1413
+ const normalized = desired.replace(/\r\n?/gu, "\n");
1414
+ const normalizedDesired = normalized.endsWith("\n") ? normalized.slice(0, -1) : normalized;
1415
+ return source.replace(/\r\n?/gu, "\n").split("\n").some((line) => line === normalizedDesired);
2101
1416
  }
2102
1417
  /**
2103
1418
  * Whether what the scan saw still describes the file when this plan is applied.
@@ -2116,19 +1431,19 @@ function unreadableInstructionEntry(status, source, sourceContent) {
2116
1431
  function selectedContent(sourceContent, source) {
2117
1432
  return sourceContent ?? source?.content ?? "";
2118
1433
  }
2119
- function planNativeCopy(app, model, link, status, sourceContent) {
1434
+ function planNativeCopy(app, link, status, sourceContent) {
2120
1435
  const content = sourceContent ?? instructionEntry(app, link.entryPath)?.content;
2121
1436
  const refusal = nativeCopyRefusal(status, sourceContent, content, link.content);
2122
1437
  if (refusal !== void 0) return { blocked: refusal };
2123
1438
  if (status?.exists === true && content === link.content && observedStateHolds(sourceContent)) return { plan: convergedPlan(app) };
2124
- return { plan: writePlan(app, link, link.content ?? "", model.homeDir) };
1439
+ return { plan: writePlan(app, link, link.content ?? "") };
2125
1440
  }
2126
1441
  function nativeCopyRefusal(status, sourceContent, content, desired) {
2127
1442
  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;
2128
1443
  }
2129
1444
  function planSymlink(app, model, link, status, options) {
2130
- 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." };
2131
1445
  const target = options.symlinkTarget ?? model.sharedInstructions.path;
1446
+ 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." };
2132
1447
  if (observedStateHolds(options.sourceContent) && pointsAt(status, target)) return { plan: convergedPlan(app) };
2133
1448
  return { plan: {
2134
1449
  operations: [{
@@ -2162,9 +1477,16 @@ function refuseBefore(app, status) {
2162
1477
  if (status?.problem !== void 0) return `Aura could not safely read the instruction entry (${status.problem}) and will not overwrite it.`;
2163
1478
  return status?.pathKind === "directory" ? "The instruction entry is a directory and cannot be replaced automatically." : void 0;
2164
1479
  }
2165
- function writePlan(app, link, content, homeDir) {
1480
+ /**
1481
+ * Writes one entry, with nothing to warn the user about afterwards.
1482
+ *
1483
+ * There used to be a manual step here telling the user to keep a project entry out of version
1484
+ * control, because a project entry could name the shared source by absolute path. It cannot any
1485
+ * more: a project entry names it relatively, and a home entry through `~`. Both are committable or
1486
+ * personal by construction rather than by advice the user has to remember to follow.
1487
+ */
1488
+ function writePlan(app, link, content) {
2166
1489
  return {
2167
- ...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.`] } : {},
2168
1490
  operations: [{
2169
1491
  content,
2170
1492
  mode: 420,
@@ -2174,6 +1496,19 @@ function writePlan(app, link, content, homeDir) {
2174
1496
  summary: `Link ${app.displayName} to the shared instruction source.`
2175
1497
  };
2176
1498
  }
1499
+ /**
1500
+ * Whether a real file at this entry holds nothing but a legacy block or plain link Aura wrote.
1501
+ *
1502
+ * The refusal above guards the user's own guidance, and an entry Aura wired as an import line holds
1503
+ * none. Without this, an app switching from an import line to a link strands every machine already
1504
+ * wired the old way: consolidation has no source to offer for a file that is Aura's.
1505
+ */
1506
+ function holdsOnlySharedReference(app, path, target) {
1507
+ const document = instructionEntry(app, path);
1508
+ if (document === void 0) return false;
1509
+ if (stripLegacyManagedBlock(document.content).trim().length === 0) return true;
1510
+ 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));
1511
+ }
2177
1512
  function instructionEntry(app, path) {
2178
1513
  return app.instructionFiles.find((document) => resolve(document.path) === resolve(path));
2179
1514
  }
@@ -2181,4 +1516,4 @@ function entryStatus(app, path) {
2181
1516
  return app.sourceFiles.find((file) => resolve(file.spec.path) === resolve(path));
2182
1517
  }
2183
1518
  //#endregion
2184
- 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 };
1519
+ 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 };