@jpcasabiancai/cayenne 0.3.3 → 0.3.5
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 +987 -159
- package/package.json +7 -3
package/dist/cli.js
CHANGED
|
@@ -27,11 +27,11 @@ var CLOUD_SUPABASE_URL, CLOUD_SUPABASE_ANON_KEY, CLOUD_APP_ORIGIN;
|
|
|
27
27
|
var init_cloud = __esm({
|
|
28
28
|
"src/cloud.ts"() {
|
|
29
29
|
"use strict";
|
|
30
|
-
CLOUD_SUPABASE_URL = "";
|
|
30
|
+
CLOUD_SUPABASE_URL = "https://ndqrkqxwkzxgnhkvcunr.supabase.co";
|
|
31
31
|
CLOUD_SUPABASE_ANON_KEY = assertPublishable(
|
|
32
|
-
""
|
|
32
|
+
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im5kcXJrcXh3a3p4Z25oa3ZjdW5yIiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODg3MTMyNzYsImV4cCI6MjEwNDI4OTI3Nn0.Dlsxw91E1QYZ4eoRDjP80Rf4feY77Z-T-fiIwUDY2k0"
|
|
33
33
|
);
|
|
34
|
-
CLOUD_APP_ORIGIN = "";
|
|
34
|
+
CLOUD_APP_ORIGIN = "https://cayenne-pink.vercel.app";
|
|
35
35
|
}
|
|
36
36
|
});
|
|
37
37
|
|
|
@@ -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();
|
|
@@ -1596,11 +1646,13 @@ var init_lock = __esm({
|
|
|
1596
1646
|
});
|
|
1597
1647
|
|
|
1598
1648
|
// ../../packages/db/src/client.ts
|
|
1599
|
-
import {
|
|
1600
|
-
|
|
1649
|
+
import {
|
|
1650
|
+
createClient
|
|
1651
|
+
} from "@supabase/supabase-js";
|
|
1652
|
+
function createRunnerClient(url, anonKey, storage, transport) {
|
|
1601
1653
|
return createClient(url, anonKey, {
|
|
1602
1654
|
auth: { storage, persistSession: true, autoRefreshToken: true, detectSessionInUrl: false },
|
|
1603
|
-
realtime: { params: { eventsPerSecond: 50 } }
|
|
1655
|
+
realtime: { params: { eventsPerSecond: 50 }, transport }
|
|
1604
1656
|
});
|
|
1605
1657
|
}
|
|
1606
1658
|
var init_client = __esm({
|
|
@@ -1617,17 +1669,38 @@ var init_src2 = __esm({
|
|
|
1617
1669
|
}
|
|
1618
1670
|
});
|
|
1619
1671
|
|
|
1672
|
+
// src/realtime-transport.ts
|
|
1673
|
+
import { WebSocket } from "ws";
|
|
1674
|
+
function realtimeTransport(handshakeTimeoutMs = REALTIME_HANDSHAKE_TIMEOUT_MS) {
|
|
1675
|
+
return class RealtimeTransport2 extends WebSocket {
|
|
1676
|
+
static handshakeTimeoutMs = handshakeTimeoutMs;
|
|
1677
|
+
constructor(address, protocols) {
|
|
1678
|
+
super(address, protocols, { handshakeTimeout: RealtimeTransport2.handshakeTimeoutMs });
|
|
1679
|
+
}
|
|
1680
|
+
};
|
|
1681
|
+
}
|
|
1682
|
+
var REALTIME_HANDSHAKE_TIMEOUT_MS, RealtimeTransport;
|
|
1683
|
+
var init_realtime_transport = __esm({
|
|
1684
|
+
"src/realtime-transport.ts"() {
|
|
1685
|
+
"use strict";
|
|
1686
|
+
REALTIME_HANDSHAKE_TIMEOUT_MS = 1e4;
|
|
1687
|
+
RealtimeTransport = realtimeTransport();
|
|
1688
|
+
}
|
|
1689
|
+
});
|
|
1690
|
+
|
|
1620
1691
|
// src/supabase.ts
|
|
1621
1692
|
import { join as join7 } from "path";
|
|
1622
1693
|
function makeClient(cfg, dir = cayenneDir()) {
|
|
1623
1694
|
const storage = new FileSessionStorage(join7(dir, "credentials.json"));
|
|
1624
|
-
|
|
1695
|
+
const transport = RealtimeTransport;
|
|
1696
|
+
return createRunnerClient(cfg.supabaseUrl, cfg.supabaseAnonKey, storage, transport);
|
|
1625
1697
|
}
|
|
1626
1698
|
var init_supabase = __esm({
|
|
1627
1699
|
"src/supabase.ts"() {
|
|
1628
1700
|
"use strict";
|
|
1629
1701
|
init_src2();
|
|
1630
1702
|
init_config();
|
|
1703
|
+
init_realtime_transport();
|
|
1631
1704
|
}
|
|
1632
1705
|
});
|
|
1633
1706
|
|
|
@@ -1928,12 +2001,17 @@ var init_dispatcher = __esm({
|
|
|
1928
2001
|
|
|
1929
2002
|
// src/commands/source.ts
|
|
1930
2003
|
import { LRUCache } from "lru-cache";
|
|
1931
|
-
var STOPPING, byCreatedAt, GIT_READ_KINDS, GIT_WORKTREE_KINDS, laneOf, CommandSource;
|
|
2004
|
+
var STOPPING, REJOIN_MIN_MS, REJOIN_MAX_MS, SOCKET_RESET_MS, LOST, messageOf, byCreatedAt, GIT_READ_KINDS, GIT_WORKTREE_KINDS, laneOf, CommandSource;
|
|
1932
2005
|
var init_source = __esm({
|
|
1933
2006
|
"src/commands/source.ts"() {
|
|
1934
2007
|
"use strict";
|
|
1935
2008
|
init_src();
|
|
1936
2009
|
STOPPING = "runner stopping";
|
|
2010
|
+
REJOIN_MIN_MS = 1e3;
|
|
2011
|
+
REJOIN_MAX_MS = 3e4;
|
|
2012
|
+
SOCKET_RESET_MS = 6e4;
|
|
2013
|
+
LOST = /* @__PURE__ */ new Set(["CHANNEL_ERROR", "TIMED_OUT", "CLOSED"]);
|
|
2014
|
+
messageOf = (err) => err instanceof Error ? err.message : String(err);
|
|
1937
2015
|
byCreatedAt = (a, b) => Date.parse(a.created_at) - Date.parse(b.created_at) || a.id.localeCompare(b.id);
|
|
1938
2016
|
GIT_READ_KINDS = /* @__PURE__ */ new Set(["git.status", "git.diff", "git.branches"]);
|
|
1939
2017
|
GIT_WORKTREE_KINDS = /* @__PURE__ */ new Set([
|
|
@@ -1968,32 +2046,175 @@ var init_source = __esm({
|
|
|
1968
2046
|
/** Rows between `enqueue` and their settlement: an `offer` for one of these registers hooks only. */
|
|
1969
2047
|
inflight = /* @__PURE__ */ new Set();
|
|
1970
2048
|
queues = /* @__PURE__ */ new Map();
|
|
2049
|
+
/** The live `runner:{id}` instance; only its statuses count (see `onStatus`). */
|
|
1971
2050
|
channel = null;
|
|
2051
|
+
/** Every instance this source gave up on: the client can hand one back if its removal did not take. */
|
|
2052
|
+
abandoned = /* @__PURE__ */ new WeakSet();
|
|
2053
|
+
/** Removals of abandoned instances; never rejects. A rejoin waits for it before reusing the topic. */
|
|
2054
|
+
removal = Promise.resolve();
|
|
2055
|
+
rejoinTimer = null;
|
|
2056
|
+
/** Rejoin attempts scheduled since the last `SUBSCRIBED`: the backoff step. */
|
|
2057
|
+
attempts = 0;
|
|
2058
|
+
/** `now()` at the first attempt that found the socket down, or at its last reset; null while up. */
|
|
2059
|
+
socketDownSince = null;
|
|
1972
2060
|
staleMs;
|
|
1973
2061
|
now;
|
|
1974
2062
|
stopped = false;
|
|
1975
2063
|
/** The in-flight `drain()`, so `stop()` can wait for work it never started itself. */
|
|
1976
2064
|
draining = null;
|
|
2065
|
+
/** A `drain()` arrived while one was in flight: that one reads once more before it ends. */
|
|
2066
|
+
again = false;
|
|
1977
2067
|
/** Broadcasts held back while a drain is in flight; see `drain()` for why. */
|
|
1978
2068
|
pending = [];
|
|
2069
|
+
/** Join `runner:{id}` at once; no connectedness check, since `subscribe` connects the socket. */
|
|
1979
2070
|
async start() {
|
|
1980
|
-
this.
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
2071
|
+
this.join(this.runnerChannel());
|
|
2072
|
+
}
|
|
2073
|
+
/**
|
|
2074
|
+
* The one place a `runner:{id}` instance is asked for, first join and rejoins alike. Private:
|
|
2075
|
+
* Realtime authorises the join against RLS, which is what keeps this topic this runner's.
|
|
2076
|
+
*/
|
|
2077
|
+
runnerChannel() {
|
|
2078
|
+
return this.db.channel(runnerTopic(this.runnerId), { config: { private: true } });
|
|
2079
|
+
}
|
|
2080
|
+
/** Make `ch` the live instance, then give it the one broadcast and status handler every instance gets. */
|
|
2081
|
+
join(ch) {
|
|
2082
|
+
this.channel = ch;
|
|
2083
|
+
ch.on("broadcast", { event: "command" }, ({ payload }) => this.onBroadcast(payload)).subscribe(
|
|
2084
|
+
(status, err) => this.onStatus(ch, status, err)
|
|
2085
|
+
);
|
|
2086
|
+
}
|
|
2087
|
+
onBroadcast(payload) {
|
|
2088
|
+
const record = payload?.record;
|
|
2089
|
+
const parsed = CommandRow.safeParse(record);
|
|
2090
|
+
if (!parsed.success) {
|
|
2091
|
+
this.log.warn({ issues: parsed.error.issues }, "ignoring malformed command broadcast");
|
|
2092
|
+
return;
|
|
2093
|
+
}
|
|
2094
|
+
if (parsed.data.runner_id !== this.runnerId) {
|
|
2095
|
+
this.log.warn({ id: parsed.data.id }, "ignoring command for another runner");
|
|
2096
|
+
return;
|
|
2097
|
+
}
|
|
2098
|
+
if (this.draining) this.pending.push(parsed.data);
|
|
2099
|
+
else this.enqueue(parsed.data);
|
|
2100
|
+
}
|
|
2101
|
+
/**
|
|
2102
|
+
* The subscribe callback of every instance this source joined (BACKLOG P-29, protocol §8 step
|
|
2103
|
+
* 5). Nothing else rejoins a lost channel: after a three-hour outage the client's own rejoin never
|
|
2104
|
+
* ran, because its socket never reopened. So a lost instance is abandoned and a fresh one joined
|
|
2105
|
+
* after a backoff; every `SUBSCRIBED` drains, since broadcasts sent while the channel was down
|
|
2106
|
+
* are gone and their rows sit `queued`.
|
|
2107
|
+
*
|
|
2108
|
+
* Only the live instance counts. An abandoned one still reports: its own removal fires `CLOSED`
|
|
2109
|
+
* in-line, and handling that as a loss would remove the instance again, whose leave fires
|
|
2110
|
+
* `CLOSED` again, and so on: removals nested inside removals.
|
|
2111
|
+
*/
|
|
2112
|
+
onStatus(ch, status, err) {
|
|
2113
|
+
if (ch !== this.channel) {
|
|
2114
|
+
this.log.debug({ status }, "ignoring the status of a runner channel no longer in use");
|
|
2115
|
+
return;
|
|
2116
|
+
}
|
|
2117
|
+
this.log.info({ status, err: err?.message }, "runner channel status");
|
|
2118
|
+
if (status === "SUBSCRIBED") {
|
|
2119
|
+
this.attempts = 0;
|
|
2120
|
+
this.socketDownSince = null;
|
|
2121
|
+
void this.drain();
|
|
2122
|
+
} else if (LOST.has(status)) {
|
|
2123
|
+
this.abandon(ch);
|
|
2124
|
+
this.scheduleRejoin(status);
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
/** Stop listening to `ch` (null first, so its removal's `CLOSED` is ignored), then remove it. */
|
|
2128
|
+
abandon(ch) {
|
|
2129
|
+
if (this.channel === ch) this.channel = null;
|
|
2130
|
+
this.abandoned.add(ch);
|
|
2131
|
+
const left = (async () => this.db.removeChannel(ch))().then(
|
|
2132
|
+
(result) => {
|
|
2133
|
+
if (result === "ok" || result === "timed out") return true;
|
|
2134
|
+
this.log.warn({ result }, "leaving the runner channel failed");
|
|
2135
|
+
return false;
|
|
2136
|
+
},
|
|
2137
|
+
(err) => {
|
|
2138
|
+
this.log.warn({ err: messageOf(err) }, "leaving the runner channel failed");
|
|
2139
|
+
return false;
|
|
2140
|
+
}
|
|
2141
|
+
);
|
|
2142
|
+
this.removal = Promise.all([this.removal, left]).then(() => {
|
|
2143
|
+
});
|
|
2144
|
+
return left;
|
|
2145
|
+
}
|
|
2146
|
+
/**
|
|
2147
|
+
* At most one pending attempt, `min(1 s * 2^n, 30 s)` away, n = attempts since the last
|
|
2148
|
+
* `SUBSCRIBED`. `reason` says why: the lost channel's status, or `SOCKET_DOWN`, `NOT_REMOVED`,
|
|
2149
|
+
* `JOIN_FAILED` for an attempt that could not join. It is logged as `reason`, not `status`, so
|
|
2150
|
+
* a search for the client's statuses never meets one of these.
|
|
2151
|
+
*/
|
|
2152
|
+
scheduleRejoin(reason) {
|
|
2153
|
+
if (this.stopped || this.rejoinTimer) return;
|
|
2154
|
+
const delayMs = Math.min(REJOIN_MIN_MS * 2 ** this.attempts, REJOIN_MAX_MS);
|
|
2155
|
+
this.attempts += 1;
|
|
2156
|
+
this.log.warn({ reason, attempt: this.attempts, delayMs }, "runner channel lost; rejoining");
|
|
2157
|
+
this.rejoinTimer = setTimeout(() => {
|
|
2158
|
+
this.rejoinTimer = null;
|
|
2159
|
+
void this.rejoin();
|
|
2160
|
+
}, delayMs);
|
|
2161
|
+
}
|
|
2162
|
+
/**
|
|
2163
|
+
* One attempt. The client hands back the instance it holds for a topic and drops a topic's
|
|
2164
|
+
* entry when any instance of it closes, so the old removal must have finished before a fresh
|
|
2165
|
+
* instance is created. A socket that is down gets a `connect()` and no join: it buffers every
|
|
2166
|
+
* push while down and flushes them all on reopen, so a join per attempt through a long outage
|
|
2167
|
+
* would arrive as a burst of joins and leaves.
|
|
2168
|
+
*/
|
|
2169
|
+
async rejoin() {
|
|
2170
|
+
await this.removal;
|
|
2171
|
+
if (this.stopped) return;
|
|
2172
|
+
try {
|
|
2173
|
+
if (!this.db.realtime.isConnected()) {
|
|
2174
|
+
await this.reconnectSocket();
|
|
2175
|
+
this.scheduleRejoin("SOCKET_DOWN");
|
|
1985
2176
|
return;
|
|
1986
2177
|
}
|
|
1987
|
-
|
|
1988
|
-
|
|
2178
|
+
this.socketDownSince = null;
|
|
2179
|
+
const ch = this.runnerChannel();
|
|
2180
|
+
if (this.abandoned.has(ch)) {
|
|
2181
|
+
this.log.warn({ attempt: this.attempts }, "runner channel still held after leaving it");
|
|
2182
|
+
this.abandon(ch);
|
|
2183
|
+
this.scheduleRejoin("NOT_REMOVED");
|
|
1989
2184
|
return;
|
|
1990
2185
|
}
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
2186
|
+
this.join(ch);
|
|
2187
|
+
} catch (err) {
|
|
2188
|
+
this.log.warn(
|
|
2189
|
+
{ attempt: this.attempts, err: messageOf(err) },
|
|
2190
|
+
"runner channel rejoin failed"
|
|
2191
|
+
);
|
|
2192
|
+
if (this.channel) this.abandon(this.channel);
|
|
2193
|
+
this.scheduleRejoin("JOIN_FAILED");
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
/**
|
|
2197
|
+
* Ask a down socket to connect. Once attempts have found it down for `SOCKET_RESET_MS`, tear it
|
|
2198
|
+
* down first: `connect()` returns early while the client still holds a conn, and a conn whose
|
|
2199
|
+
* handshake failed without a close (Node's own WebSocket, BACKLOG P-29) is held forever. The
|
|
2200
|
+
* `ws` transport should never leave one; this clears it whatever the transport. In an ordinary
|
|
2201
|
+
* outage it costs nothing, since phoenix's reconnect loop carries on from this `connect()`.
|
|
2202
|
+
*/
|
|
2203
|
+
async reconnectSocket() {
|
|
2204
|
+
const now = this.now();
|
|
2205
|
+
this.socketDownSince ??= now;
|
|
2206
|
+
const downMs = now - this.socketDownSince;
|
|
2207
|
+
if (downMs >= SOCKET_RESET_MS) {
|
|
2208
|
+
this.log.warn({ downMs }, "realtime socket still down; resetting it");
|
|
2209
|
+
this.socketDownSince = now;
|
|
2210
|
+
try {
|
|
2211
|
+
await this.db.realtime.disconnect();
|
|
2212
|
+
} catch (err) {
|
|
2213
|
+
this.log.warn({ err: messageOf(err) }, "realtime socket reset failed");
|
|
2214
|
+
}
|
|
2215
|
+
if (this.stopped) return;
|
|
2216
|
+
}
|
|
2217
|
+
this.db.realtime.connect();
|
|
1997
2218
|
}
|
|
1998
2219
|
/**
|
|
1999
2220
|
* Pick up rows queued while the runner was offline, oldest first.
|
|
@@ -2001,15 +2222,37 @@ var init_source = __esm({
|
|
|
2001
2222
|
* Broadcasts that land while the select is in flight are buffered and merged in here rather
|
|
2002
2223
|
* than enqueued as they arrive: every drained row predates them, and protocol §2.1 requires
|
|
2003
2224
|
* one session's commands to execute in `created_at` order.
|
|
2225
|
+
*
|
|
2226
|
+
* One drain at a time: a call while one is in flight (a rejoin's `SUBSCRIBED`) asks it for one
|
|
2227
|
+
* more read and waits for it. A second, overlapping select would release the buffer while the
|
|
2228
|
+
* first was still reading older rows. Rows and buffered broadcasts of every read are merged once,
|
|
2229
|
+
* at the end, because a row only the later read finds can predate a broadcast held earlier.
|
|
2004
2230
|
*/
|
|
2005
|
-
|
|
2006
|
-
if (this.stopped) return;
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2231
|
+
drain() {
|
|
2232
|
+
if (this.stopped) return Promise.resolve();
|
|
2233
|
+
if (this.draining) {
|
|
2234
|
+
this.again = true;
|
|
2235
|
+
return this.draining;
|
|
2236
|
+
}
|
|
2237
|
+
this.draining = this.drainAll();
|
|
2238
|
+
return this.draining;
|
|
2239
|
+
}
|
|
2240
|
+
async drainAll() {
|
|
2012
2241
|
const rows = [];
|
|
2242
|
+
try {
|
|
2243
|
+
do {
|
|
2244
|
+
this.again = false;
|
|
2245
|
+
await this.readQueued(rows);
|
|
2246
|
+
} while (this.again && !this.stopped);
|
|
2247
|
+
} finally {
|
|
2248
|
+
const merged = [...rows, ...this.pending].sort(byCreatedAt);
|
|
2249
|
+
this.pending = [];
|
|
2250
|
+
this.draining = null;
|
|
2251
|
+
for (const row of merged) this.enqueue(row);
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
/** One read of this runner's `queued` rows, appended to `rows`. Never rejects. */
|
|
2255
|
+
async readQueued(rows) {
|
|
2013
2256
|
try {
|
|
2014
2257
|
const { data, error } = await this.db.from("runner_commands").select("*").eq("runner_id", this.runnerId).eq("status", "queued").order("created_at", { ascending: true });
|
|
2015
2258
|
if (error) this.log.error({ err: error.message }, "drain failed");
|
|
@@ -2019,17 +2262,16 @@ var init_source = __esm({
|
|
|
2019
2262
|
}
|
|
2020
2263
|
} catch (err) {
|
|
2021
2264
|
this.log.error({ err }, "drain failed");
|
|
2022
|
-
} finally {
|
|
2023
|
-
const merged = [...rows, ...this.pending].sort(byCreatedAt);
|
|
2024
|
-
this.pending = [];
|
|
2025
|
-
this.draining = null;
|
|
2026
|
-
for (const row of merged) this.enqueue(row);
|
|
2027
|
-
release();
|
|
2028
2265
|
}
|
|
2029
2266
|
}
|
|
2030
2267
|
async stop() {
|
|
2031
2268
|
this.stopped = true;
|
|
2032
|
-
if (this.
|
|
2269
|
+
if (this.rejoinTimer) clearTimeout(this.rejoinTimer);
|
|
2270
|
+
this.rejoinTimer = null;
|
|
2271
|
+
const live = this.channel;
|
|
2272
|
+
const left = live ? this.abandon(live) : null;
|
|
2273
|
+
await this.removal;
|
|
2274
|
+
if (left && await left) this.log.info({ status: "CLOSED" }, "runner channel status");
|
|
2033
2275
|
this.pending = [];
|
|
2034
2276
|
await this.draining;
|
|
2035
2277
|
while (this.queues.size > 0) await Promise.allSettled([...this.queues.values()]);
|
|
@@ -3445,7 +3687,7 @@ function createTokenVerifier({
|
|
|
3445
3687
|
ttlMs = LOCAL_TOKEN_CACHE_MS,
|
|
3446
3688
|
negativeTtlMs = LOCAL_TOKEN_NEGATIVE_CACHE_MS,
|
|
3447
3689
|
now = Date.now,
|
|
3448
|
-
admit = () => true,
|
|
3690
|
+
admit: admit2 = () => true,
|
|
3449
3691
|
log
|
|
3450
3692
|
}) {
|
|
3451
3693
|
const cache = new LRUCache2({ max: 1024 });
|
|
@@ -3489,7 +3731,7 @@ function createTokenVerifier({
|
|
|
3489
3731
|
}
|
|
3490
3732
|
cache.delete(k);
|
|
3491
3733
|
const claims = claimsOf(token);
|
|
3492
|
-
if (!claims || claims.exp * 1e3 <= now() || !
|
|
3734
|
+
if (!claims || claims.exp * 1e3 <= now() || !admit2(claims.sub)) return refuse3(k);
|
|
3493
3735
|
if (!budgeted()) return { status: "unavailable" };
|
|
3494
3736
|
const user = await db.auth.getUser(token).then(
|
|
3495
3737
|
(r) => r.error ? retryable(r.error) ? void 0 : null : r.data.user,
|
|
@@ -4651,6 +4893,522 @@ var init_handlers = __esm({
|
|
|
4651
4893
|
}
|
|
4652
4894
|
});
|
|
4653
4895
|
|
|
4896
|
+
// src/documents/paths.ts
|
|
4897
|
+
import { realpathSync as realpathSync8 } from "fs";
|
|
4898
|
+
import { isAbsolute as isAbsolute10, join as join16, relative as relative4, sep as sep5 } from "path";
|
|
4899
|
+
function contained(dir, candidate) {
|
|
4900
|
+
const rel = relative4(dir, candidate);
|
|
4901
|
+
return rel !== "" && rel !== ".." && !rel.startsWith(`..${sep5}`) && !isAbsolute10(rel);
|
|
4902
|
+
}
|
|
4903
|
+
function resolveDocsDir(cwd, log) {
|
|
4904
|
+
const dir = docsDir(cwd);
|
|
4905
|
+
let real;
|
|
4906
|
+
try {
|
|
4907
|
+
real = realpathSync8.native(dir);
|
|
4908
|
+
} catch {
|
|
4909
|
+
return null;
|
|
4910
|
+
}
|
|
4911
|
+
if (!contained(cwd, real)) {
|
|
4912
|
+
log.warn(
|
|
4913
|
+
{ dir, resolved: real, cwd },
|
|
4914
|
+
"documents directory skipped: it resolves outside the session working directory"
|
|
4915
|
+
);
|
|
4916
|
+
return null;
|
|
4917
|
+
}
|
|
4918
|
+
return real;
|
|
4919
|
+
}
|
|
4920
|
+
function assertInsideDocsDir(dir, candidate) {
|
|
4921
|
+
let real;
|
|
4922
|
+
try {
|
|
4923
|
+
real = realpathSync8.native(candidate);
|
|
4924
|
+
} catch {
|
|
4925
|
+
throw new Error(`ROOT_NOT_ALLOWED: ${candidate} cannot be resolved`);
|
|
4926
|
+
}
|
|
4927
|
+
if (!contained(dir, real)) throw new Error(`ROOT_NOT_ALLOWED: ${candidate}`);
|
|
4928
|
+
return real;
|
|
4929
|
+
}
|
|
4930
|
+
var docsDir;
|
|
4931
|
+
var init_paths2 = __esm({
|
|
4932
|
+
"src/documents/paths.ts"() {
|
|
4933
|
+
"use strict";
|
|
4934
|
+
init_src();
|
|
4935
|
+
docsDir = (cwd) => join16(cwd, ...DOCUMENTS_DIR.split("/"));
|
|
4936
|
+
}
|
|
4937
|
+
});
|
|
4938
|
+
|
|
4939
|
+
// src/documents/mirror.ts
|
|
4940
|
+
async function syncDocuments(deps, pass) {
|
|
4941
|
+
const log = deps.log ?? logger;
|
|
4942
|
+
const { db } = deps;
|
|
4943
|
+
const result = { inserted: 0, updated: 0, missing: 0 };
|
|
4944
|
+
let known = null;
|
|
4945
|
+
try {
|
|
4946
|
+
const { data, error } = await db.from("documents").select(SELECT).eq("session_id", deps.sessionId).eq("owner_id", deps.ownerId).limit(DOCUMENTS_MIRROR_MAX_ROWS);
|
|
4947
|
+
if (error) throw new Error(error.message);
|
|
4948
|
+
known = data ?? [];
|
|
4949
|
+
} catch (err) {
|
|
4950
|
+
log.warn(
|
|
4951
|
+
{ err: said(err), sessionId: deps.sessionId },
|
|
4952
|
+
"could not read this session\u2019s document rows; writing nothing and reconciling nothing"
|
|
4953
|
+
);
|
|
4954
|
+
return result;
|
|
4955
|
+
}
|
|
4956
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
4957
|
+
for (const row of known) if (row.rel_path !== null) byPath.set(row.rel_path, row);
|
|
4958
|
+
const upload = async (path, doc) => {
|
|
4959
|
+
try {
|
|
4960
|
+
const { error } = await db.storage.from(DOCUMENTS_BUCKET).upload(path, doc.bytes, { contentType: contentTypeFor(doc), upsert: true });
|
|
4961
|
+
return error ? error.message : null;
|
|
4962
|
+
} catch (err) {
|
|
4963
|
+
return said(err);
|
|
4964
|
+
}
|
|
4965
|
+
};
|
|
4966
|
+
const publish2 = async (row, doc) => {
|
|
4967
|
+
let expected;
|
|
4968
|
+
try {
|
|
4969
|
+
expected = documentPath(deps.workspaceId, row.id, doc.filename);
|
|
4970
|
+
} catch (err) {
|
|
4971
|
+
return said(err);
|
|
4972
|
+
}
|
|
4973
|
+
if (row.storage_path !== expected) {
|
|
4974
|
+
return `storage_path ${String(row.storage_path)} is not ${expected}`;
|
|
4975
|
+
}
|
|
4976
|
+
return upload(expected, doc);
|
|
4977
|
+
};
|
|
4978
|
+
const takeBack = async (id, relPath) => {
|
|
4979
|
+
const { error } = await db.from("documents").delete().eq("id", id);
|
|
4980
|
+
if (error) {
|
|
4981
|
+
log.warn(
|
|
4982
|
+
{ err: error.message, id, relPath, sessionId: deps.sessionId },
|
|
4983
|
+
"could not remove a document row that has no object"
|
|
4984
|
+
);
|
|
4985
|
+
}
|
|
4986
|
+
};
|
|
4987
|
+
for (const doc of pass.write) {
|
|
4988
|
+
const before = byPath.get(doc.relPath);
|
|
4989
|
+
try {
|
|
4990
|
+
if (before === void 0) {
|
|
4991
|
+
const { data, error: error2 } = await db.from("documents").insert(rowOf(deps, doc)).select("id, storage_path").single();
|
|
4992
|
+
if (error2) throw new Error(error2.message);
|
|
4993
|
+
const row = data;
|
|
4994
|
+
if (!row?.id) throw new Error("the insert returned no row");
|
|
4995
|
+
const failed3 = await publish2(row, doc);
|
|
4996
|
+
if (failed3 !== null) {
|
|
4997
|
+
log.warn(
|
|
4998
|
+
{ err: failed3, relPath: doc.relPath, sessionId: deps.sessionId },
|
|
4999
|
+
"could not upload a document; removing the row so the next pass retries it"
|
|
5000
|
+
);
|
|
5001
|
+
await takeBack(row.id, doc.relPath);
|
|
5002
|
+
continue;
|
|
5003
|
+
}
|
|
5004
|
+
result.inserted += 1;
|
|
5005
|
+
continue;
|
|
5006
|
+
}
|
|
5007
|
+
const rewritten = before.content_hash !== doc.contentHash;
|
|
5008
|
+
const restated = before.status !== "present" || before.title !== doc.title || before.kind !== doc.kind || before.size_bytes !== doc.size;
|
|
5009
|
+
if (!rewritten && !restated) continue;
|
|
5010
|
+
if (rewritten) {
|
|
5011
|
+
const failed3 = await upload(before.storage_path, doc);
|
|
5012
|
+
if (failed3 !== null) {
|
|
5013
|
+
log.warn(
|
|
5014
|
+
{ err: failed3, relPath: doc.relPath, sessionId: deps.sessionId },
|
|
5015
|
+
"could not overwrite a document; its row still describes the bytes in the bucket"
|
|
5016
|
+
);
|
|
5017
|
+
continue;
|
|
5018
|
+
}
|
|
5019
|
+
}
|
|
5020
|
+
const { error } = await db.from("documents").update(patchOf(doc)).eq("id", before.id);
|
|
5021
|
+
if (error) throw new Error(error.message);
|
|
5022
|
+
result.updated += 1;
|
|
5023
|
+
} catch (err) {
|
|
5024
|
+
log.warn(
|
|
5025
|
+
{ err: said(err), relPath: doc.relPath, sessionId: deps.sessionId },
|
|
5026
|
+
"could not mirror a document"
|
|
5027
|
+
);
|
|
5028
|
+
}
|
|
5029
|
+
}
|
|
5030
|
+
if (!pass.reconcile) return result;
|
|
5031
|
+
const gone2 = known.filter((r) => r.rel_path !== null && r.status === "present" && !pass.present.has(r.rel_path)).map((r) => r.id);
|
|
5032
|
+
if (gone2.length > 0) {
|
|
5033
|
+
try {
|
|
5034
|
+
const { error } = await db.from("documents").update({ status: "missing" }).in("id", gone2);
|
|
5035
|
+
if (error) throw new Error(error.message);
|
|
5036
|
+
result.missing = gone2.length;
|
|
5037
|
+
} catch (err) {
|
|
5038
|
+
log.warn(
|
|
5039
|
+
{ err: said(err), count: gone2.length, sessionId: deps.sessionId },
|
|
5040
|
+
"could not mark vanished documents missing"
|
|
5041
|
+
);
|
|
5042
|
+
}
|
|
5043
|
+
}
|
|
5044
|
+
return result;
|
|
5045
|
+
}
|
|
5046
|
+
var DOCUMENTS_MIRROR_MAX_ROWS, SELECT, contentTypeFor, rowOf, patchOf, said;
|
|
5047
|
+
var init_mirror = __esm({
|
|
5048
|
+
"src/documents/mirror.ts"() {
|
|
5049
|
+
"use strict";
|
|
5050
|
+
init_src();
|
|
5051
|
+
init_log();
|
|
5052
|
+
DOCUMENTS_MIRROR_MAX_ROWS = 1e3;
|
|
5053
|
+
SELECT = "id, rel_path, storage_path, filename, kind, title, content_hash, size_bytes, status";
|
|
5054
|
+
contentTypeFor = (doc) => doc.kind === "file" ? "application/octet-stream" : "text/plain";
|
|
5055
|
+
rowOf = (deps, doc) => ({
|
|
5056
|
+
// Stamped from the session by `stamp_document_insert`; `documents_insert` still checks
|
|
5057
|
+
// `is_member(workspace_id)` against a non-null value, so it is sent.
|
|
5058
|
+
workspace_id: deps.workspaceId,
|
|
5059
|
+
owner_id: deps.ownerId,
|
|
5060
|
+
session_id: deps.sessionId,
|
|
5061
|
+
rel_path: doc.relPath,
|
|
5062
|
+
filename: doc.filename,
|
|
5063
|
+
kind: doc.kind,
|
|
5064
|
+
title: doc.title,
|
|
5065
|
+
content_hash: doc.contentHash,
|
|
5066
|
+
size_bytes: doc.size,
|
|
5067
|
+
source: "watcher",
|
|
5068
|
+
status: "present"
|
|
5069
|
+
});
|
|
5070
|
+
patchOf = (doc) => ({
|
|
5071
|
+
title: doc.title,
|
|
5072
|
+
kind: doc.kind,
|
|
5073
|
+
content_hash: doc.contentHash,
|
|
5074
|
+
size_bytes: doc.size,
|
|
5075
|
+
status: "present"
|
|
5076
|
+
});
|
|
5077
|
+
said = (err) => err instanceof Error ? err.message : String(err);
|
|
5078
|
+
}
|
|
5079
|
+
});
|
|
5080
|
+
|
|
5081
|
+
// src/documents/scan.ts
|
|
5082
|
+
import { createHash as createHash3 } from "crypto";
|
|
5083
|
+
import {
|
|
5084
|
+
closeSync as closeSync2,
|
|
5085
|
+
constants as fsConstants,
|
|
5086
|
+
lstatSync as lstatSync4,
|
|
5087
|
+
opendirSync,
|
|
5088
|
+
openSync as openSync2,
|
|
5089
|
+
readFileSync as readFileSync7
|
|
5090
|
+
} from "fs";
|
|
5091
|
+
import { open } from "fs/promises";
|
|
5092
|
+
import { join as join17 } from "path";
|
|
5093
|
+
function isNew(baseline, relPath, contentHash) {
|
|
5094
|
+
const before = baseline.hashes.get(relPath);
|
|
5095
|
+
if (before === void 0) return baseline.complete;
|
|
5096
|
+
if (before === UNREADABLE) return false;
|
|
5097
|
+
return before !== contentHash;
|
|
5098
|
+
}
|
|
5099
|
+
function readCapped(dir, st) {
|
|
5100
|
+
const handle = opendirSync(dir);
|
|
5101
|
+
const entries = [];
|
|
5102
|
+
try {
|
|
5103
|
+
for (; ; ) {
|
|
5104
|
+
const entry = handle.readSync();
|
|
5105
|
+
if (entry === null) break;
|
|
5106
|
+
if (entries.length === DOCS_SCAN_MAX_ENTRIES) {
|
|
5107
|
+
st.log.warn(
|
|
5108
|
+
{ dir, cap: DOCS_SCAN_MAX_ENTRIES },
|
|
5109
|
+
`documents skipped: more than ${DOCS_SCAN_MAX_ENTRIES} entries in one directory`
|
|
5110
|
+
);
|
|
5111
|
+
st.complete = false;
|
|
5112
|
+
break;
|
|
5113
|
+
}
|
|
5114
|
+
entries.push(entry);
|
|
5115
|
+
}
|
|
5116
|
+
} finally {
|
|
5117
|
+
try {
|
|
5118
|
+
handle.closeSync();
|
|
5119
|
+
} catch {
|
|
5120
|
+
}
|
|
5121
|
+
}
|
|
5122
|
+
return entries.sort((a, b) => a.name < b.name ? -1 : 1);
|
|
5123
|
+
}
|
|
5124
|
+
function admit(st, path, relPath) {
|
|
5125
|
+
const skip = (reason) => {
|
|
5126
|
+
st.log.warn({ path, relPath }, `document skipped: ${reason}`);
|
|
5127
|
+
};
|
|
5128
|
+
if (relPath.length > DOCUMENT_REL_PATH_MAX) {
|
|
5129
|
+
skip(`its path is over ${DOCUMENT_REL_PATH_MAX} characters`);
|
|
5130
|
+
return;
|
|
5131
|
+
}
|
|
5132
|
+
if (st.files.length >= DOCUMENTS_MAX_FILES) {
|
|
5133
|
+
st.log.warn(
|
|
5134
|
+
{ dir: st.root, cap: DOCUMENTS_MAX_FILES },
|
|
5135
|
+
`documents skipped: this session's directory holds more than ${DOCUMENTS_MAX_FILES} files`
|
|
5136
|
+
);
|
|
5137
|
+
st.complete = false;
|
|
5138
|
+
st.stop = true;
|
|
5139
|
+
return;
|
|
5140
|
+
}
|
|
5141
|
+
let real;
|
|
5142
|
+
try {
|
|
5143
|
+
real = assertInsideDocsDir(st.root, path);
|
|
5144
|
+
} catch (err) {
|
|
5145
|
+
skip(err instanceof Error ? err.message : String(err));
|
|
5146
|
+
return;
|
|
5147
|
+
}
|
|
5148
|
+
let stat;
|
|
5149
|
+
try {
|
|
5150
|
+
stat = lstatSync4(real);
|
|
5151
|
+
} catch {
|
|
5152
|
+
skip("it cannot be read");
|
|
5153
|
+
return;
|
|
5154
|
+
}
|
|
5155
|
+
if (!stat.isFile()) return;
|
|
5156
|
+
if (stat.size > DOCUMENT_MAX_BYTES) {
|
|
5157
|
+
skip(`it is over ${DOCUMENT_MAX_BYTES} bytes`);
|
|
5158
|
+
return;
|
|
5159
|
+
}
|
|
5160
|
+
st.files.push({ relPath, path: real, size: stat.size });
|
|
5161
|
+
}
|
|
5162
|
+
function docFiles(root, log) {
|
|
5163
|
+
const st = { root, files: [], complete: true, stop: false, log };
|
|
5164
|
+
walk2(st, root, "", 0);
|
|
5165
|
+
return st;
|
|
5166
|
+
}
|
|
5167
|
+
function walk2(st, dir, rel, depth) {
|
|
5168
|
+
if (st.stop) return;
|
|
5169
|
+
let entries;
|
|
5170
|
+
try {
|
|
5171
|
+
entries = readCapped(dir, st);
|
|
5172
|
+
} catch {
|
|
5173
|
+
st.complete = false;
|
|
5174
|
+
return;
|
|
5175
|
+
}
|
|
5176
|
+
for (const entry of entries) {
|
|
5177
|
+
if (st.stop) return;
|
|
5178
|
+
if (entry.name.startsWith(".")) continue;
|
|
5179
|
+
const path = join17(dir, entry.name);
|
|
5180
|
+
const relPath = rel === "" ? entry.name : `${rel}/${entry.name}`;
|
|
5181
|
+
if (entry.isSymbolicLink()) {
|
|
5182
|
+
st.log.warn({ path, relPath }, `document skipped: ${SYMLINK_SKIPPED}`);
|
|
5183
|
+
continue;
|
|
5184
|
+
}
|
|
5185
|
+
if (entry.isDirectory()) {
|
|
5186
|
+
if (depth + 1 < DOCS_MAX_DEPTH) walk2(st, path, relPath, depth + 1);
|
|
5187
|
+
continue;
|
|
5188
|
+
}
|
|
5189
|
+
if (entry.isFile()) admit(st, path, relPath);
|
|
5190
|
+
}
|
|
5191
|
+
}
|
|
5192
|
+
async function readAtMost(fh, limit) {
|
|
5193
|
+
const buffer = Buffer.allocUnsafe(limit);
|
|
5194
|
+
let filled = 0;
|
|
5195
|
+
while (filled < limit) {
|
|
5196
|
+
const { bytesRead } = await fh.read(buffer, filled, limit - filled, filled);
|
|
5197
|
+
if (bytesRead === 0) break;
|
|
5198
|
+
filled += bytesRead;
|
|
5199
|
+
}
|
|
5200
|
+
return buffer.subarray(0, filled);
|
|
5201
|
+
}
|
|
5202
|
+
function documentTitle(text, kind, relPath) {
|
|
5203
|
+
const head = text.slice(0, TITLE_SEARCH_MAX);
|
|
5204
|
+
let raw = "";
|
|
5205
|
+
if (kind === "markdown") raw = ATX.exec(head)?.[2] ?? "";
|
|
5206
|
+
else if (kind === "html") raw = HTML_TITLE.exec(head)?.[1] ?? "";
|
|
5207
|
+
const leaf = relPath.split("/").at(-1) ?? "";
|
|
5208
|
+
return clean(raw) || clean(leaf) || "document";
|
|
5209
|
+
}
|
|
5210
|
+
async function openProved(real) {
|
|
5211
|
+
return open(real, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
|
|
5212
|
+
}
|
|
5213
|
+
async function readDocument(file, root, log) {
|
|
5214
|
+
const skip = (reason) => {
|
|
5215
|
+
log.warn({ path: file.path, relPath: file.relPath }, `document skipped: ${reason}`);
|
|
5216
|
+
return null;
|
|
5217
|
+
};
|
|
5218
|
+
let real;
|
|
5219
|
+
try {
|
|
5220
|
+
real = assertInsideDocsDir(root, file.path);
|
|
5221
|
+
} catch (err) {
|
|
5222
|
+
return skip(err instanceof Error ? err.message : String(err));
|
|
5223
|
+
}
|
|
5224
|
+
if (real !== file.path) return skip(SYMLINK_SKIPPED);
|
|
5225
|
+
let fh;
|
|
5226
|
+
try {
|
|
5227
|
+
fh = await openProved(real);
|
|
5228
|
+
} catch {
|
|
5229
|
+
return skip("it is no longer a readable file");
|
|
5230
|
+
}
|
|
5231
|
+
let bytes;
|
|
5232
|
+
try {
|
|
5233
|
+
const stat = await fh.stat();
|
|
5234
|
+
if (!stat.isFile()) return skip("it is no longer a file");
|
|
5235
|
+
if (stat.size > DOCUMENT_MAX_BYTES) return skip(`it is over ${DOCUMENT_MAX_BYTES} bytes`);
|
|
5236
|
+
bytes = await readAtMost(fh, DOCUMENT_MAX_BYTES + 1);
|
|
5237
|
+
} finally {
|
|
5238
|
+
await fh.close().catch(() => void 0);
|
|
5239
|
+
}
|
|
5240
|
+
if (bytes.byteLength > DOCUMENT_MAX_BYTES) return skip(`it is over ${DOCUMENT_MAX_BYTES} bytes`);
|
|
5241
|
+
const filename = documentFilename(file.relPath);
|
|
5242
|
+
const kind = documentKindFor(filename);
|
|
5243
|
+
return {
|
|
5244
|
+
relPath: file.relPath,
|
|
5245
|
+
path: real,
|
|
5246
|
+
filename,
|
|
5247
|
+
kind,
|
|
5248
|
+
title: documentTitle(bytes.subarray(0, TITLE_SEARCH_MAX).toString("utf8"), kind, file.relPath),
|
|
5249
|
+
contentHash: sha256(bytes),
|
|
5250
|
+
size: bytes.byteLength,
|
|
5251
|
+
bytes
|
|
5252
|
+
};
|
|
5253
|
+
}
|
|
5254
|
+
async function scanDocuments(root, log) {
|
|
5255
|
+
const walked = docFiles(root, log);
|
|
5256
|
+
const documents = [];
|
|
5257
|
+
const present = /* @__PURE__ */ new Set();
|
|
5258
|
+
let complete = walked.complete;
|
|
5259
|
+
let budget = DOCS_SCAN_MAX_BYTES;
|
|
5260
|
+
for (const file of walked.files) {
|
|
5261
|
+
if (budget <= 0) {
|
|
5262
|
+
log.warn(
|
|
5263
|
+
{ dir: root, cap: DOCS_SCAN_MAX_BYTES },
|
|
5264
|
+
"documents skipped: this pass reached its read budget"
|
|
5265
|
+
);
|
|
5266
|
+
complete = false;
|
|
5267
|
+
break;
|
|
5268
|
+
}
|
|
5269
|
+
budget -= file.size;
|
|
5270
|
+
const doc = await readDocument(file, root, log);
|
|
5271
|
+
if (doc === null) continue;
|
|
5272
|
+
documents.push(doc);
|
|
5273
|
+
present.add(doc.relPath);
|
|
5274
|
+
}
|
|
5275
|
+
return { documents, present, complete };
|
|
5276
|
+
}
|
|
5277
|
+
function baselineOf(root, log) {
|
|
5278
|
+
const hashes = /* @__PURE__ */ new Map();
|
|
5279
|
+
if (root === null) return { hashes, complete: true };
|
|
5280
|
+
const walked = docFiles(root, log);
|
|
5281
|
+
for (const file of walked.files) {
|
|
5282
|
+
let fd;
|
|
5283
|
+
try {
|
|
5284
|
+
fd = openSync2(file.path, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
|
|
5285
|
+
} catch {
|
|
5286
|
+
hashes.set(file.relPath, UNREADABLE);
|
|
5287
|
+
continue;
|
|
5288
|
+
}
|
|
5289
|
+
try {
|
|
5290
|
+
hashes.set(file.relPath, sha256(readFileSync7(fd)));
|
|
5291
|
+
} catch {
|
|
5292
|
+
hashes.set(file.relPath, UNREADABLE);
|
|
5293
|
+
} finally {
|
|
5294
|
+
try {
|
|
5295
|
+
closeSync2(fd);
|
|
5296
|
+
} catch {
|
|
5297
|
+
}
|
|
5298
|
+
}
|
|
5299
|
+
}
|
|
5300
|
+
return { hashes, complete: walked.complete };
|
|
5301
|
+
}
|
|
5302
|
+
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;
|
|
5303
|
+
var init_scan = __esm({
|
|
5304
|
+
"src/documents/scan.ts"() {
|
|
5305
|
+
"use strict";
|
|
5306
|
+
init_src();
|
|
5307
|
+
init_paths2();
|
|
5308
|
+
DOCS_MAX_DEPTH = 3;
|
|
5309
|
+
DOCS_SCAN_MAX_ENTRIES = 500;
|
|
5310
|
+
DOCS_SCAN_MAX_BYTES = 32 * 1024 * 1024;
|
|
5311
|
+
TITLE_SEARCH_MAX = 64 * 1024;
|
|
5312
|
+
UNREADABLE = "unreadable";
|
|
5313
|
+
sha256 = (bytes) => createHash3("sha256").update(bytes).digest("hex");
|
|
5314
|
+
SYMLINK_SKIPPED = "a symlink is never followed";
|
|
5315
|
+
WHITESPACE = /\s+/g;
|
|
5316
|
+
ATX = /^[ \t]{0,3}(#{1,6})[ \t]+(.+?)[ \t]*#*[ \t]*$/m;
|
|
5317
|
+
HTML_TITLE = /<title[^>]*>([\s\S]*?)<\/title>/i;
|
|
5318
|
+
stripControl = (raw) => [...raw].filter((ch) => {
|
|
5319
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
5320
|
+
return code > 31 && (code < 127 || code > 159);
|
|
5321
|
+
}).join("");
|
|
5322
|
+
firstCodePoints = (s, max) => [...s].slice(0, max).join("");
|
|
5323
|
+
clean = (raw) => firstCodePoints(stripControl(raw).replace(WHITESPACE, " ").trim(), DOCUMENT_TITLE_MAX).trim();
|
|
5324
|
+
}
|
|
5325
|
+
});
|
|
5326
|
+
|
|
5327
|
+
// src/documents/service.ts
|
|
5328
|
+
function createDocumentsService(deps) {
|
|
5329
|
+
const log = deps.log ?? logger;
|
|
5330
|
+
const scan = deps.scan ?? scanDocuments;
|
|
5331
|
+
const sync = deps.sync ?? syncDocuments;
|
|
5332
|
+
const mirror = {
|
|
5333
|
+
db: deps.db,
|
|
5334
|
+
workspaceId: deps.workspaceId,
|
|
5335
|
+
ownerId: deps.ownerId,
|
|
5336
|
+
sessionId: deps.sessionId,
|
|
5337
|
+
log
|
|
5338
|
+
};
|
|
5339
|
+
const baseline = baselineOf(resolveDocsDir(deps.cwd, log), log);
|
|
5340
|
+
const owned = /* @__PURE__ */ new Set();
|
|
5341
|
+
let seen = false;
|
|
5342
|
+
let closed = false;
|
|
5343
|
+
let inFlight = null;
|
|
5344
|
+
let dirty = false;
|
|
5345
|
+
const pass = async () => {
|
|
5346
|
+
const dir = resolveDocsDir(deps.cwd, log);
|
|
5347
|
+
if (dir === null) {
|
|
5348
|
+
if (!seen) return;
|
|
5349
|
+
await sync(mirror, { write: [], present: /* @__PURE__ */ new Set(), reconcile: true });
|
|
5350
|
+
return;
|
|
5351
|
+
}
|
|
5352
|
+
seen = true;
|
|
5353
|
+
const { documents, present, complete } = await scan(dir, log);
|
|
5354
|
+
const write = documents.filter(
|
|
5355
|
+
(doc) => owned.has(doc.relPath) || isNew(baseline, doc.relPath, doc.contentHash)
|
|
5356
|
+
);
|
|
5357
|
+
const job = {
|
|
5358
|
+
write,
|
|
5359
|
+
present,
|
|
5360
|
+
// A pass that saw only part of the directory reconciles nothing: the rows it did not see
|
|
5361
|
+
// are not evidence that their files went away.
|
|
5362
|
+
reconcile: complete
|
|
5363
|
+
};
|
|
5364
|
+
const result = await sync(mirror, job);
|
|
5365
|
+
for (const doc of write) owned.add(doc.relPath);
|
|
5366
|
+
if (result.inserted || result.updated || result.missing) {
|
|
5367
|
+
log.info?.({ sessionId: deps.sessionId, ...result }, "documents mirrored for this session");
|
|
5368
|
+
}
|
|
5369
|
+
};
|
|
5370
|
+
const start2 = () => {
|
|
5371
|
+
dirty = false;
|
|
5372
|
+
const started = pass().catch(
|
|
5373
|
+
(err) => log.warn({ err: said2(err), sessionId: deps.sessionId }, "a documents pass failed")
|
|
5374
|
+
).finally(() => {
|
|
5375
|
+
if (inFlight === started) inFlight = null;
|
|
5376
|
+
if (dirty && !closed) void start2();
|
|
5377
|
+
});
|
|
5378
|
+
inFlight = started;
|
|
5379
|
+
return started;
|
|
5380
|
+
};
|
|
5381
|
+
return {
|
|
5382
|
+
refresh: () => {
|
|
5383
|
+
if (closed) return Promise.resolve();
|
|
5384
|
+
const running = inFlight;
|
|
5385
|
+
if (running) {
|
|
5386
|
+
dirty = true;
|
|
5387
|
+
return running;
|
|
5388
|
+
}
|
|
5389
|
+
return start2();
|
|
5390
|
+
},
|
|
5391
|
+
close: async () => {
|
|
5392
|
+
closed = true;
|
|
5393
|
+
dirty = false;
|
|
5394
|
+
await inFlight?.catch(() => void 0);
|
|
5395
|
+
}
|
|
5396
|
+
};
|
|
5397
|
+
}
|
|
5398
|
+
var DOCS_DEBOUNCE_MS, DOCS_RETRY_MS, said2;
|
|
5399
|
+
var init_service = __esm({
|
|
5400
|
+
"src/documents/service.ts"() {
|
|
5401
|
+
"use strict";
|
|
5402
|
+
init_log();
|
|
5403
|
+
init_mirror();
|
|
5404
|
+
init_paths2();
|
|
5405
|
+
init_scan();
|
|
5406
|
+
DOCS_DEBOUNCE_MS = 1e3;
|
|
5407
|
+
DOCS_RETRY_MS = 1e3;
|
|
5408
|
+
said2 = (err) => err instanceof Error ? err.message : String(err);
|
|
5409
|
+
}
|
|
5410
|
+
});
|
|
5411
|
+
|
|
4654
5412
|
// src/claude/auth-mode.ts
|
|
4655
5413
|
function authModeFrom(info) {
|
|
4656
5414
|
if (info.apiProvider && info.apiProvider !== "firstParty") return "third_party";
|
|
@@ -5149,8 +5907,8 @@ var init_policy = __esm({
|
|
|
5149
5907
|
import { spawn as spawn2 } from "child_process";
|
|
5150
5908
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
5151
5909
|
import { EventEmitter as EventEmitter2 } from "events";
|
|
5152
|
-
import { chmodSync as chmodSync4, lstatSync as
|
|
5153
|
-
import { dirname as dirname7, join as
|
|
5910
|
+
import { chmodSync as chmodSync4, lstatSync as lstatSync5, mkdirSync as mkdirSync6, rmdirSync, rmSync as rmSync6, writeFileSync as writeFileSync5 } from "fs";
|
|
5911
|
+
import { dirname as dirname7, join as join18 } from "path";
|
|
5154
5912
|
function removeFiles(files) {
|
|
5155
5913
|
for (const file of files) rmSync6(file, { force: true });
|
|
5156
5914
|
}
|
|
@@ -5303,14 +6061,14 @@ ${scrubSecrets(this.stderrTail.join("")).slice(-STDERR_TAIL_MAX)}` : "";
|
|
|
5303
6061
|
mkdirSync6(dir, { recursive: true, mode: 448 });
|
|
5304
6062
|
assertStagingInsideHome(home, dir);
|
|
5305
6063
|
for (const path of [dirname7(dir), dir]) {
|
|
5306
|
-
const st =
|
|
6064
|
+
const st = lstatSync5(path);
|
|
5307
6065
|
if (st.isSymbolicLink() || !st.isDirectory()) {
|
|
5308
6066
|
throw new Error(`${path} is not a plain directory`);
|
|
5309
6067
|
}
|
|
5310
6068
|
}
|
|
5311
6069
|
chmodSync4(dir, 448);
|
|
5312
6070
|
for (const image of images) {
|
|
5313
|
-
const path =
|
|
6071
|
+
const path = join18(dir, `${randomUUID4()}.${ATTACHMENT_MEDIA_TYPES[image.mediaType]}`);
|
|
5314
6072
|
writeFileSync5(path, image.data, { mode: 384, flag: "wx" });
|
|
5315
6073
|
written.push(path);
|
|
5316
6074
|
}
|
|
@@ -5600,15 +6358,15 @@ var init_types = __esm({
|
|
|
5600
6358
|
});
|
|
5601
6359
|
|
|
5602
6360
|
// src/skills/paths.ts
|
|
5603
|
-
import { realpathSync as
|
|
5604
|
-
import { isAbsolute as
|
|
6361
|
+
import { realpathSync as realpathSync9 } from "fs";
|
|
6362
|
+
import { isAbsolute as isAbsolute11, join as join19, relative as relative5, sep as sep6 } from "path";
|
|
5605
6363
|
function clearSessionSkills(home, runnerId, heldBy) {
|
|
5606
|
-
clearRunnerStaging(home,
|
|
6364
|
+
clearRunnerStaging(home, join19(home, "session-skills"), runnerId, heldBy);
|
|
5607
6365
|
}
|
|
5608
6366
|
function assertInsideScopeDir(path, scopeDir) {
|
|
5609
6367
|
let dirNow;
|
|
5610
6368
|
try {
|
|
5611
|
-
dirNow =
|
|
6369
|
+
dirNow = realpathSync9.native(scopeDir);
|
|
5612
6370
|
} catch {
|
|
5613
6371
|
throw new Error(`ROOT_NOT_ALLOWED: ${scopeDir} does not exist`);
|
|
5614
6372
|
}
|
|
@@ -5616,20 +6374,20 @@ function assertInsideScopeDir(path, scopeDir) {
|
|
|
5616
6374
|
throw new Error(`ROOT_NOT_ALLOWED: ${scopeDir} no longer resolves to itself (${dirNow})`);
|
|
5617
6375
|
}
|
|
5618
6376
|
const real = assertInsideRoots(path, [scopeDir]);
|
|
5619
|
-
const rel =
|
|
5620
|
-
if (rel === "" || rel === ".." || rel.startsWith(`..${
|
|
6377
|
+
const rel = relative5(scopeDir, real);
|
|
6378
|
+
if (rel === "" || rel === ".." || rel.startsWith(`..${sep6}`) || isAbsolute11(rel)) {
|
|
5621
6379
|
throw new Error(`ROOT_NOT_ALLOWED: ${path}`);
|
|
5622
6380
|
}
|
|
5623
6381
|
return real;
|
|
5624
6382
|
}
|
|
5625
6383
|
var sessionSkillsRoot, sessionSkillsDir;
|
|
5626
|
-
var
|
|
6384
|
+
var init_paths3 = __esm({
|
|
5627
6385
|
"src/skills/paths.ts"() {
|
|
5628
6386
|
"use strict";
|
|
5629
6387
|
init_paths();
|
|
5630
6388
|
init_attachments2();
|
|
5631
|
-
sessionSkillsRoot = (home, runnerId) =>
|
|
5632
|
-
sessionSkillsDir = (home, runnerId, sessionId) =>
|
|
6389
|
+
sessionSkillsRoot = (home, runnerId) => join19(home, "session-skills", runnerId);
|
|
6390
|
+
sessionSkillsDir = (home, runnerId, sessionId) => join19(sessionSkillsRoot(home, runnerId), sessionId);
|
|
5633
6391
|
}
|
|
5634
6392
|
});
|
|
5635
6393
|
|
|
@@ -5760,22 +6518,22 @@ var init_refusal = __esm({
|
|
|
5760
6518
|
});
|
|
5761
6519
|
|
|
5762
6520
|
// src/skills/scan.ts
|
|
5763
|
-
import { createHash as
|
|
6521
|
+
import { createHash as createHash4 } from "crypto";
|
|
5764
6522
|
import {
|
|
5765
6523
|
existsSync as existsSync5,
|
|
5766
|
-
constants as
|
|
5767
|
-
lstatSync as
|
|
5768
|
-
realpathSync as
|
|
6524
|
+
constants as fsConstants2,
|
|
6525
|
+
lstatSync as lstatSync6,
|
|
6526
|
+
realpathSync as realpathSync10
|
|
5769
6527
|
} from "fs";
|
|
5770
|
-
import { open, opendir } from "fs/promises";
|
|
5771
|
-
import { isAbsolute as
|
|
6528
|
+
import { open as open2, opendir } from "fs/promises";
|
|
6529
|
+
import { isAbsolute as isAbsolute12, join as join20, relative as relative6, sep as sep7 } from "path";
|
|
5772
6530
|
function inside(root, candidate) {
|
|
5773
|
-
const rel =
|
|
5774
|
-
return rel === "" || !rel.startsWith(`..${
|
|
6531
|
+
const rel = relative6(root, candidate);
|
|
6532
|
+
return rel === "" || !rel.startsWith(`..${sep7}`) && rel !== ".." && !isAbsolute12(rel);
|
|
5775
6533
|
}
|
|
5776
6534
|
function canonical2(dir) {
|
|
5777
6535
|
try {
|
|
5778
|
-
return
|
|
6536
|
+
return realpathSync10.native(dir);
|
|
5779
6537
|
} catch {
|
|
5780
6538
|
return dir;
|
|
5781
6539
|
}
|
|
@@ -5797,11 +6555,11 @@ function scopeRoots(opts) {
|
|
|
5797
6555
|
const isGitRepo = opts.isGitRepo ?? defaultIsGitRepo;
|
|
5798
6556
|
const home = canonical2(opts.home);
|
|
5799
6557
|
const homes = protectedHomes(opts);
|
|
5800
|
-
const found = [{ scope: "personal", dir:
|
|
6558
|
+
const found = [{ scope: "personal", dir: join20(home, "skills"), base: home }];
|
|
5801
6559
|
let skillsRepo;
|
|
5802
6560
|
if (opts.skillsRepo !== void 0) {
|
|
5803
6561
|
try {
|
|
5804
|
-
if (!
|
|
6562
|
+
if (!isAbsolute12(opts.skillsRepo)) throw new Error("skillsRepo must be an absolute path");
|
|
5805
6563
|
skillsRepo = resolveInsideRoots(opts.skillsRepo, opts.roots).real;
|
|
5806
6564
|
} catch (err) {
|
|
5807
6565
|
log.warn(
|
|
@@ -5813,12 +6571,12 @@ function scopeRoots(opts) {
|
|
|
5813
6571
|
for (const root of opts.roots) {
|
|
5814
6572
|
let real;
|
|
5815
6573
|
try {
|
|
5816
|
-
real =
|
|
6574
|
+
real = realpathSync10.native(root);
|
|
5817
6575
|
} catch {
|
|
5818
6576
|
continue;
|
|
5819
6577
|
}
|
|
5820
6578
|
if (!isGitRepo(real)) continue;
|
|
5821
|
-
const dir =
|
|
6579
|
+
const dir = join20(real, ".cayenne", "skills");
|
|
5822
6580
|
const shared = skillsRepo !== void 0 && real === skillsRepo ? "workspace" : "project";
|
|
5823
6581
|
if (overlapsHome(canonical2(dir), homes, shared, log)) continue;
|
|
5824
6582
|
if (skillsRepo !== void 0 && real === skillsRepo) {
|
|
@@ -5836,7 +6594,7 @@ function resolveScopeRoots(opts) {
|
|
|
5836
6594
|
for (const root of scopeRoots(opts)) {
|
|
5837
6595
|
let dirReal;
|
|
5838
6596
|
try {
|
|
5839
|
-
dirReal =
|
|
6597
|
+
dirReal = realpathSync10.native(root.dir);
|
|
5840
6598
|
} catch {
|
|
5841
6599
|
continue;
|
|
5842
6600
|
}
|
|
@@ -5861,7 +6619,7 @@ async function scanSkills(opts) {
|
|
|
5861
6619
|
if (exhausted) break;
|
|
5862
6620
|
let entries;
|
|
5863
6621
|
try {
|
|
5864
|
-
entries = await
|
|
6622
|
+
entries = await readCapped2(root.dirReal, log, root.scope);
|
|
5865
6623
|
} catch {
|
|
5866
6624
|
continue;
|
|
5867
6625
|
}
|
|
@@ -5880,7 +6638,7 @@ async function scanSkills(opts) {
|
|
|
5880
6638
|
} catch (err) {
|
|
5881
6639
|
log.warn(
|
|
5882
6640
|
{
|
|
5883
|
-
path:
|
|
6641
|
+
path: join20(root.dirReal, entry.name),
|
|
5884
6642
|
scope: root.scope,
|
|
5885
6643
|
err: err instanceof Error ? err.message : String(err)
|
|
5886
6644
|
},
|
|
@@ -5895,7 +6653,7 @@ async function scanSkills(opts) {
|
|
|
5895
6653
|
}
|
|
5896
6654
|
return found;
|
|
5897
6655
|
}
|
|
5898
|
-
async function
|
|
6656
|
+
async function readCapped2(dir, log, scope) {
|
|
5899
6657
|
const handle = await opendir(dir);
|
|
5900
6658
|
const entries = [];
|
|
5901
6659
|
try {
|
|
@@ -5914,7 +6672,7 @@ async function readCapped(dir, log, scope) {
|
|
|
5914
6672
|
}
|
|
5915
6673
|
return entries.sort((a, b) => a.name < b.name ? -1 : 1);
|
|
5916
6674
|
}
|
|
5917
|
-
async function
|
|
6675
|
+
async function readAtMost2(fh, limit) {
|
|
5918
6676
|
const buffer = Buffer.allocUnsafe(limit);
|
|
5919
6677
|
let filled = 0;
|
|
5920
6678
|
while (filled < limit) {
|
|
@@ -5925,13 +6683,13 @@ async function readAtMost(fh, limit) {
|
|
|
5925
6683
|
return buffer.subarray(0, filled);
|
|
5926
6684
|
}
|
|
5927
6685
|
async function readEntry(root, entry, log, budget) {
|
|
5928
|
-
const path =
|
|
6686
|
+
const path = join20(root.dirReal, entry.name);
|
|
5929
6687
|
const skip = (reason) => {
|
|
5930
6688
|
log.warn({ path, scope: root.scope }, `skill skipped: ${reason}`);
|
|
5931
6689
|
return null;
|
|
5932
6690
|
};
|
|
5933
6691
|
if (entry.isDirectory()) {
|
|
5934
|
-
const nested = existsSync5(
|
|
6692
|
+
const nested = existsSync5(join20(path, "SKILL.md"));
|
|
5935
6693
|
return skip(
|
|
5936
6694
|
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
6695
|
);
|
|
@@ -5941,20 +6699,20 @@ async function readEntry(root, entry, log, budget) {
|
|
|
5941
6699
|
if (!SKILL_SLUG.test(slug)) return skip("the filename is not a legal slug");
|
|
5942
6700
|
let real;
|
|
5943
6701
|
try {
|
|
5944
|
-
real =
|
|
6702
|
+
real = realpathSync10.native(path);
|
|
5945
6703
|
} catch {
|
|
5946
6704
|
return skip("cannot be resolved");
|
|
5947
6705
|
}
|
|
5948
6706
|
if (!inside(root.dirReal, real)) return skip("a symlink whose target is outside the scope root");
|
|
5949
6707
|
let proved;
|
|
5950
6708
|
try {
|
|
5951
|
-
proved =
|
|
6709
|
+
proved = lstatSync6(real);
|
|
5952
6710
|
} catch {
|
|
5953
6711
|
return skip("cannot be read");
|
|
5954
6712
|
}
|
|
5955
6713
|
let fh;
|
|
5956
6714
|
try {
|
|
5957
|
-
fh = await
|
|
6715
|
+
fh = await open2(real, fsConstants2.O_RDONLY | fsConstants2.O_NOFOLLOW);
|
|
5958
6716
|
} catch {
|
|
5959
6717
|
return skip("cannot be read");
|
|
5960
6718
|
}
|
|
@@ -5965,7 +6723,7 @@ async function readEntry(root, entry, log, budget) {
|
|
|
5965
6723
|
if (!stat.isFile()) return skip("not a regular file");
|
|
5966
6724
|
if (stat.dev !== proved.dev || stat.ino !== proved.ino) return skip("changed while being read");
|
|
5967
6725
|
if (stat.size > SKILL_BODY_MAX) return skip(`larger than ${SKILL_BODY_MAX} bytes`);
|
|
5968
|
-
bytes = await
|
|
6726
|
+
bytes = await readAtMost2(fh, SKILL_BODY_MAX + 1);
|
|
5969
6727
|
} catch {
|
|
5970
6728
|
return skip("cannot be read");
|
|
5971
6729
|
} finally {
|
|
@@ -5994,9 +6752,9 @@ async function readEntry(root, entry, log, budget) {
|
|
|
5994
6752
|
...parsed.ok && parsed.data.description !== void 0 ? { description: parsed.data.description } : {},
|
|
5995
6753
|
declaredTools: parsed.ok ? parsed.data.tools : [],
|
|
5996
6754
|
permission: parsed.ok ? parsed.data.permission : "inherit",
|
|
5997
|
-
sourcePath:
|
|
6755
|
+
sourcePath: relative6(root.base, path),
|
|
5998
6756
|
...root.projectPath !== void 0 ? { projectPath: root.projectPath } : {},
|
|
5999
|
-
contentHash:
|
|
6757
|
+
contentHash: createHash4("sha256").update(bytes).digest("hex"),
|
|
6000
6758
|
...fileMtime !== void 0 ? { fileMtime } : {},
|
|
6001
6759
|
// The reason only: `refusal.detail` can quote the file's own frontmatter (protocol §2.3).
|
|
6002
6760
|
refusal: refusal?.reason ?? null
|
|
@@ -6080,7 +6838,7 @@ function scalar(raw, prose = false) {
|
|
|
6080
6838
|
return !prose && /:\s/.test(value) || /\s#/.test(value) ? null : value;
|
|
6081
6839
|
}
|
|
6082
6840
|
var SKILL_SCAN_MAX_PER_DIR, SKILL_SCAN_MAX_SKILLS, SKILL_SCAN_MAX_BYTES, defaultIsGitRepo, summariesOf, FRONTMATTER, KEY, ITEM;
|
|
6083
|
-
var
|
|
6841
|
+
var init_scan2 = __esm({
|
|
6084
6842
|
"src/skills/scan.ts"() {
|
|
6085
6843
|
"use strict";
|
|
6086
6844
|
init_src();
|
|
@@ -6091,7 +6849,7 @@ var init_scan = __esm({
|
|
|
6091
6849
|
SKILL_SCAN_MAX_PER_DIR = 500;
|
|
6092
6850
|
SKILL_SCAN_MAX_SKILLS = 500;
|
|
6093
6851
|
SKILL_SCAN_MAX_BYTES = 32 * 1024 * 1024;
|
|
6094
|
-
defaultIsGitRepo = (dir) =>
|
|
6852
|
+
defaultIsGitRepo = (dir) => lstatSync6(join20(dir, ".git"), { throwIfNoEntry: false }) !== void 0;
|
|
6095
6853
|
summariesOf = (scan) => scan.map((s) => s.summary);
|
|
6096
6854
|
FRONTMATTER = /^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/;
|
|
6097
6855
|
KEY = /^([A-Za-z_][A-Za-z0-9_-]*):[ \t]*(.*)$/;
|
|
@@ -6100,17 +6858,17 @@ var init_scan = __esm({
|
|
|
6100
6858
|
});
|
|
6101
6859
|
|
|
6102
6860
|
// src/skills/materialise.ts
|
|
6103
|
-
import { createHash as
|
|
6861
|
+
import { createHash as createHash5 } from "crypto";
|
|
6104
6862
|
import {
|
|
6105
6863
|
chmodSync as chmodSync5,
|
|
6106
|
-
constants as
|
|
6107
|
-
lstatSync as
|
|
6864
|
+
constants as fsConstants3,
|
|
6865
|
+
lstatSync as lstatSync7,
|
|
6108
6866
|
mkdirSync as mkdirSync7,
|
|
6109
6867
|
readdirSync as readdirSync2,
|
|
6110
6868
|
rmSync as rmSync7
|
|
6111
6869
|
} from "fs";
|
|
6112
|
-
import { open as
|
|
6113
|
-
import { dirname as dirname8, isAbsolute as
|
|
6870
|
+
import { open as open3, writeFile } from "fs/promises";
|
|
6871
|
+
import { dirname as dirname8, isAbsolute as isAbsolute13, join as join21, relative as relative7, sep as sep8 } from "path";
|
|
6114
6872
|
function isSkillRefusal(err) {
|
|
6115
6873
|
const msg = err instanceof Error ? err.message : String(err);
|
|
6116
6874
|
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 +6915,7 @@ async function materialiseSkills(scan, t) {
|
|
|
6157
6915
|
}
|
|
6158
6916
|
const path = plainDirectory(t.home, sessionSkillsDir(t.home, t.runnerId, t.sessionId));
|
|
6159
6917
|
await writeFile(
|
|
6160
|
-
|
|
6918
|
+
join21(plainDirectory(t.home, join21(path, ".claude-plugin")), "plugin.json"),
|
|
6161
6919
|
`${JSON.stringify(
|
|
6162
6920
|
{
|
|
6163
6921
|
name: SKILL_PLUGIN_NAME,
|
|
@@ -6187,13 +6945,13 @@ async function materialiseSkills(scan, t) {
|
|
|
6187
6945
|
);
|
|
6188
6946
|
}
|
|
6189
6947
|
try {
|
|
6190
|
-
const dir = plainDirectory(t.home,
|
|
6191
|
-
await writeFile(
|
|
6948
|
+
const dir = plainDirectory(t.home, join21(path, "skills", slug));
|
|
6949
|
+
await writeFile(join21(dir, "SKILL.md"), bytes, { mode: 384, flag: "wx" });
|
|
6192
6950
|
slugs.push(slug);
|
|
6193
6951
|
} catch (err) {
|
|
6194
6952
|
note(log, err, { slug, scope, path: skill.path }, "skill not materialised for this session");
|
|
6195
6953
|
try {
|
|
6196
|
-
rmSync7(assertStagingInsideHome(t.home,
|
|
6954
|
+
rmSync7(assertStagingInsideHome(t.home, join21(path, "skills", slug)), {
|
|
6197
6955
|
recursive: true,
|
|
6198
6956
|
force: true
|
|
6199
6957
|
});
|
|
@@ -6220,7 +6978,7 @@ async function firstAcceptable(candidates, log, markStale) {
|
|
|
6220
6978
|
note(log, err, fields, "skill not materialised for this session");
|
|
6221
6979
|
continue;
|
|
6222
6980
|
}
|
|
6223
|
-
const hash =
|
|
6981
|
+
const hash = createHash5("sha256").update(bytes).digest("hex");
|
|
6224
6982
|
if (hash !== skill.summary.contentHash) {
|
|
6225
6983
|
markStale();
|
|
6226
6984
|
say(
|
|
@@ -6257,21 +7015,21 @@ function assertSealed(path, slugs) {
|
|
|
6257
7015
|
const want = new Set(expected);
|
|
6258
7016
|
for (const entry of entries) {
|
|
6259
7017
|
const ok = kind === "dir" ? entry.isDirectory() : entry.isFile();
|
|
6260
|
-
if (!ok || !want.has(entry.name)) throw stray(
|
|
7018
|
+
if (!ok || !want.has(entry.name)) throw stray(join21(dir, entry.name));
|
|
6261
7019
|
}
|
|
6262
|
-
if (entries.length !== want.size) throw stray(
|
|
7020
|
+
if (entries.length !== want.size) throw stray(join21(dir, [...want].join("|")));
|
|
6263
7021
|
};
|
|
6264
7022
|
named(path, [".claude-plugin", "skills"], "dir");
|
|
6265
|
-
named(
|
|
6266
|
-
named(
|
|
6267
|
-
for (const slug of slugs) named(
|
|
7023
|
+
named(join21(path, ".claude-plugin"), ["plugin.json"], "file");
|
|
7024
|
+
named(join21(path, "skills"), slugs, "dir");
|
|
7025
|
+
for (const slug of slugs) named(join21(path, "skills", slug), ["SKILL.md"], "file");
|
|
6268
7026
|
}
|
|
6269
7027
|
function plainDirectory(home, dir) {
|
|
6270
7028
|
const intended = assertStagingInsideHome(home, dir);
|
|
6271
7029
|
mkdirSync7(intended, { recursive: true, mode: 448 });
|
|
6272
7030
|
const proved = assertStagingInsideHome(home, intended);
|
|
6273
7031
|
for (const p of [dirname8(proved), proved]) {
|
|
6274
|
-
const st =
|
|
7032
|
+
const st = lstatSync7(p);
|
|
6275
7033
|
if (st.isSymbolicLink() || !st.isDirectory()) throw new Error(`${p} is not a plain directory`);
|
|
6276
7034
|
}
|
|
6277
7035
|
chmodSync5(proved, 448);
|
|
@@ -6289,7 +7047,7 @@ async function readBody(skill) {
|
|
|
6289
7047
|
}
|
|
6290
7048
|
let fh;
|
|
6291
7049
|
try {
|
|
6292
|
-
fh = await
|
|
7050
|
+
fh = await open3(real, fsConstants3.O_RDONLY | fsConstants3.O_NOFOLLOW);
|
|
6293
7051
|
} catch {
|
|
6294
7052
|
throw new Error("no longer on disk, or no longer a file the runner may follow");
|
|
6295
7053
|
}
|
|
@@ -6299,7 +7057,7 @@ async function readBody(skill) {
|
|
|
6299
7057
|
stat = await fh.stat();
|
|
6300
7058
|
if (!stat.isFile()) throw new Error("no longer a regular file");
|
|
6301
7059
|
if (stat.size > SKILL_BODY_MAX) throw new Error(`larger than ${SKILL_BODY_MAX} bytes`);
|
|
6302
|
-
bytes = await
|
|
7060
|
+
bytes = await readAtMost2(fh, SKILL_BODY_MAX + 1);
|
|
6303
7061
|
} finally {
|
|
6304
7062
|
await fh.close().catch(() => void 0);
|
|
6305
7063
|
}
|
|
@@ -6313,22 +7071,22 @@ var init_materialise = __esm({
|
|
|
6313
7071
|
init_src();
|
|
6314
7072
|
init_log();
|
|
6315
7073
|
init_attachments2();
|
|
6316
|
-
|
|
6317
|
-
|
|
7074
|
+
init_paths3();
|
|
7075
|
+
init_scan2();
|
|
6318
7076
|
SKILL_PLUGIN_NAME = "cayenne";
|
|
6319
7077
|
SESSION_SKILLS_MAX = 64;
|
|
6320
7078
|
SESSION_SKILLS_MAX_BYTES = 8 * 1024 * 1024;
|
|
6321
7079
|
BUDGET = "SKILL_BUDGET:";
|
|
6322
7080
|
PRECEDENCE = { personal: 0, project: 1, workspace: 2 };
|
|
6323
7081
|
isAtOrBelow = (p, dir) => {
|
|
6324
|
-
const rel =
|
|
6325
|
-
return rel === "" || !rel.startsWith(`..${
|
|
7082
|
+
const rel = relative7(dir, p);
|
|
7083
|
+
return rel === "" || !rel.startsWith(`..${sep8}`) && rel !== ".." && !isAbsolute13(rel);
|
|
6326
7084
|
};
|
|
6327
7085
|
}
|
|
6328
7086
|
});
|
|
6329
7087
|
|
|
6330
7088
|
// src/sessions/repo-settings.ts
|
|
6331
|
-
import { isAbsolute as
|
|
7089
|
+
import { isAbsolute as isAbsolute14, relative as relative8, sep as sep9 } from "path";
|
|
6332
7090
|
function repoSettingsPolicy(input) {
|
|
6333
7091
|
const bypassLock = input.permissionMode === "bypassPermissions" ? {} : { permissions: { disableBypassPermissionsMode: "disable" } };
|
|
6334
7092
|
const claudeMd = () => ({
|
|
@@ -6350,8 +7108,8 @@ function repoSettingsPolicy(input) {
|
|
|
6350
7108
|
return { settingSources: ["user"], ...claudeMd(), settings: { ...bypassLock } };
|
|
6351
7109
|
}
|
|
6352
7110
|
function insideRoot2(candidate, root) {
|
|
6353
|
-
const rel =
|
|
6354
|
-
return rel === "" || rel !== ".." && !rel.startsWith(`..${
|
|
7111
|
+
const rel = relative8(root, candidate);
|
|
7112
|
+
return rel === "" || rel !== ".." && !rel.startsWith(`..${sep9}`) && !isAbsolute14(rel);
|
|
6355
7113
|
}
|
|
6356
7114
|
var init_repo_settings = __esm({
|
|
6357
7115
|
"src/sessions/repo-settings.ts"() {
|
|
@@ -6361,8 +7119,8 @@ var init_repo_settings = __esm({
|
|
|
6361
7119
|
|
|
6362
7120
|
// src/sessions/manager.ts
|
|
6363
7121
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
6364
|
-
import { realpathSync as
|
|
6365
|
-
import { isAbsolute as
|
|
7122
|
+
import { realpathSync as realpathSync11 } from "fs";
|
|
7123
|
+
import { isAbsolute as isAbsolute15, sep as sep10 } from "path";
|
|
6366
7124
|
import { isDeepStrictEqual } from "util";
|
|
6367
7125
|
function narrowToSession(suggestions) {
|
|
6368
7126
|
if (!suggestions) return suggestions;
|
|
@@ -6390,6 +7148,8 @@ var init_manager = __esm({
|
|
|
6390
7148
|
"use strict";
|
|
6391
7149
|
init_src();
|
|
6392
7150
|
init_config();
|
|
7151
|
+
init_paths2();
|
|
7152
|
+
init_service();
|
|
6393
7153
|
init_introspect();
|
|
6394
7154
|
init_watch();
|
|
6395
7155
|
init_worktree();
|
|
@@ -6401,7 +7161,7 @@ var init_manager = __esm({
|
|
|
6401
7161
|
init_attachments2();
|
|
6402
7162
|
init_repo_settings();
|
|
6403
7163
|
roundUsd = (usd) => Math.round(usd * 1e6) / 1e6;
|
|
6404
|
-
isAtOrBelow2 = (p, dir) => p === dir || p.startsWith(dir.endsWith(
|
|
7164
|
+
isAtOrBelow2 = (p, dir) => p === dir || p.startsWith(dir.endsWith(sep10) ? dir : `${dir}${sep10}`);
|
|
6405
7165
|
DENY_MESSAGE_MAX = 2e3;
|
|
6406
7166
|
EXIT_ERROR_MAX = 8 * 1024;
|
|
6407
7167
|
INITIAL_PROMPT_MAX = 2e5;
|
|
@@ -6897,7 +7657,7 @@ var init_manager = __esm({
|
|
|
6897
7657
|
if (stored === null) return false;
|
|
6898
7658
|
if (isAtOrBelow2(stored, target)) return true;
|
|
6899
7659
|
try {
|
|
6900
|
-
return isAtOrBelow2(
|
|
7660
|
+
return isAtOrBelow2(realpathSync11.native(stored), target);
|
|
6901
7661
|
} catch {
|
|
6902
7662
|
return false;
|
|
6903
7663
|
}
|
|
@@ -7155,7 +7915,7 @@ var init_manager = __esm({
|
|
|
7155
7915
|
}
|
|
7156
7916
|
/** A path as the SDK reads it: relative means relative to the session's cwd. */
|
|
7157
7917
|
fromCwd(cwd, value) {
|
|
7158
|
-
return
|
|
7918
|
+
return isAbsolute15(value) ? value : `${cwd}${sep10}${value}`;
|
|
7159
7919
|
}
|
|
7160
7920
|
/**
|
|
7161
7921
|
* §2.9: a path in `updatedInput` passes the same boundary the session's `cwd` did, and the
|
|
@@ -7386,6 +8146,7 @@ var init_manager = __esm({
|
|
|
7386
8146
|
inflight: /* @__PURE__ */ new Set(),
|
|
7387
8147
|
changesVersion: this.nextChangesSeed(),
|
|
7388
8148
|
watcher: null,
|
|
8149
|
+
docs: null,
|
|
7389
8150
|
costLast: 0,
|
|
7390
8151
|
costTotal: null,
|
|
7391
8152
|
costUnsaved: 0,
|
|
@@ -7393,10 +8154,64 @@ var init_manager = __esm({
|
|
|
7393
8154
|
};
|
|
7394
8155
|
this.live.set(sessionId, live);
|
|
7395
8156
|
this.wire(sessionId, live);
|
|
8157
|
+
live.docs = this.watchDocuments(sessionId, live);
|
|
7396
8158
|
live.session.start();
|
|
7397
8159
|
live.watcher = this.watchWorkingCopy(sessionId, live);
|
|
7398
8160
|
return live;
|
|
7399
8161
|
}
|
|
8162
|
+
/**
|
|
8163
|
+
* Arm this session's documents mirror: the service, one pass now, and the watch on
|
|
8164
|
+
* `<cwd>/.cayenne/docs` (lane 5 design §3).
|
|
8165
|
+
*
|
|
8166
|
+
* The directory is never created. Most sessions never write a document, and making it would put
|
|
8167
|
+
* a directory inside the person's repository that they did not ask for; `watchDirs` retries a
|
|
8168
|
+
* directory that is not there and announces on attach, so a file written before it existed is
|
|
8169
|
+
* still picked up — one retry tick plus one debounce, inside the feature's five-second budget.
|
|
8170
|
+
*
|
|
8171
|
+
* Nothing here can fail the session. A service that cannot be built and a watch that cannot be
|
|
8172
|
+
* armed are both a Documents tab that does not update, logged once at warn.
|
|
8173
|
+
*/
|
|
8174
|
+
watchDocuments(sessionId, live) {
|
|
8175
|
+
const make = this.deps.documents ?? createDocumentsService;
|
|
8176
|
+
let service2;
|
|
8177
|
+
try {
|
|
8178
|
+
service2 = make({
|
|
8179
|
+
db: this.deps.db,
|
|
8180
|
+
workspaceId: this.deps.workspaceId,
|
|
8181
|
+
// The runner owner, always: it writes as the member it logged in as, and 0022's session
|
|
8182
|
+
// clause is what hands the row to the member whose chat it is. There is no
|
|
8183
|
+
// insert-on-behalf-of path and this must not grow one.
|
|
8184
|
+
ownerId: this.deps.runnerUserId,
|
|
8185
|
+
sessionId,
|
|
8186
|
+
// The canonical directory the provider was spawned in — the worktree when one applies —
|
|
8187
|
+
// never a path a payload carried.
|
|
8188
|
+
cwd: live.cwd,
|
|
8189
|
+
log: this.deps.log
|
|
8190
|
+
});
|
|
8191
|
+
} catch (err) {
|
|
8192
|
+
this.deps.log.warn({ err, sessionId }, "the documents service could not be built");
|
|
8193
|
+
return null;
|
|
8194
|
+
}
|
|
8195
|
+
const refresh = () => {
|
|
8196
|
+
if (this.live.get(sessionId) !== live) return;
|
|
8197
|
+
void service2.refresh().catch(
|
|
8198
|
+
(err) => this.deps.log.warn({ err, sessionId }, "documents refresh failed")
|
|
8199
|
+
);
|
|
8200
|
+
};
|
|
8201
|
+
const attach = this.deps.watchDocs ?? watchDirs;
|
|
8202
|
+
let watcher = null;
|
|
8203
|
+
try {
|
|
8204
|
+
watcher = attach([docsDir(live.cwd)], refresh, {
|
|
8205
|
+
debounceMs: DOCS_DEBOUNCE_MS,
|
|
8206
|
+
retryMs: DOCS_RETRY_MS,
|
|
8207
|
+
onError: (err) => this.deps.log.warn({ err, sessionId }, "documents watcher error; live refresh stopped")
|
|
8208
|
+
});
|
|
8209
|
+
} catch (err) {
|
|
8210
|
+
this.deps.log.warn({ err, sessionId }, "the documents watcher failed to start");
|
|
8211
|
+
}
|
|
8212
|
+
refresh();
|
|
8213
|
+
return { service: service2, watcher };
|
|
8214
|
+
}
|
|
7400
8215
|
/**
|
|
7401
8216
|
* Watch the directory the provider was spawned in — `live.cwd`, the canonical path `start()` or
|
|
7402
8217
|
* `ensureLive()` proved inside the roots (the worktree, when one applies) — and re-broadcast
|
|
@@ -7660,6 +8475,14 @@ var init_manager = __esm({
|
|
|
7660
8475
|
await Promise.resolve().then(() => watcher?.close()).catch(
|
|
7661
8476
|
(err) => this.deps.log.warn({ err, sessionId }, "closing the working-copy watcher failed")
|
|
7662
8477
|
);
|
|
8478
|
+
const docs = live.docs;
|
|
8479
|
+
live.docs = null;
|
|
8480
|
+
await Promise.resolve().then(async () => {
|
|
8481
|
+
await docs?.watcher?.close();
|
|
8482
|
+
await docs?.service.close();
|
|
8483
|
+
}).catch(
|
|
8484
|
+
(err) => this.deps.log.warn({ err, sessionId }, "closing the documents watch failed")
|
|
8485
|
+
);
|
|
7663
8486
|
if (this.live.get(sessionId) !== live) return;
|
|
7664
8487
|
this.live.delete(sessionId);
|
|
7665
8488
|
this.discardSkills(sessionId);
|
|
@@ -7804,7 +8627,7 @@ var init_manager = __esm({
|
|
|
7804
8627
|
});
|
|
7805
8628
|
|
|
7806
8629
|
// src/skills/mirror.ts
|
|
7807
|
-
function
|
|
8630
|
+
function rowOf2(deps, skill) {
|
|
7808
8631
|
return {
|
|
7809
8632
|
workspace_id: deps.workspaceId,
|
|
7810
8633
|
scope: skill.scope,
|
|
@@ -7872,7 +8695,7 @@ async function syncSkills(deps, found) {
|
|
|
7872
8695
|
const seen = /* @__PURE__ */ new Set();
|
|
7873
8696
|
const rows = [];
|
|
7874
8697
|
for (const skill of found) {
|
|
7875
|
-
const row =
|
|
8698
|
+
const row = rowOf2(deps, skill);
|
|
7876
8699
|
const key = keyOf2({
|
|
7877
8700
|
scope: skill.scope,
|
|
7878
8701
|
slug: skill.slug,
|
|
@@ -7933,7 +8756,7 @@ async function syncSkills(deps, found) {
|
|
|
7933
8756
|
return { upserted, missing };
|
|
7934
8757
|
}
|
|
7935
8758
|
var SKILLS_IDENTITY, SKILLS_MIRROR_MAX_ROWS, keyOf2, instant;
|
|
7936
|
-
var
|
|
8759
|
+
var init_mirror2 = __esm({
|
|
7937
8760
|
"src/skills/mirror.ts"() {
|
|
7938
8761
|
"use strict";
|
|
7939
8762
|
init_log();
|
|
@@ -7945,9 +8768,9 @@ var init_mirror = __esm({
|
|
|
7945
8768
|
});
|
|
7946
8769
|
|
|
7947
8770
|
// src/skills/service.ts
|
|
7948
|
-
import { createHash as
|
|
7949
|
-
import { constants as
|
|
7950
|
-
import { open as
|
|
8771
|
+
import { createHash as createHash6 } from "crypto";
|
|
8772
|
+
import { constants as fsConstants4, realpathSync as realpathSync12 } from "fs";
|
|
8773
|
+
import { open as open4 } from "fs/promises";
|
|
7951
8774
|
function createSkillsService(deps) {
|
|
7952
8775
|
const log = deps.log ?? logger;
|
|
7953
8776
|
const scanner = deps.scanner ?? scanSkills;
|
|
@@ -8008,7 +8831,7 @@ function createSkillsService(deps) {
|
|
|
8008
8831
|
let wanted;
|
|
8009
8832
|
if (payload.scope === "project" && payload.projectPath !== void 0) {
|
|
8010
8833
|
try {
|
|
8011
|
-
wanted =
|
|
8834
|
+
wanted = realpathSync12.native(payload.projectPath);
|
|
8012
8835
|
} catch {
|
|
8013
8836
|
throw missing();
|
|
8014
8837
|
}
|
|
@@ -8046,7 +8869,7 @@ function createSkillsService(deps) {
|
|
|
8046
8869
|
const real = assertInsideScopeDir(skill.path, skill.scopeDir);
|
|
8047
8870
|
let fh;
|
|
8048
8871
|
try {
|
|
8049
|
-
fh = await
|
|
8872
|
+
fh = await open4(real, fsConstants4.O_RDONLY | fsConstants4.O_NOFOLLOW);
|
|
8050
8873
|
} catch {
|
|
8051
8874
|
throw refuse2(`skill ${payload.slug} is no longer on disk`);
|
|
8052
8875
|
}
|
|
@@ -8056,28 +8879,28 @@ function createSkillsService(deps) {
|
|
|
8056
8879
|
stat = await fh.stat();
|
|
8057
8880
|
if (!stat.isFile()) throw refuse2(`skill ${payload.slug} is no longer a file`);
|
|
8058
8881
|
if (stat.size > SKILL_BODY_MAX) throw refuse2("skill body too large");
|
|
8059
|
-
bytes = await
|
|
8882
|
+
bytes = await readAtMost2(fh, SKILL_BODY_MAX + 1);
|
|
8060
8883
|
} finally {
|
|
8061
8884
|
await fh.close().catch(() => void 0);
|
|
8062
8885
|
}
|
|
8063
8886
|
if (bytes.byteLength > SKILL_BODY_MAX) throw refuse2("skill body too large");
|
|
8064
8887
|
return {
|
|
8065
8888
|
body: bytes.toString("utf8"),
|
|
8066
|
-
contentHash:
|
|
8889
|
+
contentHash: createHash6("sha256").update(bytes).digest("hex"),
|
|
8067
8890
|
sourcePath: skill.summary.sourcePath
|
|
8068
8891
|
};
|
|
8069
8892
|
}
|
|
8070
8893
|
};
|
|
8071
8894
|
}
|
|
8072
8895
|
var refuse2, SKILL_LIST_RESULT_MAX;
|
|
8073
|
-
var
|
|
8896
|
+
var init_service2 = __esm({
|
|
8074
8897
|
"src/skills/service.ts"() {
|
|
8075
8898
|
"use strict";
|
|
8076
8899
|
init_src();
|
|
8077
8900
|
init_log();
|
|
8078
|
-
|
|
8079
|
-
|
|
8080
|
-
|
|
8901
|
+
init_mirror2();
|
|
8902
|
+
init_paths3();
|
|
8903
|
+
init_scan2();
|
|
8081
8904
|
refuse2 = (message) => new Error(`VALIDATION: ${message}`);
|
|
8082
8905
|
SKILL_LIST_RESULT_MAX = 1024 * 1024;
|
|
8083
8906
|
}
|
|
@@ -8102,7 +8925,7 @@ function createTerminalChannels({
|
|
|
8102
8925
|
log.warn({ terminalId, type: m.type, result }, "terminal frame not sent");
|
|
8103
8926
|
},
|
|
8104
8927
|
(err) => {
|
|
8105
|
-
log.warn({ terminalId, type: m.type, err:
|
|
8928
|
+
log.warn({ terminalId, type: m.type, err: messageOf2(err) }, "terminal frame not sent");
|
|
8106
8929
|
}
|
|
8107
8930
|
).finally(() => {
|
|
8108
8931
|
entry.pending.delete(sent);
|
|
@@ -8126,9 +8949,9 @@ function createTerminalChannels({
|
|
|
8126
8949
|
const inflight = joining.get(terminalId);
|
|
8127
8950
|
if (inflight) return inflight;
|
|
8128
8951
|
if (joined.has(terminalId)) return;
|
|
8129
|
-
const
|
|
8130
|
-
joining.set(terminalId,
|
|
8131
|
-
return
|
|
8952
|
+
const join24 = attachOne(terminalId).finally(() => joining.delete(terminalId));
|
|
8953
|
+
joining.set(terminalId, join24);
|
|
8954
|
+
return join24;
|
|
8132
8955
|
},
|
|
8133
8956
|
flush,
|
|
8134
8957
|
detach,
|
|
@@ -8142,7 +8965,7 @@ function createTerminalChannels({
|
|
|
8142
8965
|
try {
|
|
8143
8966
|
manager.receive(terminalId, payload, "relay");
|
|
8144
8967
|
} catch (err) {
|
|
8145
|
-
log.warn({ terminalId, err:
|
|
8968
|
+
log.warn({ terminalId, err: messageOf2(err) }, "terminal message refused");
|
|
8146
8969
|
}
|
|
8147
8970
|
});
|
|
8148
8971
|
joined.set(terminalId, { channel, pending: /* @__PURE__ */ new Set() });
|
|
@@ -8173,19 +8996,19 @@ function createTerminalChannels({
|
|
|
8173
8996
|
}
|
|
8174
8997
|
}
|
|
8175
8998
|
}
|
|
8176
|
-
var
|
|
8999
|
+
var messageOf2;
|
|
8177
9000
|
var init_channels = __esm({
|
|
8178
9001
|
"src/terminal/channels.ts"() {
|
|
8179
9002
|
"use strict";
|
|
8180
9003
|
init_src();
|
|
8181
|
-
|
|
9004
|
+
messageOf2 = (err) => err instanceof Error ? err.message : String(err);
|
|
8182
9005
|
}
|
|
8183
9006
|
});
|
|
8184
9007
|
|
|
8185
9008
|
// src/terminal/env.ts
|
|
8186
|
-
import { readFileSync as
|
|
9009
|
+
import { readFileSync as readFileSync8, realpathSync as realpathSync13 } from "fs";
|
|
8187
9010
|
import { lstat } from "fs/promises";
|
|
8188
|
-
import { dirname as dirname9, isAbsolute as
|
|
9011
|
+
import { dirname as dirname9, isAbsolute as isAbsolute16, relative as relative9 } from "path";
|
|
8189
9012
|
function terminalEnv(base, { runnerOwner, claudeConfigDir }) {
|
|
8190
9013
|
const env = {};
|
|
8191
9014
|
for (const [key, value] of Object.entries(base)) {
|
|
@@ -8254,8 +9077,8 @@ function tryRealpath(fs, p) {
|
|
|
8254
9077
|
}
|
|
8255
9078
|
}
|
|
8256
9079
|
function inside2(candidate, root) {
|
|
8257
|
-
const rel =
|
|
8258
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
9080
|
+
const rel = relative9(root, candidate);
|
|
9081
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute16(rel);
|
|
8259
9082
|
}
|
|
8260
9083
|
var SECRET_ENV, MEMBER_ENV_ALLOW, MEMBER_ENV_KEYS, LOCALE_ENV, nodeShellFs;
|
|
8261
9084
|
var init_env = __esm({
|
|
@@ -8280,12 +9103,12 @@ var init_env = __esm({
|
|
|
8280
9103
|
nodeShellFs = {
|
|
8281
9104
|
readEtcShells: () => {
|
|
8282
9105
|
try {
|
|
8283
|
-
return
|
|
9106
|
+
return readFileSync8("/etc/shells", "utf8");
|
|
8284
9107
|
} catch {
|
|
8285
9108
|
return "";
|
|
8286
9109
|
}
|
|
8287
9110
|
},
|
|
8288
|
-
realpath: (p) =>
|
|
9111
|
+
realpath: (p) => realpathSync13.native(p),
|
|
8289
9112
|
lstat: (p) => lstat(p)
|
|
8290
9113
|
};
|
|
8291
9114
|
}
|
|
@@ -9115,7 +9938,7 @@ function createTerminalService({
|
|
|
9115
9938
|
await closeRow(terminalId);
|
|
9116
9939
|
await channels.detach(terminalId);
|
|
9117
9940
|
})().catch((err) => {
|
|
9118
|
-
log.warn({ terminalId, err:
|
|
9941
|
+
log.warn({ terminalId, err: messageOf3(err) }, "terminal exit cleanup failed");
|
|
9119
9942
|
}).finally(() => {
|
|
9120
9943
|
exiting.delete(done);
|
|
9121
9944
|
});
|
|
@@ -9146,7 +9969,7 @@ function createTerminalService({
|
|
|
9146
9969
|
if (!manager.has(terminalId) && !cleanedWhileOpening.has(terminalId)) {
|
|
9147
9970
|
live.delete(terminalId);
|
|
9148
9971
|
await channels.detach(terminalId).catch((detachErr) => {
|
|
9149
|
-
log.warn({ terminalId, err:
|
|
9972
|
+
log.warn({ terminalId, err: messageOf3(detachErr) }, "terminal channel detach failed");
|
|
9150
9973
|
});
|
|
9151
9974
|
await closeRow(terminalId);
|
|
9152
9975
|
}
|
|
@@ -9207,13 +10030,13 @@ function createTerminalService({
|
|
|
9207
10030
|
}
|
|
9208
10031
|
};
|
|
9209
10032
|
}
|
|
9210
|
-
var
|
|
9211
|
-
var
|
|
10033
|
+
var messageOf3;
|
|
10034
|
+
var init_service3 = __esm({
|
|
9212
10035
|
"src/terminal/service.ts"() {
|
|
9213
10036
|
"use strict";
|
|
9214
10037
|
init_manager2();
|
|
9215
10038
|
init_refusals();
|
|
9216
|
-
|
|
10039
|
+
messageOf3 = (err) => err instanceof Error ? err.message : String(err);
|
|
9217
10040
|
}
|
|
9218
10041
|
});
|
|
9219
10042
|
|
|
@@ -9225,7 +10048,7 @@ __export(start_exports, {
|
|
|
9225
10048
|
start: () => start
|
|
9226
10049
|
});
|
|
9227
10050
|
import { homedir as homedir4, hostname } from "os";
|
|
9228
|
-
import { join as
|
|
10051
|
+
import { join as join22 } from "path";
|
|
9229
10052
|
function protectedPaths(claudeConfigDir, home = homedir4()) {
|
|
9230
10053
|
const inHome = [
|
|
9231
10054
|
".claude",
|
|
@@ -9233,12 +10056,12 @@ function protectedPaths(claudeConfigDir, home = homedir4()) {
|
|
|
9233
10056
|
".codex",
|
|
9234
10057
|
".ssh",
|
|
9235
10058
|
".aws",
|
|
9236
|
-
|
|
10059
|
+
join22(".config", "gh"),
|
|
9237
10060
|
".gnupg",
|
|
9238
10061
|
".git-credentials",
|
|
9239
10062
|
".npmrc",
|
|
9240
10063
|
".netrc"
|
|
9241
|
-
].map((p) =>
|
|
10064
|
+
].map((p) => join22(home, p));
|
|
9242
10065
|
return [cayenneHome(), ...claudeConfigDir ? [claudeConfigDir] : [], ...inHome];
|
|
9243
10066
|
}
|
|
9244
10067
|
function logCodex(codex) {
|
|
@@ -9391,7 +10214,12 @@ async function compose(cfg, lock, host, preflight) {
|
|
|
9391
10214
|
promptImages: true,
|
|
9392
10215
|
// D38: this build scans `.cayenne/skills` and answers both `runner.skills.*` kinds, so the
|
|
9393
10216
|
// Skills page can say "this runner cannot list skills" about an older one instead of waiting.
|
|
9394
|
-
skills: true
|
|
10217
|
+
skills: true,
|
|
10218
|
+
// D40: this build mirrors what a session writes under `.cayenne/docs`, so the Documents tab
|
|
10219
|
+
// can say "this runner does not publish documents" about an older one instead of "none yet".
|
|
10220
|
+
// Nothing else is composed here: the mirror is one service per live session, built by the
|
|
10221
|
+
// manager from the directory the provider actually runs in (lane 5 design §3).
|
|
10222
|
+
documents: true
|
|
9395
10223
|
});
|
|
9396
10224
|
const registered = capabilities();
|
|
9397
10225
|
let loggedCodex = null;
|
|
@@ -9478,9 +10306,9 @@ async function compose(cfg, lock, host, preflight) {
|
|
|
9478
10306
|
// The roster's own array, mutated in place: `serves` reads the live list with no restart.
|
|
9479
10307
|
allowedMemberIds: liveRoster.allowedMemberIds,
|
|
9480
10308
|
// Outside every profile and repository: the creator of a worktree is the machine's record.
|
|
9481
|
-
worktreeOwners: createWorktreeOwners(
|
|
10309
|
+
worktreeOwners: createWorktreeOwners(join22(cayenneHome(), "worktrees")),
|
|
9482
10310
|
// Beside it: where each session was spawned, the only source of a resumed directory.
|
|
9483
|
-
sessionWorktrees: createSessionWorktrees(
|
|
10311
|
+
sessionWorktrees: createSessionWorktrees(join22(cayenneHome(), "session-worktrees")),
|
|
9484
10312
|
roots,
|
|
9485
10313
|
allowBypass: cfg.allowBypassPermissions,
|
|
9486
10314
|
trustedRoots,
|
|
@@ -9775,22 +10603,22 @@ var init_start = __esm({
|
|
|
9775
10603
|
init_attachments2();
|
|
9776
10604
|
init_handlers();
|
|
9777
10605
|
init_manager();
|
|
9778
|
-
|
|
9779
|
-
|
|
10606
|
+
init_paths3();
|
|
10607
|
+
init_service2();
|
|
9780
10608
|
init_supabase();
|
|
9781
10609
|
init_channels();
|
|
9782
10610
|
init_manager2();
|
|
9783
10611
|
init_process_group();
|
|
9784
10612
|
init_pty();
|
|
9785
|
-
|
|
10613
|
+
init_service3();
|
|
9786
10614
|
init_version();
|
|
9787
10615
|
}
|
|
9788
10616
|
});
|
|
9789
10617
|
|
|
9790
10618
|
// src/cli.ts
|
|
9791
|
-
import { existsSync as existsSync6, mkdirSync as mkdirSync8, readFileSync as
|
|
10619
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync8, readFileSync as readFileSync9, realpathSync as realpathSync14, rmSync as rmSync8, writeFileSync as writeFileSync6 } from "fs";
|
|
9792
10620
|
import { homedir as homedir5, hostname as hostname2, userInfo } from "os";
|
|
9793
|
-
import { dirname as dirname10, join as
|
|
10621
|
+
import { dirname as dirname10, join as join23 } from "path";
|
|
9794
10622
|
import { Command } from "commander";
|
|
9795
10623
|
import prompts from "prompts";
|
|
9796
10624
|
|
|
@@ -10392,7 +11220,7 @@ program.command("doctor").description("Check the install and say how to fix what
|
|
|
10392
11220
|
if (checks.some((c) => !c.ok)) process.exitCode = 1;
|
|
10393
11221
|
});
|
|
10394
11222
|
program.command("logs").description("Show the service log").option("-f, --follow", "keep streaming").action(async (opts) => {
|
|
10395
|
-
const file =
|
|
11223
|
+
const file = join23(cayenneDir(), "logs", "runner.log");
|
|
10396
11224
|
if (!existsSync6(file)) throw new Error(`no log yet at ${file}. Run \`cayenne up\` first.`);
|
|
10397
11225
|
const print = (chunk) => {
|
|
10398
11226
|
for (const line of chunk.split("\n")) {
|
|
@@ -10405,12 +11233,12 @@ program.command("logs").description("Show the service log").option("-f, --follow
|
|
|
10405
11233
|
}
|
|
10406
11234
|
}
|
|
10407
11235
|
};
|
|
10408
|
-
print(
|
|
11236
|
+
print(readFileSync9(file, "utf8"));
|
|
10409
11237
|
if (opts.follow) {
|
|
10410
11238
|
const { watch: watch2 } = await import("fs");
|
|
10411
|
-
let size =
|
|
11239
|
+
let size = readFileSync9(file).length;
|
|
10412
11240
|
watch2(file, () => {
|
|
10413
|
-
const buf =
|
|
11241
|
+
const buf = readFileSync9(file);
|
|
10414
11242
|
if (buf.length > size) print(buf.subarray(size).toString("utf8"));
|
|
10415
11243
|
size = buf.length;
|
|
10416
11244
|
});
|
|
@@ -10438,7 +11266,7 @@ program.command("update").description("Re-pin the installed app and restart the
|
|
|
10438
11266
|
cliPath: record.cliPath,
|
|
10439
11267
|
profile: currentProfile(),
|
|
10440
11268
|
pathEnv: record.pathEnv,
|
|
10441
|
-
logDir:
|
|
11269
|
+
logDir: join23(dir, "logs")
|
|
10442
11270
|
})
|
|
10443
11271
|
);
|
|
10444
11272
|
await launchd.bootout(label);
|
|
@@ -10454,12 +11282,12 @@ program.command("uninstall").description("Stop the service and remove the launch
|
|
|
10454
11282
|
console.log(`removed ${label}`);
|
|
10455
11283
|
return;
|
|
10456
11284
|
}
|
|
10457
|
-
const dir =
|
|
10458
|
-
const home =
|
|
11285
|
+
const dir = realpathSync14(cayenneDir());
|
|
11286
|
+
const home = realpathSync14(homedir5());
|
|
10459
11287
|
if (dir === "/" || dir === home || home.startsWith(`${dir}/`)) {
|
|
10460
11288
|
throw new Error(`refusing to purge ${dir}: it is the home directory or an ancestor of it`);
|
|
10461
11289
|
}
|
|
10462
|
-
if (!existsSync6(
|
|
11290
|
+
if (!existsSync6(join23(dir, "config.json")) && !existsSync6(join23(dir, "app", "install.json"))) {
|
|
10463
11291
|
throw new Error(`refusing to purge ${dir}: it does not look like a Cayenne profile`);
|
|
10464
11292
|
}
|
|
10465
11293
|
if (!opts.yes) {
|