@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.
- package/dist/cli.js +752 -116
- 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() || !
|
|
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
|
|
5153
|
-
import { dirname as dirname7, join as
|
|
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 =
|
|
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 =
|
|
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
|
|
5604
|
-
import { isAbsolute as
|
|
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,
|
|
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 =
|
|
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 =
|
|
5620
|
-
if (rel === "" || rel === ".." || rel.startsWith(`..${
|
|
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
|
|
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) =>
|
|
5632
|
-
sessionSkillsDir = (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
|
|
6329
|
+
import { createHash as createHash4 } from "crypto";
|
|
5764
6330
|
import {
|
|
5765
6331
|
existsSync as existsSync5,
|
|
5766
|
-
constants as
|
|
5767
|
-
lstatSync as
|
|
5768
|
-
realpathSync as
|
|
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
|
|
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 =
|
|
5774
|
-
return rel === "" || !rel.startsWith(`..${
|
|
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
|
|
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:
|
|
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 (!
|
|
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 =
|
|
6382
|
+
real = realpathSync10.native(root);
|
|
5817
6383
|
} catch {
|
|
5818
6384
|
continue;
|
|
5819
6385
|
}
|
|
5820
6386
|
if (!isGitRepo(real)) continue;
|
|
5821
|
-
const dir =
|
|
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 =
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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:
|
|
6563
|
+
sourcePath: relative6(root.base, path),
|
|
5998
6564
|
...root.projectPath !== void 0 ? { projectPath: root.projectPath } : {},
|
|
5999
|
-
contentHash:
|
|
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
|
|
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) =>
|
|
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
|
|
6669
|
+
import { createHash as createHash5 } from "crypto";
|
|
6104
6670
|
import {
|
|
6105
6671
|
chmodSync as chmodSync5,
|
|
6106
|
-
constants as
|
|
6107
|
-
lstatSync as
|
|
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
|
|
6113
|
-
import { dirname as dirname8, isAbsolute as
|
|
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
|
-
|
|
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,
|
|
6191
|
-
await writeFile(
|
|
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,
|
|
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 =
|
|
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(
|
|
6826
|
+
if (!ok || !want.has(entry.name)) throw stray(join21(dir, entry.name));
|
|
6261
6827
|
}
|
|
6262
|
-
if (entries.length !== want.size) throw stray(
|
|
6828
|
+
if (entries.length !== want.size) throw stray(join21(dir, [...want].join("|")));
|
|
6263
6829
|
};
|
|
6264
6830
|
named(path, [".claude-plugin", "skills"], "dir");
|
|
6265
|
-
named(
|
|
6266
|
-
named(
|
|
6267
|
-
for (const slug of slugs) named(
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
6317
|
-
|
|
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 =
|
|
6325
|
-
return rel === "" || !rel.startsWith(`..${
|
|
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
|
|
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 =
|
|
6354
|
-
return rel === "" || rel !== ".." && !rel.startsWith(`..${
|
|
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
|
|
6365
|
-
import { isAbsolute as
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
7949
|
-
import { constants as
|
|
7950
|
-
import { open as
|
|
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 =
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
8704
|
+
var init_service2 = __esm({
|
|
8074
8705
|
"src/skills/service.ts"() {
|
|
8075
8706
|
"use strict";
|
|
8076
8707
|
init_src();
|
|
8077
8708
|
init_log();
|
|
8078
|
-
|
|
8079
|
-
|
|
8080
|
-
|
|
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
|
|
8130
|
-
joining.set(terminalId,
|
|
8131
|
-
return
|
|
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
|
|
8817
|
+
import { readFileSync as readFileSync8, realpathSync as realpathSync13 } from "fs";
|
|
8187
8818
|
import { lstat } from "fs/promises";
|
|
8188
|
-
import { dirname as dirname9, isAbsolute as
|
|
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 =
|
|
8258
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
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
|
|
8914
|
+
return readFileSync8("/etc/shells", "utf8");
|
|
8284
8915
|
} catch {
|
|
8285
8916
|
return "";
|
|
8286
8917
|
}
|
|
8287
8918
|
},
|
|
8288
|
-
realpath: (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
|
|
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
|
|
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
|
-
|
|
9867
|
+
join22(".config", "gh"),
|
|
9237
9868
|
".gnupg",
|
|
9238
9869
|
".git-credentials",
|
|
9239
9870
|
".npmrc",
|
|
9240
9871
|
".netrc"
|
|
9241
|
-
].map((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(
|
|
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(
|
|
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
|
-
|
|
9779
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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(
|
|
11044
|
+
print(readFileSync9(file, "utf8"));
|
|
10409
11045
|
if (opts.follow) {
|
|
10410
11046
|
const { watch: watch2 } = await import("fs");
|
|
10411
|
-
let size =
|
|
11047
|
+
let size = readFileSync9(file).length;
|
|
10412
11048
|
watch2(file, () => {
|
|
10413
|
-
const buf =
|
|
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:
|
|
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 =
|
|
10458
|
-
const home =
|
|
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(
|
|
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) {
|