@jpcasabiancai/cayenne 0.3.3 → 0.3.4

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.
Files changed (2) hide show
  1. package/dist/cli.js +752 -116
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -632,6 +632,48 @@ var init_content = __esm({
632
632
  }
633
633
  });
634
634
 
635
+ // ../../packages/shared/src/protocol/documents.ts
636
+ function documentFilename(relPath) {
637
+ const leaf = relPath.split(/[/\\]/).at(-1) ?? "";
638
+ const folded = leaf.replace(OUTSIDE_CLASS, "-");
639
+ const prefixed = /^[A-Za-z0-9]/.test(folded) ? folded : `d-${folded}`;
640
+ return prefixed.replace(DASH_RUN, "-").slice(0, MAX_FILENAME);
641
+ }
642
+ function documentKindFor(filename) {
643
+ const dot = filename.lastIndexOf(".");
644
+ const ext = dot === -1 ? "" : filename.slice(dot + 1).toLowerCase();
645
+ return KIND_BY_EXTENSION.get(ext) ?? "file";
646
+ }
647
+ function documentPath(workspaceId, documentId, filename) {
648
+ const path = `${workspaceId.toLowerCase()}/${documentId.toLowerCase()}/${filename}`;
649
+ if (!DOCUMENT_NAME.test(path)) {
650
+ throw new Error(`VALIDATION: not a valid document name: ${path}`);
651
+ }
652
+ return path;
653
+ }
654
+ var DOCUMENTS_BUCKET, DOCUMENT_MAX_BYTES, DOCUMENT_TITLE_MAX, DOCUMENTS_DIR, DOCUMENTS_MAX_FILES, DOCUMENT_REL_PATH_MAX, DOCUMENT_NAME, KIND_BY_EXTENSION, MAX_FILENAME, OUTSIDE_CLASS, DASH_RUN;
655
+ var init_documents = __esm({
656
+ "../../packages/shared/src/protocol/documents.ts"() {
657
+ "use strict";
658
+ DOCUMENTS_BUCKET = "documents";
659
+ DOCUMENT_MAX_BYTES = 2 * 1024 * 1024;
660
+ DOCUMENT_TITLE_MAX = 200;
661
+ DOCUMENTS_DIR = ".cayenne/docs";
662
+ DOCUMENTS_MAX_FILES = 200;
663
+ DOCUMENT_REL_PATH_MAX = 1024;
664
+ DOCUMENT_NAME = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\/[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/;
665
+ KIND_BY_EXTENSION = /* @__PURE__ */ new Map([
666
+ ["md", "markdown"],
667
+ ["markdown", "markdown"],
668
+ ["html", "html"],
669
+ ["htm", "html"]
670
+ ]);
671
+ MAX_FILENAME = 200;
672
+ OUTSIDE_CLASS = /[^A-Za-z0-9._-]/g;
673
+ DASH_RUN = /-{2,}/g;
674
+ }
675
+ });
676
+
635
677
  // ../../packages/shared/src/protocol/events.ts
636
678
  import { z as z7 } from "zod";
637
679
  var SessionStatus, SessionEvent, SESSION_EVENT_NAME, sessionTopic, runnerTopic, terminalTopic;
@@ -816,6 +858,13 @@ var init_runner = __esm({
816
858
  * older runner will never handle. Absent reads as false; optional so an older row parses.
817
859
  */
818
860
  skills: z9.boolean().optional(),
861
+ /**
862
+ * Whether this runner mirrors `.cayenne/docs` into `public.documents` (D40, protocol §9). No kind
863
+ * carries documents, so the flag is the only signal: the tab says "this runner does not publish
864
+ * documents" instead of "nothing written yet". Absent reads as false; optional so an older row
865
+ * parses.
866
+ */
867
+ documents: z9.boolean().optional(),
819
868
  /**
820
869
  * The raw Ed25519 public key of the running process (32 bytes, unpadded base64url), which signs
821
870
  * the local server's `hello` (protocol §6.2). New on every start, republished by every capabilities refresh that changes the row;
@@ -861,6 +910,7 @@ var init_protocol = __esm({
861
910
  init_attachments();
862
911
  init_command();
863
912
  init_content();
913
+ init_documents();
864
914
  init_events();
865
915
  init_git();
866
916
  init_github();
@@ -3445,7 +3495,7 @@ function createTokenVerifier({
3445
3495
  ttlMs = LOCAL_TOKEN_CACHE_MS,
3446
3496
  negativeTtlMs = LOCAL_TOKEN_NEGATIVE_CACHE_MS,
3447
3497
  now = Date.now,
3448
- admit = () => true,
3498
+ admit: admit2 = () => true,
3449
3499
  log
3450
3500
  }) {
3451
3501
  const cache = new LRUCache2({ max: 1024 });
@@ -3489,7 +3539,7 @@ function createTokenVerifier({
3489
3539
  }
3490
3540
  cache.delete(k);
3491
3541
  const claims = claimsOf(token);
3492
- if (!claims || claims.exp * 1e3 <= now() || !admit(claims.sub)) return refuse3(k);
3542
+ if (!claims || claims.exp * 1e3 <= now() || !admit2(claims.sub)) return refuse3(k);
3493
3543
  if (!budgeted()) return { status: "unavailable" };
3494
3544
  const user = await db.auth.getUser(token).then(
3495
3545
  (r) => r.error ? retryable(r.error) ? void 0 : null : r.data.user,
@@ -4651,6 +4701,522 @@ var init_handlers = __esm({
4651
4701
  }
4652
4702
  });
4653
4703
 
4704
+ // src/documents/paths.ts
4705
+ import { realpathSync as realpathSync8 } from "fs";
4706
+ import { isAbsolute as isAbsolute10, join as join16, relative as relative4, sep as sep5 } from "path";
4707
+ function contained(dir, candidate) {
4708
+ const rel = relative4(dir, candidate);
4709
+ return rel !== "" && rel !== ".." && !rel.startsWith(`..${sep5}`) && !isAbsolute10(rel);
4710
+ }
4711
+ function resolveDocsDir(cwd, log) {
4712
+ const dir = docsDir(cwd);
4713
+ let real;
4714
+ try {
4715
+ real = realpathSync8.native(dir);
4716
+ } catch {
4717
+ return null;
4718
+ }
4719
+ if (!contained(cwd, real)) {
4720
+ log.warn(
4721
+ { dir, resolved: real, cwd },
4722
+ "documents directory skipped: it resolves outside the session working directory"
4723
+ );
4724
+ return null;
4725
+ }
4726
+ return real;
4727
+ }
4728
+ function assertInsideDocsDir(dir, candidate) {
4729
+ let real;
4730
+ try {
4731
+ real = realpathSync8.native(candidate);
4732
+ } catch {
4733
+ throw new Error(`ROOT_NOT_ALLOWED: ${candidate} cannot be resolved`);
4734
+ }
4735
+ if (!contained(dir, real)) throw new Error(`ROOT_NOT_ALLOWED: ${candidate}`);
4736
+ return real;
4737
+ }
4738
+ var docsDir;
4739
+ var init_paths2 = __esm({
4740
+ "src/documents/paths.ts"() {
4741
+ "use strict";
4742
+ init_src();
4743
+ docsDir = (cwd) => join16(cwd, ...DOCUMENTS_DIR.split("/"));
4744
+ }
4745
+ });
4746
+
4747
+ // src/documents/mirror.ts
4748
+ async function syncDocuments(deps, pass) {
4749
+ const log = deps.log ?? logger;
4750
+ const { db } = deps;
4751
+ const result = { inserted: 0, updated: 0, missing: 0 };
4752
+ let known = null;
4753
+ try {
4754
+ const { data, error } = await db.from("documents").select(SELECT).eq("session_id", deps.sessionId).eq("owner_id", deps.ownerId).limit(DOCUMENTS_MIRROR_MAX_ROWS);
4755
+ if (error) throw new Error(error.message);
4756
+ known = data ?? [];
4757
+ } catch (err) {
4758
+ log.warn(
4759
+ { err: said(err), sessionId: deps.sessionId },
4760
+ "could not read this session\u2019s document rows; writing nothing and reconciling nothing"
4761
+ );
4762
+ return result;
4763
+ }
4764
+ const byPath = /* @__PURE__ */ new Map();
4765
+ for (const row of known) if (row.rel_path !== null) byPath.set(row.rel_path, row);
4766
+ const upload = async (path, doc) => {
4767
+ try {
4768
+ const { error } = await db.storage.from(DOCUMENTS_BUCKET).upload(path, doc.bytes, { contentType: contentTypeFor(doc), upsert: true });
4769
+ return error ? error.message : null;
4770
+ } catch (err) {
4771
+ return said(err);
4772
+ }
4773
+ };
4774
+ const publish2 = async (row, doc) => {
4775
+ let expected;
4776
+ try {
4777
+ expected = documentPath(deps.workspaceId, row.id, doc.filename);
4778
+ } catch (err) {
4779
+ return said(err);
4780
+ }
4781
+ if (row.storage_path !== expected) {
4782
+ return `storage_path ${String(row.storage_path)} is not ${expected}`;
4783
+ }
4784
+ return upload(expected, doc);
4785
+ };
4786
+ const takeBack = async (id, relPath) => {
4787
+ const { error } = await db.from("documents").delete().eq("id", id);
4788
+ if (error) {
4789
+ log.warn(
4790
+ { err: error.message, id, relPath, sessionId: deps.sessionId },
4791
+ "could not remove a document row that has no object"
4792
+ );
4793
+ }
4794
+ };
4795
+ for (const doc of pass.write) {
4796
+ const before = byPath.get(doc.relPath);
4797
+ try {
4798
+ if (before === void 0) {
4799
+ const { data, error: error2 } = await db.from("documents").insert(rowOf(deps, doc)).select("id, storage_path").single();
4800
+ if (error2) throw new Error(error2.message);
4801
+ const row = data;
4802
+ if (!row?.id) throw new Error("the insert returned no row");
4803
+ const failed3 = await publish2(row, doc);
4804
+ if (failed3 !== null) {
4805
+ log.warn(
4806
+ { err: failed3, relPath: doc.relPath, sessionId: deps.sessionId },
4807
+ "could not upload a document; removing the row so the next pass retries it"
4808
+ );
4809
+ await takeBack(row.id, doc.relPath);
4810
+ continue;
4811
+ }
4812
+ result.inserted += 1;
4813
+ continue;
4814
+ }
4815
+ const rewritten = before.content_hash !== doc.contentHash;
4816
+ const restated = before.status !== "present" || before.title !== doc.title || before.kind !== doc.kind || before.size_bytes !== doc.size;
4817
+ if (!rewritten && !restated) continue;
4818
+ if (rewritten) {
4819
+ const failed3 = await upload(before.storage_path, doc);
4820
+ if (failed3 !== null) {
4821
+ log.warn(
4822
+ { err: failed3, relPath: doc.relPath, sessionId: deps.sessionId },
4823
+ "could not overwrite a document; its row still describes the bytes in the bucket"
4824
+ );
4825
+ continue;
4826
+ }
4827
+ }
4828
+ const { error } = await db.from("documents").update(patchOf(doc)).eq("id", before.id);
4829
+ if (error) throw new Error(error.message);
4830
+ result.updated += 1;
4831
+ } catch (err) {
4832
+ log.warn(
4833
+ { err: said(err), relPath: doc.relPath, sessionId: deps.sessionId },
4834
+ "could not mirror a document"
4835
+ );
4836
+ }
4837
+ }
4838
+ if (!pass.reconcile) return result;
4839
+ const gone2 = known.filter((r) => r.rel_path !== null && r.status === "present" && !pass.present.has(r.rel_path)).map((r) => r.id);
4840
+ if (gone2.length > 0) {
4841
+ try {
4842
+ const { error } = await db.from("documents").update({ status: "missing" }).in("id", gone2);
4843
+ if (error) throw new Error(error.message);
4844
+ result.missing = gone2.length;
4845
+ } catch (err) {
4846
+ log.warn(
4847
+ { err: said(err), count: gone2.length, sessionId: deps.sessionId },
4848
+ "could not mark vanished documents missing"
4849
+ );
4850
+ }
4851
+ }
4852
+ return result;
4853
+ }
4854
+ var DOCUMENTS_MIRROR_MAX_ROWS, SELECT, contentTypeFor, rowOf, patchOf, said;
4855
+ var init_mirror = __esm({
4856
+ "src/documents/mirror.ts"() {
4857
+ "use strict";
4858
+ init_src();
4859
+ init_log();
4860
+ DOCUMENTS_MIRROR_MAX_ROWS = 1e3;
4861
+ SELECT = "id, rel_path, storage_path, filename, kind, title, content_hash, size_bytes, status";
4862
+ contentTypeFor = (doc) => doc.kind === "file" ? "application/octet-stream" : "text/plain";
4863
+ rowOf = (deps, doc) => ({
4864
+ // Stamped from the session by `stamp_document_insert`; `documents_insert` still checks
4865
+ // `is_member(workspace_id)` against a non-null value, so it is sent.
4866
+ workspace_id: deps.workspaceId,
4867
+ owner_id: deps.ownerId,
4868
+ session_id: deps.sessionId,
4869
+ rel_path: doc.relPath,
4870
+ filename: doc.filename,
4871
+ kind: doc.kind,
4872
+ title: doc.title,
4873
+ content_hash: doc.contentHash,
4874
+ size_bytes: doc.size,
4875
+ source: "watcher",
4876
+ status: "present"
4877
+ });
4878
+ patchOf = (doc) => ({
4879
+ title: doc.title,
4880
+ kind: doc.kind,
4881
+ content_hash: doc.contentHash,
4882
+ size_bytes: doc.size,
4883
+ status: "present"
4884
+ });
4885
+ said = (err) => err instanceof Error ? err.message : String(err);
4886
+ }
4887
+ });
4888
+
4889
+ // src/documents/scan.ts
4890
+ import { createHash as createHash3 } from "crypto";
4891
+ import {
4892
+ closeSync as closeSync2,
4893
+ constants as fsConstants,
4894
+ lstatSync as lstatSync4,
4895
+ opendirSync,
4896
+ openSync as openSync2,
4897
+ readFileSync as readFileSync7
4898
+ } from "fs";
4899
+ import { open } from "fs/promises";
4900
+ import { join as join17 } from "path";
4901
+ function isNew(baseline, relPath, contentHash) {
4902
+ const before = baseline.hashes.get(relPath);
4903
+ if (before === void 0) return baseline.complete;
4904
+ if (before === UNREADABLE) return false;
4905
+ return before !== contentHash;
4906
+ }
4907
+ function readCapped(dir, st) {
4908
+ const handle = opendirSync(dir);
4909
+ const entries = [];
4910
+ try {
4911
+ for (; ; ) {
4912
+ const entry = handle.readSync();
4913
+ if (entry === null) break;
4914
+ if (entries.length === DOCS_SCAN_MAX_ENTRIES) {
4915
+ st.log.warn(
4916
+ { dir, cap: DOCS_SCAN_MAX_ENTRIES },
4917
+ `documents skipped: more than ${DOCS_SCAN_MAX_ENTRIES} entries in one directory`
4918
+ );
4919
+ st.complete = false;
4920
+ break;
4921
+ }
4922
+ entries.push(entry);
4923
+ }
4924
+ } finally {
4925
+ try {
4926
+ handle.closeSync();
4927
+ } catch {
4928
+ }
4929
+ }
4930
+ return entries.sort((a, b) => a.name < b.name ? -1 : 1);
4931
+ }
4932
+ function admit(st, path, relPath) {
4933
+ const skip = (reason) => {
4934
+ st.log.warn({ path, relPath }, `document skipped: ${reason}`);
4935
+ };
4936
+ if (relPath.length > DOCUMENT_REL_PATH_MAX) {
4937
+ skip(`its path is over ${DOCUMENT_REL_PATH_MAX} characters`);
4938
+ return;
4939
+ }
4940
+ if (st.files.length >= DOCUMENTS_MAX_FILES) {
4941
+ st.log.warn(
4942
+ { dir: st.root, cap: DOCUMENTS_MAX_FILES },
4943
+ `documents skipped: this session's directory holds more than ${DOCUMENTS_MAX_FILES} files`
4944
+ );
4945
+ st.complete = false;
4946
+ st.stop = true;
4947
+ return;
4948
+ }
4949
+ let real;
4950
+ try {
4951
+ real = assertInsideDocsDir(st.root, path);
4952
+ } catch (err) {
4953
+ skip(err instanceof Error ? err.message : String(err));
4954
+ return;
4955
+ }
4956
+ let stat;
4957
+ try {
4958
+ stat = lstatSync4(real);
4959
+ } catch {
4960
+ skip("it cannot be read");
4961
+ return;
4962
+ }
4963
+ if (!stat.isFile()) return;
4964
+ if (stat.size > DOCUMENT_MAX_BYTES) {
4965
+ skip(`it is over ${DOCUMENT_MAX_BYTES} bytes`);
4966
+ return;
4967
+ }
4968
+ st.files.push({ relPath, path: real, size: stat.size });
4969
+ }
4970
+ function docFiles(root, log) {
4971
+ const st = { root, files: [], complete: true, stop: false, log };
4972
+ walk2(st, root, "", 0);
4973
+ return st;
4974
+ }
4975
+ function walk2(st, dir, rel, depth) {
4976
+ if (st.stop) return;
4977
+ let entries;
4978
+ try {
4979
+ entries = readCapped(dir, st);
4980
+ } catch {
4981
+ st.complete = false;
4982
+ return;
4983
+ }
4984
+ for (const entry of entries) {
4985
+ if (st.stop) return;
4986
+ if (entry.name.startsWith(".")) continue;
4987
+ const path = join17(dir, entry.name);
4988
+ const relPath = rel === "" ? entry.name : `${rel}/${entry.name}`;
4989
+ if (entry.isSymbolicLink()) {
4990
+ st.log.warn({ path, relPath }, `document skipped: ${SYMLINK_SKIPPED}`);
4991
+ continue;
4992
+ }
4993
+ if (entry.isDirectory()) {
4994
+ if (depth + 1 < DOCS_MAX_DEPTH) walk2(st, path, relPath, depth + 1);
4995
+ continue;
4996
+ }
4997
+ if (entry.isFile()) admit(st, path, relPath);
4998
+ }
4999
+ }
5000
+ async function readAtMost(fh, limit) {
5001
+ const buffer = Buffer.allocUnsafe(limit);
5002
+ let filled = 0;
5003
+ while (filled < limit) {
5004
+ const { bytesRead } = await fh.read(buffer, filled, limit - filled, filled);
5005
+ if (bytesRead === 0) break;
5006
+ filled += bytesRead;
5007
+ }
5008
+ return buffer.subarray(0, filled);
5009
+ }
5010
+ function documentTitle(text, kind, relPath) {
5011
+ const head = text.slice(0, TITLE_SEARCH_MAX);
5012
+ let raw = "";
5013
+ if (kind === "markdown") raw = ATX.exec(head)?.[2] ?? "";
5014
+ else if (kind === "html") raw = HTML_TITLE.exec(head)?.[1] ?? "";
5015
+ const leaf = relPath.split("/").at(-1) ?? "";
5016
+ return clean(raw) || clean(leaf) || "document";
5017
+ }
5018
+ async function openProved(real) {
5019
+ return open(real, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
5020
+ }
5021
+ async function readDocument(file, root, log) {
5022
+ const skip = (reason) => {
5023
+ log.warn({ path: file.path, relPath: file.relPath }, `document skipped: ${reason}`);
5024
+ return null;
5025
+ };
5026
+ let real;
5027
+ try {
5028
+ real = assertInsideDocsDir(root, file.path);
5029
+ } catch (err) {
5030
+ return skip(err instanceof Error ? err.message : String(err));
5031
+ }
5032
+ if (real !== file.path) return skip(SYMLINK_SKIPPED);
5033
+ let fh;
5034
+ try {
5035
+ fh = await openProved(real);
5036
+ } catch {
5037
+ return skip("it is no longer a readable file");
5038
+ }
5039
+ let bytes;
5040
+ try {
5041
+ const stat = await fh.stat();
5042
+ if (!stat.isFile()) return skip("it is no longer a file");
5043
+ if (stat.size > DOCUMENT_MAX_BYTES) return skip(`it is over ${DOCUMENT_MAX_BYTES} bytes`);
5044
+ bytes = await readAtMost(fh, DOCUMENT_MAX_BYTES + 1);
5045
+ } finally {
5046
+ await fh.close().catch(() => void 0);
5047
+ }
5048
+ if (bytes.byteLength > DOCUMENT_MAX_BYTES) return skip(`it is over ${DOCUMENT_MAX_BYTES} bytes`);
5049
+ const filename = documentFilename(file.relPath);
5050
+ const kind = documentKindFor(filename);
5051
+ return {
5052
+ relPath: file.relPath,
5053
+ path: real,
5054
+ filename,
5055
+ kind,
5056
+ title: documentTitle(bytes.subarray(0, TITLE_SEARCH_MAX).toString("utf8"), kind, file.relPath),
5057
+ contentHash: sha256(bytes),
5058
+ size: bytes.byteLength,
5059
+ bytes
5060
+ };
5061
+ }
5062
+ async function scanDocuments(root, log) {
5063
+ const walked = docFiles(root, log);
5064
+ const documents = [];
5065
+ const present = /* @__PURE__ */ new Set();
5066
+ let complete = walked.complete;
5067
+ let budget = DOCS_SCAN_MAX_BYTES;
5068
+ for (const file of walked.files) {
5069
+ if (budget <= 0) {
5070
+ log.warn(
5071
+ { dir: root, cap: DOCS_SCAN_MAX_BYTES },
5072
+ "documents skipped: this pass reached its read budget"
5073
+ );
5074
+ complete = false;
5075
+ break;
5076
+ }
5077
+ budget -= file.size;
5078
+ const doc = await readDocument(file, root, log);
5079
+ if (doc === null) continue;
5080
+ documents.push(doc);
5081
+ present.add(doc.relPath);
5082
+ }
5083
+ return { documents, present, complete };
5084
+ }
5085
+ function baselineOf(root, log) {
5086
+ const hashes = /* @__PURE__ */ new Map();
5087
+ if (root === null) return { hashes, complete: true };
5088
+ const walked = docFiles(root, log);
5089
+ for (const file of walked.files) {
5090
+ let fd;
5091
+ try {
5092
+ fd = openSync2(file.path, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
5093
+ } catch {
5094
+ hashes.set(file.relPath, UNREADABLE);
5095
+ continue;
5096
+ }
5097
+ try {
5098
+ hashes.set(file.relPath, sha256(readFileSync7(fd)));
5099
+ } catch {
5100
+ hashes.set(file.relPath, UNREADABLE);
5101
+ } finally {
5102
+ try {
5103
+ closeSync2(fd);
5104
+ } catch {
5105
+ }
5106
+ }
5107
+ }
5108
+ return { hashes, complete: walked.complete };
5109
+ }
5110
+ var DOCS_MAX_DEPTH, DOCS_SCAN_MAX_ENTRIES, DOCS_SCAN_MAX_BYTES, TITLE_SEARCH_MAX, UNREADABLE, sha256, SYMLINK_SKIPPED, WHITESPACE, ATX, HTML_TITLE, stripControl, firstCodePoints, clean;
5111
+ var init_scan = __esm({
5112
+ "src/documents/scan.ts"() {
5113
+ "use strict";
5114
+ init_src();
5115
+ init_paths2();
5116
+ DOCS_MAX_DEPTH = 3;
5117
+ DOCS_SCAN_MAX_ENTRIES = 500;
5118
+ DOCS_SCAN_MAX_BYTES = 32 * 1024 * 1024;
5119
+ TITLE_SEARCH_MAX = 64 * 1024;
5120
+ UNREADABLE = "unreadable";
5121
+ sha256 = (bytes) => createHash3("sha256").update(bytes).digest("hex");
5122
+ SYMLINK_SKIPPED = "a symlink is never followed";
5123
+ WHITESPACE = /\s+/g;
5124
+ ATX = /^[ \t]{0,3}(#{1,6})[ \t]+(.+?)[ \t]*#*[ \t]*$/m;
5125
+ HTML_TITLE = /<title[^>]*>([\s\S]*?)<\/title>/i;
5126
+ stripControl = (raw) => [...raw].filter((ch) => {
5127
+ const code = ch.codePointAt(0) ?? 0;
5128
+ return code > 31 && (code < 127 || code > 159);
5129
+ }).join("");
5130
+ firstCodePoints = (s, max) => [...s].slice(0, max).join("");
5131
+ clean = (raw) => firstCodePoints(stripControl(raw).replace(WHITESPACE, " ").trim(), DOCUMENT_TITLE_MAX).trim();
5132
+ }
5133
+ });
5134
+
5135
+ // src/documents/service.ts
5136
+ function createDocumentsService(deps) {
5137
+ const log = deps.log ?? logger;
5138
+ const scan = deps.scan ?? scanDocuments;
5139
+ const sync = deps.sync ?? syncDocuments;
5140
+ const mirror = {
5141
+ db: deps.db,
5142
+ workspaceId: deps.workspaceId,
5143
+ ownerId: deps.ownerId,
5144
+ sessionId: deps.sessionId,
5145
+ log
5146
+ };
5147
+ const baseline = baselineOf(resolveDocsDir(deps.cwd, log), log);
5148
+ const owned = /* @__PURE__ */ new Set();
5149
+ let seen = false;
5150
+ let closed = false;
5151
+ let inFlight = null;
5152
+ let dirty = false;
5153
+ const pass = async () => {
5154
+ const dir = resolveDocsDir(deps.cwd, log);
5155
+ if (dir === null) {
5156
+ if (!seen) return;
5157
+ await sync(mirror, { write: [], present: /* @__PURE__ */ new Set(), reconcile: true });
5158
+ return;
5159
+ }
5160
+ seen = true;
5161
+ const { documents, present, complete } = await scan(dir, log);
5162
+ const write = documents.filter(
5163
+ (doc) => owned.has(doc.relPath) || isNew(baseline, doc.relPath, doc.contentHash)
5164
+ );
5165
+ const job = {
5166
+ write,
5167
+ present,
5168
+ // A pass that saw only part of the directory reconciles nothing: the rows it did not see
5169
+ // are not evidence that their files went away.
5170
+ reconcile: complete
5171
+ };
5172
+ const result = await sync(mirror, job);
5173
+ for (const doc of write) owned.add(doc.relPath);
5174
+ if (result.inserted || result.updated || result.missing) {
5175
+ log.info?.({ sessionId: deps.sessionId, ...result }, "documents mirrored for this session");
5176
+ }
5177
+ };
5178
+ const start2 = () => {
5179
+ dirty = false;
5180
+ const started = pass().catch(
5181
+ (err) => log.warn({ err: said2(err), sessionId: deps.sessionId }, "a documents pass failed")
5182
+ ).finally(() => {
5183
+ if (inFlight === started) inFlight = null;
5184
+ if (dirty && !closed) void start2();
5185
+ });
5186
+ inFlight = started;
5187
+ return started;
5188
+ };
5189
+ return {
5190
+ refresh: () => {
5191
+ if (closed) return Promise.resolve();
5192
+ const running = inFlight;
5193
+ if (running) {
5194
+ dirty = true;
5195
+ return running;
5196
+ }
5197
+ return start2();
5198
+ },
5199
+ close: async () => {
5200
+ closed = true;
5201
+ dirty = false;
5202
+ await inFlight?.catch(() => void 0);
5203
+ }
5204
+ };
5205
+ }
5206
+ var DOCS_DEBOUNCE_MS, DOCS_RETRY_MS, said2;
5207
+ var init_service = __esm({
5208
+ "src/documents/service.ts"() {
5209
+ "use strict";
5210
+ init_log();
5211
+ init_mirror();
5212
+ init_paths2();
5213
+ init_scan();
5214
+ DOCS_DEBOUNCE_MS = 1e3;
5215
+ DOCS_RETRY_MS = 1e3;
5216
+ said2 = (err) => err instanceof Error ? err.message : String(err);
5217
+ }
5218
+ });
5219
+
4654
5220
  // src/claude/auth-mode.ts
4655
5221
  function authModeFrom(info) {
4656
5222
  if (info.apiProvider && info.apiProvider !== "firstParty") return "third_party";
@@ -5149,8 +5715,8 @@ var init_policy = __esm({
5149
5715
  import { spawn as spawn2 } from "child_process";
5150
5716
  import { randomUUID as randomUUID4 } from "crypto";
5151
5717
  import { EventEmitter as EventEmitter2 } from "events";
5152
- import { chmodSync as chmodSync4, lstatSync as lstatSync4, mkdirSync as mkdirSync6, rmdirSync, rmSync as rmSync6, writeFileSync as writeFileSync5 } from "fs";
5153
- import { dirname as dirname7, join as join16 } from "path";
5718
+ import { chmodSync as chmodSync4, lstatSync as lstatSync5, mkdirSync as mkdirSync6, rmdirSync, rmSync as rmSync6, writeFileSync as writeFileSync5 } from "fs";
5719
+ import { dirname as dirname7, join as join18 } from "path";
5154
5720
  function removeFiles(files) {
5155
5721
  for (const file of files) rmSync6(file, { force: true });
5156
5722
  }
@@ -5303,14 +5869,14 @@ ${scrubSecrets(this.stderrTail.join("")).slice(-STDERR_TAIL_MAX)}` : "";
5303
5869
  mkdirSync6(dir, { recursive: true, mode: 448 });
5304
5870
  assertStagingInsideHome(home, dir);
5305
5871
  for (const path of [dirname7(dir), dir]) {
5306
- const st = lstatSync4(path);
5872
+ const st = lstatSync5(path);
5307
5873
  if (st.isSymbolicLink() || !st.isDirectory()) {
5308
5874
  throw new Error(`${path} is not a plain directory`);
5309
5875
  }
5310
5876
  }
5311
5877
  chmodSync4(dir, 448);
5312
5878
  for (const image of images) {
5313
- const path = join16(dir, `${randomUUID4()}.${ATTACHMENT_MEDIA_TYPES[image.mediaType]}`);
5879
+ const path = join18(dir, `${randomUUID4()}.${ATTACHMENT_MEDIA_TYPES[image.mediaType]}`);
5314
5880
  writeFileSync5(path, image.data, { mode: 384, flag: "wx" });
5315
5881
  written.push(path);
5316
5882
  }
@@ -5600,15 +6166,15 @@ var init_types = __esm({
5600
6166
  });
5601
6167
 
5602
6168
  // src/skills/paths.ts
5603
- import { realpathSync as realpathSync8 } from "fs";
5604
- import { isAbsolute as isAbsolute10, join as join17, relative as relative4, sep as sep5 } from "path";
6169
+ import { realpathSync as realpathSync9 } from "fs";
6170
+ import { isAbsolute as isAbsolute11, join as join19, relative as relative5, sep as sep6 } from "path";
5605
6171
  function clearSessionSkills(home, runnerId, heldBy) {
5606
- clearRunnerStaging(home, join17(home, "session-skills"), runnerId, heldBy);
6172
+ clearRunnerStaging(home, join19(home, "session-skills"), runnerId, heldBy);
5607
6173
  }
5608
6174
  function assertInsideScopeDir(path, scopeDir) {
5609
6175
  let dirNow;
5610
6176
  try {
5611
- dirNow = realpathSync8.native(scopeDir);
6177
+ dirNow = realpathSync9.native(scopeDir);
5612
6178
  } catch {
5613
6179
  throw new Error(`ROOT_NOT_ALLOWED: ${scopeDir} does not exist`);
5614
6180
  }
@@ -5616,20 +6182,20 @@ function assertInsideScopeDir(path, scopeDir) {
5616
6182
  throw new Error(`ROOT_NOT_ALLOWED: ${scopeDir} no longer resolves to itself (${dirNow})`);
5617
6183
  }
5618
6184
  const real = assertInsideRoots(path, [scopeDir]);
5619
- const rel = relative4(scopeDir, real);
5620
- if (rel === "" || rel === ".." || rel.startsWith(`..${sep5}`) || isAbsolute10(rel)) {
6185
+ const rel = relative5(scopeDir, real);
6186
+ if (rel === "" || rel === ".." || rel.startsWith(`..${sep6}`) || isAbsolute11(rel)) {
5621
6187
  throw new Error(`ROOT_NOT_ALLOWED: ${path}`);
5622
6188
  }
5623
6189
  return real;
5624
6190
  }
5625
6191
  var sessionSkillsRoot, sessionSkillsDir;
5626
- var init_paths2 = __esm({
6192
+ var init_paths3 = __esm({
5627
6193
  "src/skills/paths.ts"() {
5628
6194
  "use strict";
5629
6195
  init_paths();
5630
6196
  init_attachments2();
5631
- sessionSkillsRoot = (home, runnerId) => join17(home, "session-skills", runnerId);
5632
- sessionSkillsDir = (home, runnerId, sessionId) => join17(sessionSkillsRoot(home, runnerId), sessionId);
6197
+ sessionSkillsRoot = (home, runnerId) => join19(home, "session-skills", runnerId);
6198
+ sessionSkillsDir = (home, runnerId, sessionId) => join19(sessionSkillsRoot(home, runnerId), sessionId);
5633
6199
  }
5634
6200
  });
5635
6201
 
@@ -5760,22 +6326,22 @@ var init_refusal = __esm({
5760
6326
  });
5761
6327
 
5762
6328
  // src/skills/scan.ts
5763
- import { createHash as createHash3 } from "crypto";
6329
+ import { createHash as createHash4 } from "crypto";
5764
6330
  import {
5765
6331
  existsSync as existsSync5,
5766
- constants as fsConstants,
5767
- lstatSync as lstatSync5,
5768
- realpathSync as realpathSync9
6332
+ constants as fsConstants2,
6333
+ lstatSync as lstatSync6,
6334
+ realpathSync as realpathSync10
5769
6335
  } from "fs";
5770
- import { open, opendir } from "fs/promises";
5771
- import { isAbsolute as isAbsolute11, join as join18, relative as relative5, sep as sep6 } from "path";
6336
+ import { open as open2, opendir } from "fs/promises";
6337
+ import { isAbsolute as isAbsolute12, join as join20, relative as relative6, sep as sep7 } from "path";
5772
6338
  function inside(root, candidate) {
5773
- const rel = relative5(root, candidate);
5774
- return rel === "" || !rel.startsWith(`..${sep6}`) && rel !== ".." && !isAbsolute11(rel);
6339
+ const rel = relative6(root, candidate);
6340
+ return rel === "" || !rel.startsWith(`..${sep7}`) && rel !== ".." && !isAbsolute12(rel);
5775
6341
  }
5776
6342
  function canonical2(dir) {
5777
6343
  try {
5778
- return realpathSync9.native(dir);
6344
+ return realpathSync10.native(dir);
5779
6345
  } catch {
5780
6346
  return dir;
5781
6347
  }
@@ -5797,11 +6363,11 @@ function scopeRoots(opts) {
5797
6363
  const isGitRepo = opts.isGitRepo ?? defaultIsGitRepo;
5798
6364
  const home = canonical2(opts.home);
5799
6365
  const homes = protectedHomes(opts);
5800
- const found = [{ scope: "personal", dir: join18(home, "skills"), base: home }];
6366
+ const found = [{ scope: "personal", dir: join20(home, "skills"), base: home }];
5801
6367
  let skillsRepo;
5802
6368
  if (opts.skillsRepo !== void 0) {
5803
6369
  try {
5804
- if (!isAbsolute11(opts.skillsRepo)) throw new Error("skillsRepo must be an absolute path");
6370
+ if (!isAbsolute12(opts.skillsRepo)) throw new Error("skillsRepo must be an absolute path");
5805
6371
  skillsRepo = resolveInsideRoots(opts.skillsRepo, opts.roots).real;
5806
6372
  } catch (err) {
5807
6373
  log.warn(
@@ -5813,12 +6379,12 @@ function scopeRoots(opts) {
5813
6379
  for (const root of opts.roots) {
5814
6380
  let real;
5815
6381
  try {
5816
- real = realpathSync9.native(root);
6382
+ real = realpathSync10.native(root);
5817
6383
  } catch {
5818
6384
  continue;
5819
6385
  }
5820
6386
  if (!isGitRepo(real)) continue;
5821
- const dir = join18(real, ".cayenne", "skills");
6387
+ const dir = join20(real, ".cayenne", "skills");
5822
6388
  const shared = skillsRepo !== void 0 && real === skillsRepo ? "workspace" : "project";
5823
6389
  if (overlapsHome(canonical2(dir), homes, shared, log)) continue;
5824
6390
  if (skillsRepo !== void 0 && real === skillsRepo) {
@@ -5836,7 +6402,7 @@ function resolveScopeRoots(opts) {
5836
6402
  for (const root of scopeRoots(opts)) {
5837
6403
  let dirReal;
5838
6404
  try {
5839
- dirReal = realpathSync9.native(root.dir);
6405
+ dirReal = realpathSync10.native(root.dir);
5840
6406
  } catch {
5841
6407
  continue;
5842
6408
  }
@@ -5861,7 +6427,7 @@ async function scanSkills(opts) {
5861
6427
  if (exhausted) break;
5862
6428
  let entries;
5863
6429
  try {
5864
- entries = await readCapped(root.dirReal, log, root.scope);
6430
+ entries = await readCapped2(root.dirReal, log, root.scope);
5865
6431
  } catch {
5866
6432
  continue;
5867
6433
  }
@@ -5880,7 +6446,7 @@ async function scanSkills(opts) {
5880
6446
  } catch (err) {
5881
6447
  log.warn(
5882
6448
  {
5883
- path: join18(root.dirReal, entry.name),
6449
+ path: join20(root.dirReal, entry.name),
5884
6450
  scope: root.scope,
5885
6451
  err: err instanceof Error ? err.message : String(err)
5886
6452
  },
@@ -5895,7 +6461,7 @@ async function scanSkills(opts) {
5895
6461
  }
5896
6462
  return found;
5897
6463
  }
5898
- async function readCapped(dir, log, scope) {
6464
+ async function readCapped2(dir, log, scope) {
5899
6465
  const handle = await opendir(dir);
5900
6466
  const entries = [];
5901
6467
  try {
@@ -5914,7 +6480,7 @@ async function readCapped(dir, log, scope) {
5914
6480
  }
5915
6481
  return entries.sort((a, b) => a.name < b.name ? -1 : 1);
5916
6482
  }
5917
- async function readAtMost(fh, limit) {
6483
+ async function readAtMost2(fh, limit) {
5918
6484
  const buffer = Buffer.allocUnsafe(limit);
5919
6485
  let filled = 0;
5920
6486
  while (filled < limit) {
@@ -5925,13 +6491,13 @@ async function readAtMost(fh, limit) {
5925
6491
  return buffer.subarray(0, filled);
5926
6492
  }
5927
6493
  async function readEntry(root, entry, log, budget) {
5928
- const path = join18(root.dirReal, entry.name);
6494
+ const path = join20(root.dirReal, entry.name);
5929
6495
  const skip = (reason) => {
5930
6496
  log.warn({ path, scope: root.scope }, `skill skipped: ${reason}`);
5931
6497
  return null;
5932
6498
  };
5933
6499
  if (entry.isDirectory()) {
5934
- const nested = existsSync5(join18(path, "SKILL.md"));
6500
+ const nested = existsSync5(join20(path, "SKILL.md"));
5935
6501
  return skip(
5936
6502
  nested ? `a directory holding SKILL.md: Cayenne's store is flat, so move it to ${entry.name}.md beside this directory` : "a directory, and the scan does not recurse"
5937
6503
  );
@@ -5941,20 +6507,20 @@ async function readEntry(root, entry, log, budget) {
5941
6507
  if (!SKILL_SLUG.test(slug)) return skip("the filename is not a legal slug");
5942
6508
  let real;
5943
6509
  try {
5944
- real = realpathSync9.native(path);
6510
+ real = realpathSync10.native(path);
5945
6511
  } catch {
5946
6512
  return skip("cannot be resolved");
5947
6513
  }
5948
6514
  if (!inside(root.dirReal, real)) return skip("a symlink whose target is outside the scope root");
5949
6515
  let proved;
5950
6516
  try {
5951
- proved = lstatSync5(real);
6517
+ proved = lstatSync6(real);
5952
6518
  } catch {
5953
6519
  return skip("cannot be read");
5954
6520
  }
5955
6521
  let fh;
5956
6522
  try {
5957
- fh = await open(real, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
6523
+ fh = await open2(real, fsConstants2.O_RDONLY | fsConstants2.O_NOFOLLOW);
5958
6524
  } catch {
5959
6525
  return skip("cannot be read");
5960
6526
  }
@@ -5965,7 +6531,7 @@ async function readEntry(root, entry, log, budget) {
5965
6531
  if (!stat.isFile()) return skip("not a regular file");
5966
6532
  if (stat.dev !== proved.dev || stat.ino !== proved.ino) return skip("changed while being read");
5967
6533
  if (stat.size > SKILL_BODY_MAX) return skip(`larger than ${SKILL_BODY_MAX} bytes`);
5968
- bytes = await readAtMost(fh, SKILL_BODY_MAX + 1);
6534
+ bytes = await readAtMost2(fh, SKILL_BODY_MAX + 1);
5969
6535
  } catch {
5970
6536
  return skip("cannot be read");
5971
6537
  } finally {
@@ -5994,9 +6560,9 @@ async function readEntry(root, entry, log, budget) {
5994
6560
  ...parsed.ok && parsed.data.description !== void 0 ? { description: parsed.data.description } : {},
5995
6561
  declaredTools: parsed.ok ? parsed.data.tools : [],
5996
6562
  permission: parsed.ok ? parsed.data.permission : "inherit",
5997
- sourcePath: relative5(root.base, path),
6563
+ sourcePath: relative6(root.base, path),
5998
6564
  ...root.projectPath !== void 0 ? { projectPath: root.projectPath } : {},
5999
- contentHash: createHash3("sha256").update(bytes).digest("hex"),
6565
+ contentHash: createHash4("sha256").update(bytes).digest("hex"),
6000
6566
  ...fileMtime !== void 0 ? { fileMtime } : {},
6001
6567
  // The reason only: `refusal.detail` can quote the file's own frontmatter (protocol §2.3).
6002
6568
  refusal: refusal?.reason ?? null
@@ -6080,7 +6646,7 @@ function scalar(raw, prose = false) {
6080
6646
  return !prose && /:\s/.test(value) || /\s#/.test(value) ? null : value;
6081
6647
  }
6082
6648
  var SKILL_SCAN_MAX_PER_DIR, SKILL_SCAN_MAX_SKILLS, SKILL_SCAN_MAX_BYTES, defaultIsGitRepo, summariesOf, FRONTMATTER, KEY, ITEM;
6083
- var init_scan = __esm({
6649
+ var init_scan2 = __esm({
6084
6650
  "src/skills/scan.ts"() {
6085
6651
  "use strict";
6086
6652
  init_src();
@@ -6091,7 +6657,7 @@ var init_scan = __esm({
6091
6657
  SKILL_SCAN_MAX_PER_DIR = 500;
6092
6658
  SKILL_SCAN_MAX_SKILLS = 500;
6093
6659
  SKILL_SCAN_MAX_BYTES = 32 * 1024 * 1024;
6094
- defaultIsGitRepo = (dir) => lstatSync5(join18(dir, ".git"), { throwIfNoEntry: false }) !== void 0;
6660
+ defaultIsGitRepo = (dir) => lstatSync6(join20(dir, ".git"), { throwIfNoEntry: false }) !== void 0;
6095
6661
  summariesOf = (scan) => scan.map((s) => s.summary);
6096
6662
  FRONTMATTER = /^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/;
6097
6663
  KEY = /^([A-Za-z_][A-Za-z0-9_-]*):[ \t]*(.*)$/;
@@ -6100,17 +6666,17 @@ var init_scan = __esm({
6100
6666
  });
6101
6667
 
6102
6668
  // src/skills/materialise.ts
6103
- import { createHash as createHash4 } from "crypto";
6669
+ import { createHash as createHash5 } from "crypto";
6104
6670
  import {
6105
6671
  chmodSync as chmodSync5,
6106
- constants as fsConstants2,
6107
- lstatSync as lstatSync6,
6672
+ constants as fsConstants3,
6673
+ lstatSync as lstatSync7,
6108
6674
  mkdirSync as mkdirSync7,
6109
6675
  readdirSync as readdirSync2,
6110
6676
  rmSync as rmSync7
6111
6677
  } from "fs";
6112
- import { open as open2, writeFile } from "fs/promises";
6113
- import { dirname as dirname8, isAbsolute as isAbsolute12, join as join19, relative as relative6, sep as sep7 } from "path";
6678
+ import { open as open3, writeFile } from "fs/promises";
6679
+ import { dirname as dirname8, isAbsolute as isAbsolute13, join as join21, relative as relative7, sep as sep8 } from "path";
6114
6680
  function isSkillRefusal(err) {
6115
6681
  const msg = err instanceof Error ? err.message : String(err);
6116
6682
  return msg.startsWith("ROOT_NOT_ALLOWED") || msg.startsWith(BUDGET) || msg.includes("is not a plain directory") || msg.includes("is not part of the session skills plugin");
@@ -6157,7 +6723,7 @@ async function materialiseSkills(scan, t) {
6157
6723
  }
6158
6724
  const path = plainDirectory(t.home, sessionSkillsDir(t.home, t.runnerId, t.sessionId));
6159
6725
  await writeFile(
6160
- join19(plainDirectory(t.home, join19(path, ".claude-plugin")), "plugin.json"),
6726
+ join21(plainDirectory(t.home, join21(path, ".claude-plugin")), "plugin.json"),
6161
6727
  `${JSON.stringify(
6162
6728
  {
6163
6729
  name: SKILL_PLUGIN_NAME,
@@ -6187,13 +6753,13 @@ async function materialiseSkills(scan, t) {
6187
6753
  );
6188
6754
  }
6189
6755
  try {
6190
- const dir = plainDirectory(t.home, join19(path, "skills", slug));
6191
- await writeFile(join19(dir, "SKILL.md"), bytes, { mode: 384, flag: "wx" });
6756
+ const dir = plainDirectory(t.home, join21(path, "skills", slug));
6757
+ await writeFile(join21(dir, "SKILL.md"), bytes, { mode: 384, flag: "wx" });
6192
6758
  slugs.push(slug);
6193
6759
  } catch (err) {
6194
6760
  note(log, err, { slug, scope, path: skill.path }, "skill not materialised for this session");
6195
6761
  try {
6196
- rmSync7(assertStagingInsideHome(t.home, join19(path, "skills", slug)), {
6762
+ rmSync7(assertStagingInsideHome(t.home, join21(path, "skills", slug)), {
6197
6763
  recursive: true,
6198
6764
  force: true
6199
6765
  });
@@ -6220,7 +6786,7 @@ async function firstAcceptable(candidates, log, markStale) {
6220
6786
  note(log, err, fields, "skill not materialised for this session");
6221
6787
  continue;
6222
6788
  }
6223
- const hash = createHash4("sha256").update(bytes).digest("hex");
6789
+ const hash = createHash5("sha256").update(bytes).digest("hex");
6224
6790
  if (hash !== skill.summary.contentHash) {
6225
6791
  markStale();
6226
6792
  say(
@@ -6257,21 +6823,21 @@ function assertSealed(path, slugs) {
6257
6823
  const want = new Set(expected);
6258
6824
  for (const entry of entries) {
6259
6825
  const ok = kind === "dir" ? entry.isDirectory() : entry.isFile();
6260
- if (!ok || !want.has(entry.name)) throw stray(join19(dir, entry.name));
6826
+ if (!ok || !want.has(entry.name)) throw stray(join21(dir, entry.name));
6261
6827
  }
6262
- if (entries.length !== want.size) throw stray(join19(dir, [...want].join("|")));
6828
+ if (entries.length !== want.size) throw stray(join21(dir, [...want].join("|")));
6263
6829
  };
6264
6830
  named(path, [".claude-plugin", "skills"], "dir");
6265
- named(join19(path, ".claude-plugin"), ["plugin.json"], "file");
6266
- named(join19(path, "skills"), slugs, "dir");
6267
- for (const slug of slugs) named(join19(path, "skills", slug), ["SKILL.md"], "file");
6831
+ named(join21(path, ".claude-plugin"), ["plugin.json"], "file");
6832
+ named(join21(path, "skills"), slugs, "dir");
6833
+ for (const slug of slugs) named(join21(path, "skills", slug), ["SKILL.md"], "file");
6268
6834
  }
6269
6835
  function plainDirectory(home, dir) {
6270
6836
  const intended = assertStagingInsideHome(home, dir);
6271
6837
  mkdirSync7(intended, { recursive: true, mode: 448 });
6272
6838
  const proved = assertStagingInsideHome(home, intended);
6273
6839
  for (const p of [dirname8(proved), proved]) {
6274
- const st = lstatSync6(p);
6840
+ const st = lstatSync7(p);
6275
6841
  if (st.isSymbolicLink() || !st.isDirectory()) throw new Error(`${p} is not a plain directory`);
6276
6842
  }
6277
6843
  chmodSync5(proved, 448);
@@ -6289,7 +6855,7 @@ async function readBody(skill) {
6289
6855
  }
6290
6856
  let fh;
6291
6857
  try {
6292
- fh = await open2(real, fsConstants2.O_RDONLY | fsConstants2.O_NOFOLLOW);
6858
+ fh = await open3(real, fsConstants3.O_RDONLY | fsConstants3.O_NOFOLLOW);
6293
6859
  } catch {
6294
6860
  throw new Error("no longer on disk, or no longer a file the runner may follow");
6295
6861
  }
@@ -6299,7 +6865,7 @@ async function readBody(skill) {
6299
6865
  stat = await fh.stat();
6300
6866
  if (!stat.isFile()) throw new Error("no longer a regular file");
6301
6867
  if (stat.size > SKILL_BODY_MAX) throw new Error(`larger than ${SKILL_BODY_MAX} bytes`);
6302
- bytes = await readAtMost(fh, SKILL_BODY_MAX + 1);
6868
+ bytes = await readAtMost2(fh, SKILL_BODY_MAX + 1);
6303
6869
  } finally {
6304
6870
  await fh.close().catch(() => void 0);
6305
6871
  }
@@ -6313,22 +6879,22 @@ var init_materialise = __esm({
6313
6879
  init_src();
6314
6880
  init_log();
6315
6881
  init_attachments2();
6316
- init_paths2();
6317
- init_scan();
6882
+ init_paths3();
6883
+ init_scan2();
6318
6884
  SKILL_PLUGIN_NAME = "cayenne";
6319
6885
  SESSION_SKILLS_MAX = 64;
6320
6886
  SESSION_SKILLS_MAX_BYTES = 8 * 1024 * 1024;
6321
6887
  BUDGET = "SKILL_BUDGET:";
6322
6888
  PRECEDENCE = { personal: 0, project: 1, workspace: 2 };
6323
6889
  isAtOrBelow = (p, dir) => {
6324
- const rel = relative6(dir, p);
6325
- return rel === "" || !rel.startsWith(`..${sep7}`) && rel !== ".." && !isAbsolute12(rel);
6890
+ const rel = relative7(dir, p);
6891
+ return rel === "" || !rel.startsWith(`..${sep8}`) && rel !== ".." && !isAbsolute13(rel);
6326
6892
  };
6327
6893
  }
6328
6894
  });
6329
6895
 
6330
6896
  // src/sessions/repo-settings.ts
6331
- import { isAbsolute as isAbsolute13, relative as relative7, sep as sep8 } from "path";
6897
+ import { isAbsolute as isAbsolute14, relative as relative8, sep as sep9 } from "path";
6332
6898
  function repoSettingsPolicy(input) {
6333
6899
  const bypassLock = input.permissionMode === "bypassPermissions" ? {} : { permissions: { disableBypassPermissionsMode: "disable" } };
6334
6900
  const claudeMd = () => ({
@@ -6350,8 +6916,8 @@ function repoSettingsPolicy(input) {
6350
6916
  return { settingSources: ["user"], ...claudeMd(), settings: { ...bypassLock } };
6351
6917
  }
6352
6918
  function insideRoot2(candidate, root) {
6353
- const rel = relative7(root, candidate);
6354
- return rel === "" || rel !== ".." && !rel.startsWith(`..${sep8}`) && !isAbsolute13(rel);
6919
+ const rel = relative8(root, candidate);
6920
+ return rel === "" || rel !== ".." && !rel.startsWith(`..${sep9}`) && !isAbsolute14(rel);
6355
6921
  }
6356
6922
  var init_repo_settings = __esm({
6357
6923
  "src/sessions/repo-settings.ts"() {
@@ -6361,8 +6927,8 @@ var init_repo_settings = __esm({
6361
6927
 
6362
6928
  // src/sessions/manager.ts
6363
6929
  import { randomUUID as randomUUID5 } from "crypto";
6364
- import { realpathSync as realpathSync10 } from "fs";
6365
- import { isAbsolute as isAbsolute14, sep as sep9 } from "path";
6930
+ import { realpathSync as realpathSync11 } from "fs";
6931
+ import { isAbsolute as isAbsolute15, sep as sep10 } from "path";
6366
6932
  import { isDeepStrictEqual } from "util";
6367
6933
  function narrowToSession(suggestions) {
6368
6934
  if (!suggestions) return suggestions;
@@ -6390,6 +6956,8 @@ var init_manager = __esm({
6390
6956
  "use strict";
6391
6957
  init_src();
6392
6958
  init_config();
6959
+ init_paths2();
6960
+ init_service();
6393
6961
  init_introspect();
6394
6962
  init_watch();
6395
6963
  init_worktree();
@@ -6401,7 +6969,7 @@ var init_manager = __esm({
6401
6969
  init_attachments2();
6402
6970
  init_repo_settings();
6403
6971
  roundUsd = (usd) => Math.round(usd * 1e6) / 1e6;
6404
- isAtOrBelow2 = (p, dir) => p === dir || p.startsWith(dir.endsWith(sep9) ? dir : `${dir}${sep9}`);
6972
+ isAtOrBelow2 = (p, dir) => p === dir || p.startsWith(dir.endsWith(sep10) ? dir : `${dir}${sep10}`);
6405
6973
  DENY_MESSAGE_MAX = 2e3;
6406
6974
  EXIT_ERROR_MAX = 8 * 1024;
6407
6975
  INITIAL_PROMPT_MAX = 2e5;
@@ -6897,7 +7465,7 @@ var init_manager = __esm({
6897
7465
  if (stored === null) return false;
6898
7466
  if (isAtOrBelow2(stored, target)) return true;
6899
7467
  try {
6900
- return isAtOrBelow2(realpathSync10.native(stored), target);
7468
+ return isAtOrBelow2(realpathSync11.native(stored), target);
6901
7469
  } catch {
6902
7470
  return false;
6903
7471
  }
@@ -7155,7 +7723,7 @@ var init_manager = __esm({
7155
7723
  }
7156
7724
  /** A path as the SDK reads it: relative means relative to the session's cwd. */
7157
7725
  fromCwd(cwd, value) {
7158
- return isAbsolute14(value) ? value : `${cwd}${sep9}${value}`;
7726
+ return isAbsolute15(value) ? value : `${cwd}${sep10}${value}`;
7159
7727
  }
7160
7728
  /**
7161
7729
  * §2.9: a path in `updatedInput` passes the same boundary the session's `cwd` did, and the
@@ -7386,6 +7954,7 @@ var init_manager = __esm({
7386
7954
  inflight: /* @__PURE__ */ new Set(),
7387
7955
  changesVersion: this.nextChangesSeed(),
7388
7956
  watcher: null,
7957
+ docs: null,
7389
7958
  costLast: 0,
7390
7959
  costTotal: null,
7391
7960
  costUnsaved: 0,
@@ -7393,10 +7962,64 @@ var init_manager = __esm({
7393
7962
  };
7394
7963
  this.live.set(sessionId, live);
7395
7964
  this.wire(sessionId, live);
7965
+ live.docs = this.watchDocuments(sessionId, live);
7396
7966
  live.session.start();
7397
7967
  live.watcher = this.watchWorkingCopy(sessionId, live);
7398
7968
  return live;
7399
7969
  }
7970
+ /**
7971
+ * Arm this session's documents mirror: the service, one pass now, and the watch on
7972
+ * `<cwd>/.cayenne/docs` (lane 5 design §3).
7973
+ *
7974
+ * The directory is never created. Most sessions never write a document, and making it would put
7975
+ * a directory inside the person's repository that they did not ask for; `watchDirs` retries a
7976
+ * directory that is not there and announces on attach, so a file written before it existed is
7977
+ * still picked up — one retry tick plus one debounce, inside the feature's five-second budget.
7978
+ *
7979
+ * Nothing here can fail the session. A service that cannot be built and a watch that cannot be
7980
+ * armed are both a Documents tab that does not update, logged once at warn.
7981
+ */
7982
+ watchDocuments(sessionId, live) {
7983
+ const make = this.deps.documents ?? createDocumentsService;
7984
+ let service2;
7985
+ try {
7986
+ service2 = make({
7987
+ db: this.deps.db,
7988
+ workspaceId: this.deps.workspaceId,
7989
+ // The runner owner, always: it writes as the member it logged in as, and 0022's session
7990
+ // clause is what hands the row to the member whose chat it is. There is no
7991
+ // insert-on-behalf-of path and this must not grow one.
7992
+ ownerId: this.deps.runnerUserId,
7993
+ sessionId,
7994
+ // The canonical directory the provider was spawned in — the worktree when one applies —
7995
+ // never a path a payload carried.
7996
+ cwd: live.cwd,
7997
+ log: this.deps.log
7998
+ });
7999
+ } catch (err) {
8000
+ this.deps.log.warn({ err, sessionId }, "the documents service could not be built");
8001
+ return null;
8002
+ }
8003
+ const refresh = () => {
8004
+ if (this.live.get(sessionId) !== live) return;
8005
+ void service2.refresh().catch(
8006
+ (err) => this.deps.log.warn({ err, sessionId }, "documents refresh failed")
8007
+ );
8008
+ };
8009
+ const attach = this.deps.watchDocs ?? watchDirs;
8010
+ let watcher = null;
8011
+ try {
8012
+ watcher = attach([docsDir(live.cwd)], refresh, {
8013
+ debounceMs: DOCS_DEBOUNCE_MS,
8014
+ retryMs: DOCS_RETRY_MS,
8015
+ onError: (err) => this.deps.log.warn({ err, sessionId }, "documents watcher error; live refresh stopped")
8016
+ });
8017
+ } catch (err) {
8018
+ this.deps.log.warn({ err, sessionId }, "the documents watcher failed to start");
8019
+ }
8020
+ refresh();
8021
+ return { service: service2, watcher };
8022
+ }
7400
8023
  /**
7401
8024
  * Watch the directory the provider was spawned in — `live.cwd`, the canonical path `start()` or
7402
8025
  * `ensureLive()` proved inside the roots (the worktree, when one applies) — and re-broadcast
@@ -7660,6 +8283,14 @@ var init_manager = __esm({
7660
8283
  await Promise.resolve().then(() => watcher?.close()).catch(
7661
8284
  (err) => this.deps.log.warn({ err, sessionId }, "closing the working-copy watcher failed")
7662
8285
  );
8286
+ const docs = live.docs;
8287
+ live.docs = null;
8288
+ await Promise.resolve().then(async () => {
8289
+ await docs?.watcher?.close();
8290
+ await docs?.service.close();
8291
+ }).catch(
8292
+ (err) => this.deps.log.warn({ err, sessionId }, "closing the documents watch failed")
8293
+ );
7663
8294
  if (this.live.get(sessionId) !== live) return;
7664
8295
  this.live.delete(sessionId);
7665
8296
  this.discardSkills(sessionId);
@@ -7804,7 +8435,7 @@ var init_manager = __esm({
7804
8435
  });
7805
8436
 
7806
8437
  // src/skills/mirror.ts
7807
- function rowOf(deps, skill) {
8438
+ function rowOf2(deps, skill) {
7808
8439
  return {
7809
8440
  workspace_id: deps.workspaceId,
7810
8441
  scope: skill.scope,
@@ -7872,7 +8503,7 @@ async function syncSkills(deps, found) {
7872
8503
  const seen = /* @__PURE__ */ new Set();
7873
8504
  const rows = [];
7874
8505
  for (const skill of found) {
7875
- const row = rowOf(deps, skill);
8506
+ const row = rowOf2(deps, skill);
7876
8507
  const key = keyOf2({
7877
8508
  scope: skill.scope,
7878
8509
  slug: skill.slug,
@@ -7933,7 +8564,7 @@ async function syncSkills(deps, found) {
7933
8564
  return { upserted, missing };
7934
8565
  }
7935
8566
  var SKILLS_IDENTITY, SKILLS_MIRROR_MAX_ROWS, keyOf2, instant;
7936
- var init_mirror = __esm({
8567
+ var init_mirror2 = __esm({
7937
8568
  "src/skills/mirror.ts"() {
7938
8569
  "use strict";
7939
8570
  init_log();
@@ -7945,9 +8576,9 @@ var init_mirror = __esm({
7945
8576
  });
7946
8577
 
7947
8578
  // src/skills/service.ts
7948
- import { createHash as createHash5 } from "crypto";
7949
- import { constants as fsConstants3, realpathSync as realpathSync11 } from "fs";
7950
- import { open as open3 } from "fs/promises";
8579
+ import { createHash as createHash6 } from "crypto";
8580
+ import { constants as fsConstants4, realpathSync as realpathSync12 } from "fs";
8581
+ import { open as open4 } from "fs/promises";
7951
8582
  function createSkillsService(deps) {
7952
8583
  const log = deps.log ?? logger;
7953
8584
  const scanner = deps.scanner ?? scanSkills;
@@ -8008,7 +8639,7 @@ function createSkillsService(deps) {
8008
8639
  let wanted;
8009
8640
  if (payload.scope === "project" && payload.projectPath !== void 0) {
8010
8641
  try {
8011
- wanted = realpathSync11.native(payload.projectPath);
8642
+ wanted = realpathSync12.native(payload.projectPath);
8012
8643
  } catch {
8013
8644
  throw missing();
8014
8645
  }
@@ -8046,7 +8677,7 @@ function createSkillsService(deps) {
8046
8677
  const real = assertInsideScopeDir(skill.path, skill.scopeDir);
8047
8678
  let fh;
8048
8679
  try {
8049
- fh = await open3(real, fsConstants3.O_RDONLY | fsConstants3.O_NOFOLLOW);
8680
+ fh = await open4(real, fsConstants4.O_RDONLY | fsConstants4.O_NOFOLLOW);
8050
8681
  } catch {
8051
8682
  throw refuse2(`skill ${payload.slug} is no longer on disk`);
8052
8683
  }
@@ -8056,28 +8687,28 @@ function createSkillsService(deps) {
8056
8687
  stat = await fh.stat();
8057
8688
  if (!stat.isFile()) throw refuse2(`skill ${payload.slug} is no longer a file`);
8058
8689
  if (stat.size > SKILL_BODY_MAX) throw refuse2("skill body too large");
8059
- bytes = await readAtMost(fh, SKILL_BODY_MAX + 1);
8690
+ bytes = await readAtMost2(fh, SKILL_BODY_MAX + 1);
8060
8691
  } finally {
8061
8692
  await fh.close().catch(() => void 0);
8062
8693
  }
8063
8694
  if (bytes.byteLength > SKILL_BODY_MAX) throw refuse2("skill body too large");
8064
8695
  return {
8065
8696
  body: bytes.toString("utf8"),
8066
- contentHash: createHash5("sha256").update(bytes).digest("hex"),
8697
+ contentHash: createHash6("sha256").update(bytes).digest("hex"),
8067
8698
  sourcePath: skill.summary.sourcePath
8068
8699
  };
8069
8700
  }
8070
8701
  };
8071
8702
  }
8072
8703
  var refuse2, SKILL_LIST_RESULT_MAX;
8073
- var init_service = __esm({
8704
+ var init_service2 = __esm({
8074
8705
  "src/skills/service.ts"() {
8075
8706
  "use strict";
8076
8707
  init_src();
8077
8708
  init_log();
8078
- init_mirror();
8079
- init_paths2();
8080
- init_scan();
8709
+ init_mirror2();
8710
+ init_paths3();
8711
+ init_scan2();
8081
8712
  refuse2 = (message) => new Error(`VALIDATION: ${message}`);
8082
8713
  SKILL_LIST_RESULT_MAX = 1024 * 1024;
8083
8714
  }
@@ -8126,9 +8757,9 @@ function createTerminalChannels({
8126
8757
  const inflight = joining.get(terminalId);
8127
8758
  if (inflight) return inflight;
8128
8759
  if (joined.has(terminalId)) return;
8129
- const join22 = attachOne(terminalId).finally(() => joining.delete(terminalId));
8130
- joining.set(terminalId, join22);
8131
- return join22;
8760
+ const join24 = attachOne(terminalId).finally(() => joining.delete(terminalId));
8761
+ joining.set(terminalId, join24);
8762
+ return join24;
8132
8763
  },
8133
8764
  flush,
8134
8765
  detach,
@@ -8183,9 +8814,9 @@ var init_channels = __esm({
8183
8814
  });
8184
8815
 
8185
8816
  // src/terminal/env.ts
8186
- import { readFileSync as readFileSync7, realpathSync as realpathSync12 } from "fs";
8817
+ import { readFileSync as readFileSync8, realpathSync as realpathSync13 } from "fs";
8187
8818
  import { lstat } from "fs/promises";
8188
- import { dirname as dirname9, isAbsolute as isAbsolute15, relative as relative8 } from "path";
8819
+ import { dirname as dirname9, isAbsolute as isAbsolute16, relative as relative9 } from "path";
8189
8820
  function terminalEnv(base, { runnerOwner, claudeConfigDir }) {
8190
8821
  const env = {};
8191
8822
  for (const [key, value] of Object.entries(base)) {
@@ -8254,8 +8885,8 @@ function tryRealpath(fs, p) {
8254
8885
  }
8255
8886
  }
8256
8887
  function inside2(candidate, root) {
8257
- const rel = relative8(root, candidate);
8258
- return rel === "" || !rel.startsWith("..") && !isAbsolute15(rel);
8888
+ const rel = relative9(root, candidate);
8889
+ return rel === "" || !rel.startsWith("..") && !isAbsolute16(rel);
8259
8890
  }
8260
8891
  var SECRET_ENV, MEMBER_ENV_ALLOW, MEMBER_ENV_KEYS, LOCALE_ENV, nodeShellFs;
8261
8892
  var init_env = __esm({
@@ -8280,12 +8911,12 @@ var init_env = __esm({
8280
8911
  nodeShellFs = {
8281
8912
  readEtcShells: () => {
8282
8913
  try {
8283
- return readFileSync7("/etc/shells", "utf8");
8914
+ return readFileSync8("/etc/shells", "utf8");
8284
8915
  } catch {
8285
8916
  return "";
8286
8917
  }
8287
8918
  },
8288
- realpath: (p) => realpathSync12.native(p),
8919
+ realpath: (p) => realpathSync13.native(p),
8289
8920
  lstat: (p) => lstat(p)
8290
8921
  };
8291
8922
  }
@@ -9208,7 +9839,7 @@ function createTerminalService({
9208
9839
  };
9209
9840
  }
9210
9841
  var messageOf2;
9211
- var init_service2 = __esm({
9842
+ var init_service3 = __esm({
9212
9843
  "src/terminal/service.ts"() {
9213
9844
  "use strict";
9214
9845
  init_manager2();
@@ -9225,7 +9856,7 @@ __export(start_exports, {
9225
9856
  start: () => start
9226
9857
  });
9227
9858
  import { homedir as homedir4, hostname } from "os";
9228
- import { join as join20 } from "path";
9859
+ import { join as join22 } from "path";
9229
9860
  function protectedPaths(claudeConfigDir, home = homedir4()) {
9230
9861
  const inHome = [
9231
9862
  ".claude",
@@ -9233,12 +9864,12 @@ function protectedPaths(claudeConfigDir, home = homedir4()) {
9233
9864
  ".codex",
9234
9865
  ".ssh",
9235
9866
  ".aws",
9236
- join20(".config", "gh"),
9867
+ join22(".config", "gh"),
9237
9868
  ".gnupg",
9238
9869
  ".git-credentials",
9239
9870
  ".npmrc",
9240
9871
  ".netrc"
9241
- ].map((p) => join20(home, p));
9872
+ ].map((p) => join22(home, p));
9242
9873
  return [cayenneHome(), ...claudeConfigDir ? [claudeConfigDir] : [], ...inHome];
9243
9874
  }
9244
9875
  function logCodex(codex) {
@@ -9391,7 +10022,12 @@ async function compose(cfg, lock, host, preflight) {
9391
10022
  promptImages: true,
9392
10023
  // D38: this build scans `.cayenne/skills` and answers both `runner.skills.*` kinds, so the
9393
10024
  // Skills page can say "this runner cannot list skills" about an older one instead of waiting.
9394
- skills: true
10025
+ skills: true,
10026
+ // D40: this build mirrors what a session writes under `.cayenne/docs`, so the Documents tab
10027
+ // can say "this runner does not publish documents" about an older one instead of "none yet".
10028
+ // Nothing else is composed here: the mirror is one service per live session, built by the
10029
+ // manager from the directory the provider actually runs in (lane 5 design §3).
10030
+ documents: true
9395
10031
  });
9396
10032
  const registered = capabilities();
9397
10033
  let loggedCodex = null;
@@ -9478,9 +10114,9 @@ async function compose(cfg, lock, host, preflight) {
9478
10114
  // The roster's own array, mutated in place: `serves` reads the live list with no restart.
9479
10115
  allowedMemberIds: liveRoster.allowedMemberIds,
9480
10116
  // Outside every profile and repository: the creator of a worktree is the machine's record.
9481
- worktreeOwners: createWorktreeOwners(join20(cayenneHome(), "worktrees")),
10117
+ worktreeOwners: createWorktreeOwners(join22(cayenneHome(), "worktrees")),
9482
10118
  // Beside it: where each session was spawned, the only source of a resumed directory.
9483
- sessionWorktrees: createSessionWorktrees(join20(cayenneHome(), "session-worktrees")),
10119
+ sessionWorktrees: createSessionWorktrees(join22(cayenneHome(), "session-worktrees")),
9484
10120
  roots,
9485
10121
  allowBypass: cfg.allowBypassPermissions,
9486
10122
  trustedRoots,
@@ -9775,22 +10411,22 @@ var init_start = __esm({
9775
10411
  init_attachments2();
9776
10412
  init_handlers();
9777
10413
  init_manager();
9778
- init_paths2();
9779
- init_service();
10414
+ init_paths3();
10415
+ init_service2();
9780
10416
  init_supabase();
9781
10417
  init_channels();
9782
10418
  init_manager2();
9783
10419
  init_process_group();
9784
10420
  init_pty();
9785
- init_service2();
10421
+ init_service3();
9786
10422
  init_version();
9787
10423
  }
9788
10424
  });
9789
10425
 
9790
10426
  // src/cli.ts
9791
- import { existsSync as existsSync6, mkdirSync as mkdirSync8, readFileSync as readFileSync8, realpathSync as realpathSync13, rmSync as rmSync8, writeFileSync as writeFileSync6 } from "fs";
10427
+ import { existsSync as existsSync6, mkdirSync as mkdirSync8, readFileSync as readFileSync9, realpathSync as realpathSync14, rmSync as rmSync8, writeFileSync as writeFileSync6 } from "fs";
9792
10428
  import { homedir as homedir5, hostname as hostname2, userInfo } from "os";
9793
- import { dirname as dirname10, join as join21 } from "path";
10429
+ import { dirname as dirname10, join as join23 } from "path";
9794
10430
  import { Command } from "commander";
9795
10431
  import prompts from "prompts";
9796
10432
 
@@ -10392,7 +11028,7 @@ program.command("doctor").description("Check the install and say how to fix what
10392
11028
  if (checks.some((c) => !c.ok)) process.exitCode = 1;
10393
11029
  });
10394
11030
  program.command("logs").description("Show the service log").option("-f, --follow", "keep streaming").action(async (opts) => {
10395
- const file = join21(cayenneDir(), "logs", "runner.log");
11031
+ const file = join23(cayenneDir(), "logs", "runner.log");
10396
11032
  if (!existsSync6(file)) throw new Error(`no log yet at ${file}. Run \`cayenne up\` first.`);
10397
11033
  const print = (chunk) => {
10398
11034
  for (const line of chunk.split("\n")) {
@@ -10405,12 +11041,12 @@ program.command("logs").description("Show the service log").option("-f, --follow
10405
11041
  }
10406
11042
  }
10407
11043
  };
10408
- print(readFileSync8(file, "utf8"));
11044
+ print(readFileSync9(file, "utf8"));
10409
11045
  if (opts.follow) {
10410
11046
  const { watch: watch2 } = await import("fs");
10411
- let size = readFileSync8(file).length;
11047
+ let size = readFileSync9(file).length;
10412
11048
  watch2(file, () => {
10413
- const buf = readFileSync8(file);
11049
+ const buf = readFileSync9(file);
10414
11050
  if (buf.length > size) print(buf.subarray(size).toString("utf8"));
10415
11051
  size = buf.length;
10416
11052
  });
@@ -10438,7 +11074,7 @@ program.command("update").description("Re-pin the installed app and restart the
10438
11074
  cliPath: record.cliPath,
10439
11075
  profile: currentProfile(),
10440
11076
  pathEnv: record.pathEnv,
10441
- logDir: join21(dir, "logs")
11077
+ logDir: join23(dir, "logs")
10442
11078
  })
10443
11079
  );
10444
11080
  await launchd.bootout(label);
@@ -10454,12 +11090,12 @@ program.command("uninstall").description("Stop the service and remove the launch
10454
11090
  console.log(`removed ${label}`);
10455
11091
  return;
10456
11092
  }
10457
- const dir = realpathSync13(cayenneDir());
10458
- const home = realpathSync13(homedir5());
11093
+ const dir = realpathSync14(cayenneDir());
11094
+ const home = realpathSync14(homedir5());
10459
11095
  if (dir === "/" || dir === home || home.startsWith(`${dir}/`)) {
10460
11096
  throw new Error(`refusing to purge ${dir}: it is the home directory or an ancestor of it`);
10461
11097
  }
10462
- if (!existsSync6(join21(dir, "config.json")) && !existsSync6(join21(dir, "app", "install.json"))) {
11098
+ if (!existsSync6(join23(dir, "config.json")) && !existsSync6(join23(dir, "app", "install.json"))) {
10463
11099
  throw new Error(`refusing to purge ${dir}: it does not look like a Cayenne profile`);
10464
11100
  }
10465
11101
  if (!opts.yes) {