sealkeep 0.9.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +163 -14
- package/CHANGELOG.md +252 -1
- package/CONTROL_PLANE.md +2 -2
- package/LICENSE +1 -1
- package/README.md +108 -22
- package/THIRD_PARTY.md +2 -2
- package/THREAT_MODEL.md +23 -4
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/chunk-access.d.ts +26 -4
- package/dist/packages/sealkeep-crypto/src/chunk-access.js +219 -0
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/envelope.d.ts +11 -1
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/envelope.js +58 -21
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/format.d.ts +2 -2
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/index.d.ts +1 -0
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/index.js +1 -0
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/recipients.d.ts +1 -0
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/recipients.js +53 -13
- package/dist/packages/sealkeep-crypto/src/squeeze.d.ts +6 -0
- package/dist/packages/sealkeep-crypto/src/squeeze.js +39 -0
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/stream.d.ts +36 -3
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/stream.js +196 -37
- package/dist/site/index.html +1808 -1904
- package/dist/site/llms.txt +67 -0
- package/dist/site/trust/architecture-data-flow.html +53 -0
- package/dist/site/trust/audit-roadmap.html +37 -0
- package/dist/site/trust/deployment-responsibility.html +11 -0
- package/dist/site/trust/dpa-sample.html +30 -0
- package/dist/site/trust/release-provenance.html +21 -0
- package/dist/site/trust/subprocessors.html +15 -0
- package/dist/site/trust/threat-model.html +34 -0
- package/dist/site/trust/toms.html +41 -0
- package/dist/site/trust-document.css +32 -0
- package/dist/site/trust.html +73 -0
- package/dist/site/visual/assets/index-8Nxnkc7o.js +207 -0
- package/dist/site/visual/assets/index-oxLy1bpy.css +1 -0
- package/dist/site/visual/index.html +18 -0
- package/dist/site.zip +0 -0
- package/dist/src/activity.d.ts +9 -0
- package/dist/src/activity.js +90 -1
- package/dist/src/adapters.d.ts +175 -5
- package/dist/src/adapters.js +961 -73
- package/dist/src/agent-context.d.ts +135 -0
- package/dist/src/agent-context.js +1059 -0
- package/dist/src/archive-copies.d.ts +47 -0
- package/dist/src/archive-copies.js +179 -0
- package/dist/src/audit.d.ts +1 -1
- package/dist/src/audit.js +29 -4
- package/dist/src/autopilot.d.ts +52 -7
- package/dist/src/autopilot.js +143 -25
- package/dist/src/background-bandwidth.d.ts +46 -0
- package/dist/src/background-bandwidth.js +301 -0
- package/dist/src/background-cpu.d.ts +82 -0
- package/dist/src/background-cpu.js +212 -0
- package/dist/src/background-worker-error.d.ts +12 -0
- package/dist/src/background-worker-error.js +18 -0
- package/dist/src/branding.d.ts +24 -1
- package/dist/src/branding.js +31 -1
- package/dist/src/bridge.d.ts +233 -0
- package/dist/src/bridge.js +604 -0
- package/dist/src/byte-stream.d.ts +91 -0
- package/dist/src/byte-stream.js +385 -0
- package/dist/src/chunk-store.d.ts +41 -8
- package/dist/src/chunk-store.js +161 -65
- package/dist/src/cli.js +1698 -163
- package/dist/src/cloud.d.ts +841 -31
- package/dist/src/cloud.js +3196 -277
- package/dist/src/context-background.d.ts +37 -0
- package/dist/src/context-background.js +309 -0
- package/dist/src/context-drain-child.d.ts +1 -0
- package/dist/src/context-drain-child.js +98 -0
- package/dist/src/context-reader.d.ts +118 -0
- package/dist/src/context-reader.js +447 -0
- package/dist/src/control-plane/auth.d.ts +32 -4
- package/dist/src/control-plane/auth.js +85 -24
- package/dist/src/control-plane/server.js +19 -6
- package/dist/src/control-plane.d.ts +17 -1
- package/dist/src/control-plane.js +32 -6
- package/dist/src/crypto.d.ts +1 -1
- package/dist/src/crypto.js +5 -5
- package/dist/src/daemon-lease.d.ts +70 -0
- package/dist/src/daemon-lease.js +420 -0
- package/dist/src/daemon.d.ts +94 -1
- package/dist/src/daemon.js +1082 -105
- package/dist/src/darwin-service-policy.d.ts +41 -0
- package/dist/src/darwin-service-policy.js +60 -0
- package/dist/src/dashboard-cli.js +15 -15
- package/dist/src/device-authorization.d.ts +37 -0
- package/dist/src/device-authorization.js +199 -0
- package/dist/src/device-enrollment.d.ts +91 -0
- package/dist/src/device-enrollment.js +349 -0
- package/dist/src/disk.d.ts +17 -12
- package/dist/src/disk.js +43 -17
- package/dist/src/doctor.d.ts +35 -1
- package/dist/src/doctor.js +332 -41
- package/dist/src/durable-ticket-lock.d.ts +24 -0
- package/dist/src/durable-ticket-lock.js +232 -0
- package/dist/src/enroll.d.ts +1 -1
- package/dist/src/enroll.js +13 -7
- package/dist/src/env.d.ts +10 -1
- package/dist/src/env.js +11 -3
- package/dist/src/errors.d.ts +8 -8
- package/dist/src/errors.js +6 -6
- package/dist/src/flush.d.ts +12 -0
- package/dist/src/flush.js +37 -0
- package/dist/src/heartbeat.d.ts +86 -12
- package/dist/src/heartbeat.js +415 -29
- package/dist/src/index-background-watchdog.d.ts +1 -0
- package/dist/src/index-background-watchdog.js +94 -0
- package/dist/src/index-background-work.d.ts +21 -0
- package/dist/src/index-background-work.js +25 -0
- package/dist/src/index-background.d.ts +64 -0
- package/dist/src/index-background.js +394 -0
- package/dist/src/index-build-child.d.ts +1 -0
- package/dist/src/index-build-child.js +109 -0
- package/dist/src/index-manifest.d.ts +52 -0
- package/dist/src/index-manifest.js +444 -0
- package/dist/src/index-publication-proof.d.ts +84 -0
- package/dist/src/index-publication-proof.js +380 -0
- package/dist/src/index-publication-state.d.ts +149 -0
- package/dist/src/index-publication-state.js +696 -0
- package/dist/src/index-publication-verifier.d.ts +89 -0
- package/dist/src/index-publication-verifier.js +341 -0
- package/dist/src/index-publish.d.ts +62 -0
- package/dist/src/index-publish.js +540 -0
- package/dist/src/index-scratch-cleanup.d.ts +19 -0
- package/dist/src/index-scratch-cleanup.js +166 -0
- package/dist/src/index-segment-types.d.ts +132 -0
- package/dist/src/index-segment-types.js +21 -0
- package/dist/src/index-segments.d.ts +9 -0
- package/dist/src/index-segments.js +516 -0
- package/dist/src/index-store.d.ts +123 -0
- package/dist/src/index-store.js +495 -0
- package/dist/src/index-sync.d.ts +91 -15
- package/dist/src/index-sync.js +286 -53
- package/dist/src/index-upgrade-publication.d.ts +30 -0
- package/dist/src/index-upgrade-publication.js +179 -0
- package/dist/src/integration-manager.d.ts +32 -0
- package/dist/src/integration-manager.js +394 -0
- package/dist/src/leakscan.js +1 -1
- package/dist/src/local-api.d.ts +56 -15
- package/dist/src/local-api.js +4979 -541
- package/dist/src/machine-settings.d.ts +51 -0
- package/dist/src/machine-settings.js +166 -0
- package/dist/src/managed-chunks.d.ts +5 -2
- package/dist/src/managed-chunks.js +14 -14
- package/dist/src/mcp-install.d.ts +11 -9
- package/dist/src/mcp-install.js +73 -28
- package/dist/src/mcp-workspace.d.ts +18 -0
- package/dist/src/mcp-workspace.js +50 -0
- package/dist/src/mcp.js +294 -25
- package/dist/src/migrate.js +27 -21
- package/dist/src/notify.d.ts +1 -1
- package/dist/src/notify.js +5 -5
- package/dist/src/offload.d.ts +201 -14
- package/dist/src/offload.js +1848 -140
- package/dist/src/onboarding.d.ts +8 -1
- package/dist/src/onboarding.js +4 -4
- package/dist/src/packages.d.ts +2 -2
- package/dist/src/packages.js +10 -2
- package/dist/src/passkey.d.ts +0 -1
- package/dist/src/passkey.js +2 -7
- package/dist/src/password-lock.d.ts +2 -2
- package/dist/src/password-lock.js +6 -6
- package/dist/src/paths.d.ts +2 -0
- package/dist/src/paths.js +2 -0
- package/dist/src/presence.d.ts +86 -0
- package/dist/src/presence.js +240 -0
- package/dist/src/progress-deadline.d.ts +21 -0
- package/dist/src/progress-deadline.js +91 -0
- package/dist/src/project-repair.d.ts +55 -0
- package/dist/src/project-repair.js +131 -0
- package/dist/src/providers/gcs.d.ts +28 -7
- package/dist/src/providers/gcs.js +35 -24
- package/dist/src/providers/gdrive.d.ts +71 -8
- package/dist/src/providers/gdrive.js +223 -50
- package/dist/src/providers/index.d.ts +11 -3
- package/dist/src/providers/index.js +30 -10
- package/dist/src/providers/s3.d.ts +30 -8
- package/dist/src/providers/s3.js +41 -30
- package/dist/src/providers/safe-storage-fetch.d.ts +12 -0
- package/dist/src/providers/safe-storage-fetch.js +72 -0
- package/dist/src/queue.d.ts +136 -19
- package/dist/src/queue.js +862 -96
- package/dist/src/reclaim-transaction.d.ts +156 -0
- package/dist/src/reclaim-transaction.js +1027 -0
- package/dist/src/recovery-codes.d.ts +32 -0
- package/dist/src/recovery-codes.js +338 -0
- package/dist/src/recovery.js +12 -9
- package/dist/src/rehydrate.d.ts +25 -22
- package/dist/src/rehydrate.js +319 -23
- package/dist/src/restore.d.ts +57 -4
- package/dist/src/restore.js +272 -36
- package/dist/src/resume-stub.d.ts +92 -0
- package/dist/src/resume-stub.js +417 -0
- package/dist/src/retention.d.ts +98 -7
- package/dist/src/retention.js +1066 -62
- package/dist/src/rotate.js +3 -3
- package/dist/src/search.d.ts +566 -8
- package/dist/src/search.js +5940 -297
- package/dist/src/secrets.d.ts +51 -7
- package/dist/src/secrets.js +316 -24
- package/dist/src/service.d.ts +49 -11
- package/dist/src/service.js +776 -35
- package/dist/src/share.js +3 -3
- package/dist/src/shared-spaces.d.ts +98 -0
- package/dist/src/shared-spaces.js +214 -0
- package/dist/src/source-reader.d.ts +73 -0
- package/dist/src/source-reader.js +715 -0
- package/dist/src/spool.d.ts +1 -1
- package/dist/src/spool.js +1 -1
- package/dist/src/start-tui.js +2 -1
- package/dist/src/start.js +2 -2
- package/dist/src/storage-endpoint.d.ts +21 -0
- package/dist/src/storage-endpoint.js +122 -0
- package/dist/src/storage-setup.js +12 -12
- package/dist/src/storage-targets.d.ts +109 -6
- package/dist/src/storage-targets.js +975 -67
- package/dist/src/stream-to-cloud.d.ts +5 -1
- package/dist/src/stream-to-cloud.js +34 -14
- package/dist/src/sync-rules.d.ts +31 -6
- package/dist/src/sync-rules.js +153 -14
- package/dist/src/team-backfill-scheduling.d.ts +8 -0
- package/dist/src/team-backfill-scheduling.js +33 -0
- package/dist/src/team-backfill.d.ts +116 -0
- package/dist/src/team-backfill.js +1429 -0
- package/dist/src/team-index-cache.d.ts +16 -0
- package/dist/src/team-index-cache.js +152 -0
- package/dist/src/team-offboarding.d.ts +38 -0
- package/dist/src/team-offboarding.js +1043 -0
- package/dist/src/team-presence.d.ts +127 -0
- package/dist/src/team-presence.js +904 -0
- package/dist/src/team-publication-policy.d.ts +20 -0
- package/dist/src/team-publication-policy.js +140 -0
- package/dist/src/team-realtime.d.ts +68 -0
- package/dist/src/team-realtime.js +816 -0
- package/dist/src/team-source-facts-cache.d.ts +23 -0
- package/dist/src/team-source-facts-cache.js +255 -0
- package/dist/src/trash.d.ts +1 -1
- package/dist/src/trash.js +2 -2
- package/dist/src/tui.js +11 -12
- package/dist/src/types.d.ts +173 -7
- package/dist/src/types.js +20 -0
- package/dist/src/ui-server.d.ts +163 -35
- package/dist/src/ui-server.js +712 -72
- package/dist/src/ui.d.ts +1 -2
- package/dist/src/ui.js +1 -2
- package/dist/src/upload.d.ts +27 -0
- package/dist/src/upload.js +383 -43
- package/dist/src/vault.d.ts +226 -30
- package/dist/src/vault.js +1776 -192
- package/dist/src/watcher.d.ts +7 -1
- package/dist/src/watcher.js +198 -55
- package/dist/src/worker.d.ts +27 -3
- package/dist/src/worker.js +274 -55
- package/package.json +33 -12
- package/scripts/native-reboot-rehearsal.mjs +90 -0
- package/web/app.js +6032 -343
- package/web/bootstrap.js +17 -0
- package/web/index.html +255 -57
- package/web/rail.js +317 -40
- package/web/retention.html +2 -2
- package/web/rules-view.js +188 -16
- package/web/sessions-view.js +485 -62
- package/web/sessions.html +2 -2
- package/web/setup-api.js +152 -29
- package/web/setup-logic.js +68 -9
- package/web/setup.html +113 -44
- package/web/setup.js +604 -71
- package/web/style.css +513 -98
- package/dist/packages/vaultline-crypto/src/chunk-access.js +0 -93
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/aead.d.ts +0 -0
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/aead.js +0 -0
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/format.js +0 -0
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/sha256-stream.d.ts +0 -0
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/sha256-stream.js +0 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { AgentId } from "./adapters.js";
|
|
2
|
+
import { type TrashResult } from "./trash.js";
|
|
3
|
+
export type LocalSettings = {
|
|
4
|
+
version: 1;
|
|
5
|
+
deviceLabel: string;
|
|
6
|
+
watchedAgents: AgentId[];
|
|
7
|
+
/** Reserved for a future adapter-aware custom-root model; not exposed by the current UI. */
|
|
8
|
+
watchedPaths: string[];
|
|
9
|
+
exclusions: string[];
|
|
10
|
+
reclaimEnabled: boolean;
|
|
11
|
+
trashStrategy: "auto" | TrashResult["strategy"];
|
|
12
|
+
schedule: {
|
|
13
|
+
intervalMinutes: number;
|
|
14
|
+
};
|
|
15
|
+
/** Sustained aggregate background network limit in megabits/second. null = safe automatic limit. */
|
|
16
|
+
bandwidth: {
|
|
17
|
+
uploadLimitMbps: number | null;
|
|
18
|
+
};
|
|
19
|
+
/** MB the disk guard always leaves free. null = automatic. */
|
|
20
|
+
diskReserveMb: number | null;
|
|
21
|
+
/**
|
|
22
|
+
* How long a verified encrypted archive remains cached on this machine.
|
|
23
|
+
* This never applies to the original Codex/Claude transcript. null keeps
|
|
24
|
+
* every sealed copy locally; 0 lets a verified duplicate leave immediately.
|
|
25
|
+
*/
|
|
26
|
+
localArchiveCacheDays: number | null;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* True when an automatic watcher must leave this transcript alone.
|
|
30
|
+
*
|
|
31
|
+
* Entries without wildcards name either one file or a whole directory tree.
|
|
32
|
+
* Entries with `*`, `**`, or `?` use conventional path-glob semantics. This
|
|
33
|
+
* matcher is intentionally about transcript storage paths, not project cwd:
|
|
34
|
+
* the live watcher knows the former before it reads any transcript metadata.
|
|
35
|
+
*/
|
|
36
|
+
export declare function transcriptPathIsExcluded(path: string, exclusions: readonly string[], home?: string): boolean;
|
|
37
|
+
/** One policy check shared by filesystem events, lifecycle hooks, and workers. */
|
|
38
|
+
export declare function automaticTranscriptIsEnabled(settings: Pick<LocalSettings, "watchedAgents" | "exclusions">, agent: string, path: string, home?: string): boolean;
|
|
39
|
+
/** Safe defaults: no deletion and no exclusion until the person asks. */
|
|
40
|
+
export declare function defaultLocalSettings(): LocalSettings;
|
|
41
|
+
/** Automatic work must not infer "unlimited" from an older settings file.
|
|
42
|
+
* 8 Mbps stays below common 10–20 Mbps uplinks while a large archive still
|
|
43
|
+
* makes steady progress. The local Settings page exposes this default. */
|
|
44
|
+
export declare const DEFAULT_BACKGROUND_UPLOAD_LIMIT_MBPS = 8;
|
|
45
|
+
export declare function backgroundUploadBytesPerSecond(settings: Pick<LocalSettings, "bandwidth">): number;
|
|
46
|
+
export declare const localSettingsPath: (dataDir: string) => string;
|
|
47
|
+
/** Missing/older fields inherit safe defaults; this file is preferences, not cryptographic state. */
|
|
48
|
+
export declare function readLocalSettings(dataDir: string): Promise<LocalSettings>;
|
|
49
|
+
/** Atomic and owner-only: per-machine preferences are never synced. */
|
|
50
|
+
export declare function writeLocalSettings(dataDir: string, settings: LocalSettings): Promise<void>;
|
|
51
|
+
export declare function resolvedTrashStrategy(settings: LocalSettings): TrashResult["strategy"];
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import { homedir, hostname } from "node:os";
|
|
4
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
5
|
+
import { defaultStrategy } from "./trash.js";
|
|
6
|
+
/**
|
|
7
|
+
* Turns a person-entered exclusion into the same absolute, slash-normalised
|
|
8
|
+
* shape used for transcript candidates. `~` is useful in a machine-local
|
|
9
|
+
* setting, and a relative entry is deliberately relative to that machine's
|
|
10
|
+
* home rather than to whichever directory happened to launch the daemon.
|
|
11
|
+
*/
|
|
12
|
+
function normaliseExclusion(value, home) {
|
|
13
|
+
const trimmed = value.trim();
|
|
14
|
+
const expanded = trimmed === "~"
|
|
15
|
+
? home
|
|
16
|
+
: trimmed.startsWith("~/") || trimmed.startsWith("~\\")
|
|
17
|
+
? join(home, trimmed.slice(2))
|
|
18
|
+
: trimmed;
|
|
19
|
+
return (isAbsolute(expanded) ? resolve(expanded) : resolve(home, expanded)).replaceAll("\\", "/");
|
|
20
|
+
}
|
|
21
|
+
function escapeRegexCharacter(character) {
|
|
22
|
+
return /[\\^$.*+?()[\]{}|]/.test(character) ? `\\${character}` : character;
|
|
23
|
+
}
|
|
24
|
+
/** A small path-only glob: `*` stays in one directory, `**` crosses them. */
|
|
25
|
+
function globRegex(pattern) {
|
|
26
|
+
let source = "^";
|
|
27
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
28
|
+
const character = pattern[index];
|
|
29
|
+
if (character === "*" && pattern[index + 1] === "*") {
|
|
30
|
+
index += 1;
|
|
31
|
+
// `foo/**/bar` also matches `foo/bar`; a plain terminal `/**` matches
|
|
32
|
+
// descendants, while the directory itself is handled by the prefix
|
|
33
|
+
// check in transcriptPathIsExcluded.
|
|
34
|
+
if (pattern[index + 1] === "/") {
|
|
35
|
+
index += 1;
|
|
36
|
+
source += "(?:.*/)?";
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
source += ".*";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
else if (character === "*") {
|
|
43
|
+
source += "[^/]*";
|
|
44
|
+
}
|
|
45
|
+
else if (character === "?") {
|
|
46
|
+
source += "[^/]";
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
source += escapeRegexCharacter(character);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
source += "$";
|
|
53
|
+
return new RegExp(source, process.platform === "win32" ? "i" : "");
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* True when an automatic watcher must leave this transcript alone.
|
|
57
|
+
*
|
|
58
|
+
* Entries without wildcards name either one file or a whole directory tree.
|
|
59
|
+
* Entries with `*`, `**`, or `?` use conventional path-glob semantics. This
|
|
60
|
+
* matcher is intentionally about transcript storage paths, not project cwd:
|
|
61
|
+
* the live watcher knows the former before it reads any transcript metadata.
|
|
62
|
+
*/
|
|
63
|
+
export function transcriptPathIsExcluded(path, exclusions, home = process.env.HOME ?? homedir()) {
|
|
64
|
+
const candidate = resolve(path).replaceAll("\\", "/");
|
|
65
|
+
const foldedCandidate = process.platform === "win32" ? candidate.toLowerCase() : candidate;
|
|
66
|
+
for (const raw of exclusions) {
|
|
67
|
+
if (!raw.trim())
|
|
68
|
+
continue;
|
|
69
|
+
const pattern = normaliseExclusion(raw, home).replace(/\/+$/, "");
|
|
70
|
+
if (!pattern)
|
|
71
|
+
continue;
|
|
72
|
+
if (/[*?]/.test(pattern)) {
|
|
73
|
+
if (globRegex(pattern).test(candidate))
|
|
74
|
+
return true;
|
|
75
|
+
// `/private/**` is normally understood to include the directory itself,
|
|
76
|
+
// which lets discovery prune that branch before walking its files.
|
|
77
|
+
if (pattern.endsWith("/**")) {
|
|
78
|
+
const directory = pattern.slice(0, -3);
|
|
79
|
+
const foldedDirectory = process.platform === "win32" ? directory.toLowerCase() : directory;
|
|
80
|
+
if (foldedCandidate === foldedDirectory)
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const foldedPattern = process.platform === "win32" ? pattern.toLowerCase() : pattern;
|
|
86
|
+
if (foldedCandidate === foldedPattern || foldedCandidate.startsWith(`${foldedPattern}/`))
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
/** One policy check shared by filesystem events, lifecycle hooks, and workers. */
|
|
92
|
+
export function automaticTranscriptIsEnabled(settings, agent, path, home = process.env.HOME ?? homedir()) {
|
|
93
|
+
return (agent === "codex" || agent === "claude")
|
|
94
|
+
&& settings.watchedAgents.includes(agent)
|
|
95
|
+
&& !transcriptPathIsExcluded(path, settings.exclusions, home);
|
|
96
|
+
}
|
|
97
|
+
/** Safe defaults: no deletion and no exclusion until the person asks. */
|
|
98
|
+
export function defaultLocalSettings() {
|
|
99
|
+
return {
|
|
100
|
+
version: 1,
|
|
101
|
+
deviceLabel: hostname(),
|
|
102
|
+
watchedAgents: ["codex", "claude"],
|
|
103
|
+
watchedPaths: [],
|
|
104
|
+
exclusions: [],
|
|
105
|
+
reclaimEnabled: false,
|
|
106
|
+
trashStrategy: "auto",
|
|
107
|
+
schedule: { intervalMinutes: 5 },
|
|
108
|
+
bandwidth: { uploadLimitMbps: null },
|
|
109
|
+
diskReserveMb: null,
|
|
110
|
+
// A week keeps recent memory restorable while offline without allowing a
|
|
111
|
+
// second, encrypted copy of the whole history to grow forever on disk.
|
|
112
|
+
localArchiveCacheDays: 7,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/** Automatic work must not infer "unlimited" from an older settings file.
|
|
116
|
+
* 8 Mbps stays below common 10–20 Mbps uplinks while a large archive still
|
|
117
|
+
* makes steady progress. The local Settings page exposes this default. */
|
|
118
|
+
export const DEFAULT_BACKGROUND_UPLOAD_LIMIT_MBPS = 8;
|
|
119
|
+
export function backgroundUploadBytesPerSecond(settings) {
|
|
120
|
+
const configured = settings.bandwidth.uploadLimitMbps;
|
|
121
|
+
const mbps = typeof configured === "number" && Number.isFinite(configured) && configured > 0
|
|
122
|
+
? configured
|
|
123
|
+
: DEFAULT_BACKGROUND_UPLOAD_LIMIT_MBPS;
|
|
124
|
+
return mbps * 1_000_000 / 8;
|
|
125
|
+
}
|
|
126
|
+
export const localSettingsPath = (dataDir) => join(dataDir, "local-settings.json");
|
|
127
|
+
/** Missing/older fields inherit safe defaults; this file is preferences, not cryptographic state. */
|
|
128
|
+
export async function readLocalSettings(dataDir) {
|
|
129
|
+
const fallback = defaultLocalSettings();
|
|
130
|
+
try {
|
|
131
|
+
const saved = JSON.parse(await readFile(localSettingsPath(dataDir), "utf8"));
|
|
132
|
+
return {
|
|
133
|
+
...fallback,
|
|
134
|
+
...saved,
|
|
135
|
+
watchedAgents: Array.isArray(saved?.watchedAgents)
|
|
136
|
+
? saved.watchedAgents.filter((agent) => agent === "codex" || agent === "claude")
|
|
137
|
+
: fallback.watchedAgents,
|
|
138
|
+
watchedPaths: Array.isArray(saved?.watchedPaths) ? saved.watchedPaths.filter((path) => typeof path === "string") : [],
|
|
139
|
+
exclusions: Array.isArray(saved?.exclusions) ? saved.exclusions.filter((path) => typeof path === "string") : [],
|
|
140
|
+
schedule: { ...fallback.schedule, ...saved?.schedule },
|
|
141
|
+
bandwidth: { ...fallback.bandwidth, ...saved?.bandwidth },
|
|
142
|
+
localArchiveCacheDays: saved?.localArchiveCacheDays === null
|
|
143
|
+
? null
|
|
144
|
+
: typeof saved?.localArchiveCacheDays === "number"
|
|
145
|
+
&& Number.isFinite(saved.localArchiveCacheDays)
|
|
146
|
+
&& saved.localArchiveCacheDays >= 0
|
|
147
|
+
&& saved.localArchiveCacheDays <= 3650
|
|
148
|
+
? saved.localArchiveCacheDays
|
|
149
|
+
: fallback.localArchiveCacheDays,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return fallback;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/** Atomic and owner-only: per-machine preferences are never synced. */
|
|
157
|
+
export async function writeLocalSettings(dataDir, settings) {
|
|
158
|
+
await mkdir(dataDir, { recursive: true, mode: 0o700 });
|
|
159
|
+
const target = localSettingsPath(dataDir);
|
|
160
|
+
const temp = `${target}.${randomBytes(9).toString("hex")}.tmp`;
|
|
161
|
+
await writeFile(temp, JSON.stringify(settings, null, 2) + "\n", { mode: 0o600 });
|
|
162
|
+
await rename(temp, target);
|
|
163
|
+
}
|
|
164
|
+
export function resolvedTrashStrategy(settings) {
|
|
165
|
+
return settings.trashStrategy === "auto" ? defaultStrategy() : settings.trashStrategy;
|
|
166
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ChunkStoreClient } from "./chunk-store.js";
|
|
2
|
+
import { type ManagedDeleteContext } from "./cloud.js";
|
|
2
3
|
/**
|
|
3
4
|
* Managed chunk folders, Paul's way: no chunk support in the control plane at
|
|
4
5
|
* all. The client already chunks — every sealed chunk is an ordinary file of
|
|
@@ -12,7 +13,7 @@ import { type ChunkStoreClient } from "./chunk-store.js";
|
|
|
12
13
|
* the object's cloud reference is `<archiveId>.<name>`:
|
|
13
14
|
*
|
|
14
15
|
* f0be…12af/chunk-000000 → vault_ref f0be…12af.chunk-000000
|
|
15
|
-
* f0be…12af/envelope.
|
|
16
|
+
* f0be…12af/envelope.skmeta → vault_ref f0be…12af.envelope.skmeta
|
|
16
17
|
*
|
|
17
18
|
* Deterministic both ways, so any machine of the vault re-derives every
|
|
18
19
|
* reference from the archive id and the chunk count — no listing, no state.
|
|
@@ -52,4 +53,6 @@ export declare function collapseCloudRows<T extends CloudRowLike>(rows: T[]): (T
|
|
|
52
53
|
* `pullCiphertext` hash-checks every read against the digest recorded at
|
|
53
54
|
* upload before handing it back.
|
|
54
55
|
*/
|
|
55
|
-
export declare function managedChunkClient(dataDir: string
|
|
56
|
+
export declare function managedChunkClient(dataDir: string, options?: {
|
|
57
|
+
deleteContext?: ManagedDeleteContext;
|
|
58
|
+
}): ChunkStoreClient;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { fail,
|
|
2
|
+
import { fail, isSealkeepError } from "./errors.js";
|
|
3
3
|
import { chunkObjectName, ENVELOPE_OBJECT } from "./chunk-store.js";
|
|
4
4
|
import { completeUpload, deleteCloudArchive, pullCiphertext, putCiphertext, requestUpload, statCiphertext } from "./cloud.js";
|
|
5
5
|
/**
|
|
@@ -15,7 +15,7 @@ import { completeUpload, deleteCloudArchive, pullCiphertext, putCiphertext, requ
|
|
|
15
15
|
* the object's cloud reference is `<archiveId>.<name>`:
|
|
16
16
|
*
|
|
17
17
|
* f0be…12af/chunk-000000 → vault_ref f0be…12af.chunk-000000
|
|
18
|
-
* f0be…12af/envelope.
|
|
18
|
+
* f0be…12af/envelope.skmeta → vault_ref f0be…12af.envelope.skmeta
|
|
19
19
|
*
|
|
20
20
|
* Deterministic both ways, so any machine of the vault re-derives every
|
|
21
21
|
* reference from the archive id and the chunk count — no listing, no state.
|
|
@@ -44,7 +44,7 @@ export function managedChunkRefs(archiveId, chunkCount) {
|
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
46
|
/** Recognises a cloud row that is one member of a chunk family, by its reference alone. */
|
|
47
|
-
const FAMILY_REF = /^(.+)\.(chunk-\d{6}|envelope\.
|
|
47
|
+
const FAMILY_REF = /^(.+)\.(chunk-\d{6}|envelope\.skmeta)$/;
|
|
48
48
|
/**
|
|
49
49
|
* Folds chunk-family rows into one synthetic row per archive for listings:
|
|
50
50
|
* the family's reference is the archive id, its bytes are the chunk sum (the
|
|
@@ -78,28 +78,28 @@ export function collapseCloudRows(rows) {
|
|
|
78
78
|
* `pullCiphertext` hash-checks every read against the digest recorded at
|
|
79
79
|
* upload before handing it back.
|
|
80
80
|
*/
|
|
81
|
-
export function managedChunkClient(dataDir) {
|
|
81
|
+
export function managedChunkClient(dataDir, options = {}) {
|
|
82
82
|
return {
|
|
83
|
-
async putObject(key, body) {
|
|
83
|
+
async putObject(key, body, writeOptions = {}) {
|
|
84
84
|
const ref = managedRefForKey(key);
|
|
85
85
|
const digest = createHash("sha256").update(body).digest("hex");
|
|
86
|
-
const lease = await requestUpload(dataDir, { vaultRef: ref, ciphertextSha256: digest, bytes: body.length });
|
|
87
|
-
await putCiphertext(lease, body);
|
|
88
|
-
await completeUpload(dataDir, lease.lease_id, body.length);
|
|
86
|
+
const lease = await requestUpload(dataDir, { vaultRef: ref, ciphertextSha256: digest, bytes: body.length }, undefined, { signal: writeOptions.signal });
|
|
87
|
+
await putCiphertext(lease, body, writeOptions.signal);
|
|
88
|
+
await completeUpload(dataDir, lease.lease_id, body.length, undefined, { signal: writeOptions.signal });
|
|
89
89
|
return { checksum: digest };
|
|
90
90
|
},
|
|
91
|
-
async headObject(key) {
|
|
92
|
-
return statCiphertext(dataDir, managedRefForKey(key));
|
|
91
|
+
async headObject(key, readOptions = {}) {
|
|
92
|
+
return statCiphertext(dataDir, managedRefForKey(key), undefined, { signal: readOptions.signal });
|
|
93
93
|
},
|
|
94
|
-
async getObject(key) {
|
|
95
|
-
return (await pullCiphertext(dataDir, managedRefForKey(key))).ciphertext;
|
|
94
|
+
async getObject(key, readOptions = {}) {
|
|
95
|
+
return (await pullCiphertext(dataDir, managedRefForKey(key), undefined, { signal: readOptions.signal })).ciphertext;
|
|
96
96
|
},
|
|
97
97
|
async deleteObject(key) {
|
|
98
98
|
try {
|
|
99
|
-
await deleteCloudArchive(dataDir, managedRefForKey(key));
|
|
99
|
+
await deleteCloudArchive(dataDir, managedRefForKey(key), undefined, { context: options.deleteContext });
|
|
100
100
|
}
|
|
101
101
|
catch (error) {
|
|
102
|
-
if (
|
|
102
|
+
if (isSealkeepError(error) && (error.code === "archive_not_found" || /returned 404|not[ _-]?found/i.test(error.message)))
|
|
103
103
|
return;
|
|
104
104
|
throw error;
|
|
105
105
|
}
|
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
2
1
|
import { type SetupAgentId } from "./adapters.js";
|
|
3
|
-
|
|
2
|
+
/** Test seam for agent-owned installers (`which`/`where` and `claude mcp add`). */
|
|
3
|
+
export type McpExec = (file: string, args: string[], options?: {
|
|
4
|
+
env?: NodeJS.ProcessEnv;
|
|
5
|
+
}) => Promise<unknown>;
|
|
4
6
|
/**
|
|
5
7
|
* Registering Sealkeep's MCP server with the agents on this machine.
|
|
6
8
|
*
|
|
7
9
|
* Archiving sessions and being able to *ask about* them are different products,
|
|
8
10
|
* and only the first one was wired. `src/mcp.ts` has always exposed
|
|
9
|
-
* `
|
|
10
|
-
*
|
|
11
|
-
*
|
|
11
|
+
* `sealkeep_search` and `sealkeep_recover` (still answering to their original
|
|
12
|
+
* `vaultline_search` / `vaultline_recover` names too, for configs that already
|
|
13
|
+
* point at them), but nothing installed it and nothing told an agent it
|
|
14
|
+
* existed — a door with no sign on it. This is the sign.
|
|
12
15
|
*
|
|
13
16
|
* What is deliberately NOT here: any claim that an agent can reach archives it
|
|
14
17
|
* does not hold locally. The MCP server searches this machine's index. A
|
|
@@ -36,17 +39,16 @@ export declare function mcpServerSpec(dataDir: string, executable?: string): {
|
|
|
36
39
|
/**
|
|
37
40
|
* Whether the command we are about to hand another program can actually be run.
|
|
38
41
|
*
|
|
39
|
-
* Registering `
|
|
42
|
+
* Registering `sealkeep` when it is not on PATH — a checkout, an npx run, a
|
|
40
43
|
* shell whose PATH the agent does not inherit — writes a server the agent
|
|
41
44
|
* reports as "Failed to connect: ENOENT" forever, while this command claims
|
|
42
45
|
* success. Reporting a registration that cannot start is the same false claim
|
|
43
46
|
* as an archive that was never verified, and it is caught the same way: prove
|
|
44
47
|
* it before saying it.
|
|
45
48
|
*/
|
|
46
|
-
export declare function executableResolves(executable?: string, exec?:
|
|
49
|
+
export declare function executableResolves(executable?: string, exec?: McpExec): Promise<boolean>;
|
|
47
50
|
export declare function installMcpServer(dataDir: string, options?: {
|
|
48
51
|
only?: string;
|
|
49
|
-
exec?:
|
|
52
|
+
exec?: McpExec;
|
|
50
53
|
home?: string;
|
|
51
54
|
}): Promise<McpInstallResult>;
|
|
52
|
-
export {};
|
package/dist/src/mcp-install.js
CHANGED
|
@@ -4,9 +4,13 @@ import { dirname, join } from "node:path";
|
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
6
|
import { detectSetupAgents } from "./adapters.js";
|
|
7
|
+
import { PRODUCT_SLUG } from "./branding.js";
|
|
7
8
|
const run = promisify(execFile);
|
|
9
|
+
const execute = (file, args, options) => options?.env
|
|
10
|
+
? run(file, args, { env: options.env })
|
|
11
|
+
: run(file, args);
|
|
8
12
|
/** What every agent needs to start the server, in one place so the copies cannot drift. */
|
|
9
|
-
export function mcpServerSpec(dataDir, executable =
|
|
13
|
+
export function mcpServerSpec(dataDir, executable = PRODUCT_SLUG) {
|
|
10
14
|
return {
|
|
11
15
|
command: executable,
|
|
12
16
|
args: ["mcp"],
|
|
@@ -16,14 +20,14 @@ export function mcpServerSpec(dataDir, executable = "vaultline") {
|
|
|
16
20
|
/**
|
|
17
21
|
* Whether the command we are about to hand another program can actually be run.
|
|
18
22
|
*
|
|
19
|
-
* Registering `
|
|
23
|
+
* Registering `sealkeep` when it is not on PATH — a checkout, an npx run, a
|
|
20
24
|
* shell whose PATH the agent does not inherit — writes a server the agent
|
|
21
25
|
* reports as "Failed to connect: ENOENT" forever, while this command claims
|
|
22
26
|
* success. Reporting a registration that cannot start is the same false claim
|
|
23
27
|
* as an archive that was never verified, and it is caught the same way: prove
|
|
24
28
|
* it before saying it.
|
|
25
29
|
*/
|
|
26
|
-
export async function executableResolves(executable =
|
|
30
|
+
export async function executableResolves(executable = PRODUCT_SLUG, exec = execute) {
|
|
27
31
|
try {
|
|
28
32
|
await exec(process.platform === "win32" ? "where" : "which", [executable]);
|
|
29
33
|
return true;
|
|
@@ -39,14 +43,14 @@ export async function executableResolves(executable = "vaultline", exec = run) {
|
|
|
39
43
|
* installer then refused as foreign — the client normalises what it stores, so
|
|
40
44
|
* a hand-written entry never matches what the client would have written.
|
|
41
45
|
*/
|
|
42
|
-
async function installForClaude(dataDir, exec =
|
|
46
|
+
async function installForClaude(dataDir, exec = execute, home) {
|
|
43
47
|
const spec = mcpServerSpec(dataDir);
|
|
44
48
|
try {
|
|
45
49
|
await exec("claude", [
|
|
46
|
-
"mcp", "add", "--transport", "stdio", "--scope", "user",
|
|
50
|
+
"mcp", "add", "--transport", "stdio", "--scope", "user", PRODUCT_SLUG,
|
|
47
51
|
"--env", `SEALKEEP_DATA_DIR=${dataDir}`,
|
|
48
52
|
"--", spec.command, ...spec.args
|
|
49
|
-
]);
|
|
53
|
+
], home ? { env: { ...process.env, HOME: home, USERPROFILE: home } } : undefined);
|
|
50
54
|
return { installed: true, detail: "registered with Claude Code — ask it to search your sessions" };
|
|
51
55
|
}
|
|
52
56
|
catch (error) {
|
|
@@ -58,38 +62,77 @@ async function installForClaude(dataDir, exec = run) {
|
|
|
58
62
|
}
|
|
59
63
|
/**
|
|
60
64
|
* Codex configures itself in TOML. This still ships no TOML *parser* — but it
|
|
61
|
-
* does not need one to APPEND safely: a `[mcp_servers.
|
|
65
|
+
* does not need one to APPEND safely: a `[mcp_servers.sealkeep]` table at the
|
|
62
66
|
* end of the file is a new top-level table, valid TOML regardless of what
|
|
63
67
|
* precedes it, as long as that section does not already exist. So the rule is:
|
|
68
|
+
* config file absent → create it exclusively with owner-only permissions;
|
|
64
69
|
* config file present and section absent → back it up, append, registered;
|
|
65
|
-
*
|
|
66
|
-
*
|
|
70
|
+
* section present → leave it alone. The backup makes an edit to an existing
|
|
71
|
+
* config as reversible as never having made it.
|
|
67
72
|
*/
|
|
68
|
-
async function
|
|
73
|
+
async function writeCodexFragment(dataDir) {
|
|
69
74
|
const spec = mcpServerSpec(dataDir);
|
|
70
75
|
const fragmentPath = join(dataDir, "integrations", "codex-mcp.toml");
|
|
71
76
|
const table = [
|
|
72
|
-
|
|
77
|
+
`[mcp_servers.${PRODUCT_SLUG}]`,
|
|
73
78
|
`command = ${JSON.stringify(spec.command)}`,
|
|
74
79
|
`args = ${JSON.stringify(spec.args)}`,
|
|
75
80
|
"",
|
|
76
|
-
|
|
81
|
+
`[mcp_servers.${PRODUCT_SLUG}.env]`,
|
|
77
82
|
`SEALKEEP_DATA_DIR = ${JSON.stringify(dataDir)}`,
|
|
78
83
|
""
|
|
79
84
|
].join("\n");
|
|
80
85
|
await mkdir(dirname(fragmentPath), { recursive: true });
|
|
81
|
-
await writeFile(fragmentPath, `#
|
|
86
|
+
await writeFile(fragmentPath, `# Sealkeep MCP registration for Codex.\n${table}`, { mode: 0o600 });
|
|
87
|
+
return { fragmentPath, table };
|
|
88
|
+
}
|
|
89
|
+
async function installForCodex(dataDir, home) {
|
|
90
|
+
const { fragmentPath, table } = await writeCodexFragment(dataDir);
|
|
82
91
|
const configPath = join(home ?? homedir(), ".codex", "config.toml");
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
92
|
+
let current;
|
|
93
|
+
try {
|
|
94
|
+
current = await readFile(configPath, "utf8");
|
|
86
95
|
}
|
|
87
|
-
|
|
96
|
+
catch (error) {
|
|
97
|
+
if (error.code !== "ENOENT") {
|
|
98
|
+
const message = error instanceof Error ? error.message.split("\n")[0] : "unknown error";
|
|
99
|
+
return { installed: false, detail: `could not read ~/.codex/config.toml: ${message}`, fragmentPath };
|
|
100
|
+
}
|
|
101
|
+
await mkdir(dirname(configPath), { recursive: true, mode: 0o700 });
|
|
102
|
+
try {
|
|
103
|
+
// `wx` is the safety boundary: if Codex (or another setup process) creates
|
|
104
|
+
// the file after our read, never replace what it wrote.
|
|
105
|
+
await writeFile(configPath, table, { mode: 0o600, flag: "wx" });
|
|
106
|
+
return { installed: true, detail: "created ~/.codex/config.toml and registered the local Sealkeep MCP", fragmentPath };
|
|
107
|
+
}
|
|
108
|
+
catch (createError) {
|
|
109
|
+
if (createError.code !== "EEXIST") {
|
|
110
|
+
const message = createError instanceof Error ? createError.message.split("\n")[0] : "unknown error";
|
|
111
|
+
return { installed: false, detail: `could not create ~/.codex/config.toml: ${message}`, fragmentPath };
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
current = await readFile(configPath, "utf8");
|
|
115
|
+
}
|
|
116
|
+
catch (raceError) {
|
|
117
|
+
const message = raceError instanceof Error ? raceError.message.split("\n")[0] : "unknown error";
|
|
118
|
+
return { installed: false, detail: `Codex created config.toml during setup, but Sealkeep could not read it: ${message}`, fragmentPath };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (current.includes(`[mcp_servers.${PRODUCT_SLUG}]`)) {
|
|
88
123
|
return { installed: true, detail: "already registered in ~/.codex/config.toml", fragmentPath };
|
|
89
124
|
}
|
|
90
|
-
|
|
125
|
+
/**
|
|
126
|
+
* A registration from before the rename names a command that no longer
|
|
127
|
+
* exists, so Codex fails to start it forever. Appending a good table beside
|
|
128
|
+
* it does not help — the dead one still errors on every launch — so the
|
|
129
|
+
* legacy block is REPLACED, not left to rot next to its replacement.
|
|
130
|
+
*/
|
|
131
|
+
const legacy = /\[mcp_servers\.vaultline(?:\.env)?\][^[]*/g;
|
|
132
|
+
const cleaned = current.replace(legacy, "").replace(/\n{3,}/g, "\n\n");
|
|
133
|
+
const backupPath = `${configPath}.${PRODUCT_SLUG}-backup-${Date.now()}`;
|
|
91
134
|
await writeFile(backupPath, current, { mode: 0o600 });
|
|
92
|
-
await writeFile(configPath, `${
|
|
135
|
+
await writeFile(configPath, `${cleaned}${cleaned.endsWith("\n") ? "" : "\n"}\n${table}`, { mode: 0o600 });
|
|
93
136
|
return { installed: true, detail: `registered in ~/.codex/config.toml (backup kept beside it)`, fragmentPath, backupPath };
|
|
94
137
|
}
|
|
95
138
|
export async function installMcpServer(dataDir, options = {}) {
|
|
@@ -104,8 +147,8 @@ export async function installMcpServer(dataDir, options = {}) {
|
|
|
104
147
|
* there. A fragment is inert until someone merges it deliberately, so
|
|
105
148
|
* withholding it would only remove information.
|
|
106
149
|
*/
|
|
107
|
-
const runnable = await executableResolves(
|
|
108
|
-
const notOnPath = "`
|
|
150
|
+
const runnable = await executableResolves(PRODUCT_SLUG, options.exec);
|
|
151
|
+
const notOnPath = "The `sealkeep` executable is not on PATH, so the MCP server could not start. Install it globally (`npm i -g sealkeep`), then reopen Setup.";
|
|
109
152
|
for (const found of detected) {
|
|
110
153
|
if (!found.detected)
|
|
111
154
|
continue;
|
|
@@ -113,18 +156,20 @@ export async function installMcpServer(dataDir, options = {}) {
|
|
|
113
156
|
continue;
|
|
114
157
|
if (found.agent === "claude") {
|
|
115
158
|
agents.push(runnable
|
|
116
|
-
? { agent: found.agent, label: found.label, ...(await installForClaude(dataDir, options.exec)) }
|
|
159
|
+
? { agent: found.agent, label: found.label, ...(await installForClaude(dataDir, options.exec, options.home)) }
|
|
117
160
|
: { agent: found.agent, label: found.label, installed: false, detail: `not registered — ${notOnPath}` });
|
|
118
161
|
}
|
|
119
162
|
else if (found.agent === "codex") {
|
|
120
163
|
// A registration that cannot start is worse than none (same rule as
|
|
121
|
-
// Claude): only a runnable `
|
|
164
|
+
// Claude): only a runnable `sealkeep` earns the config append; without
|
|
122
165
|
// it the fragment is written and the advice says why.
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
166
|
+
if (runnable) {
|
|
167
|
+
agents.push({ ...(await installForCodex(dataDir, options.home)), agent: found.agent, label: found.label });
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
const { fragmentPath } = await writeCodexFragment(dataDir);
|
|
171
|
+
agents.push({ agent: found.agent, label: found.label, fragmentPath, installed: false, detail: `not registered — ${notOnPath}` });
|
|
172
|
+
}
|
|
128
173
|
}
|
|
129
174
|
else {
|
|
130
175
|
// Detected, but Sealkeep has no adapter for its sessions, so an MCP
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { TeamSpaceBinding } from "./vault.js";
|
|
2
|
+
type Selection = {
|
|
3
|
+
project: string;
|
|
4
|
+
projectKey: string;
|
|
5
|
+
};
|
|
6
|
+
export type WorkspaceSelection = Selection | {
|
|
7
|
+
note: string;
|
|
8
|
+
candidates: Selection[];
|
|
9
|
+
};
|
|
10
|
+
/** Roots are authoritative when supported. No label or last-used-project fallback. */
|
|
11
|
+
export declare function resolveMcpWorkspace(input: {
|
|
12
|
+
roots?: readonly {
|
|
13
|
+
uri: string;
|
|
14
|
+
}[];
|
|
15
|
+
cwd: string;
|
|
16
|
+
bindings: Record<string, TeamSpaceBinding>;
|
|
17
|
+
}): Promise<WorkspaceSelection>;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { realpath, stat } from "node:fs/promises";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { execFile } from "node:child_process";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { projectFromCwd } from "./adapters.js";
|
|
7
|
+
/** Roots are authoritative when supported. No label or last-used-project fallback. */
|
|
8
|
+
export async function resolveMcpWorkspace(input) {
|
|
9
|
+
const candidates = Object.entries(input.bindings).flatMap(([projectKey, binding]) => /^(g1|p1):[a-f0-9]{64}$/.test(projectKey) && binding.localConfirmed && binding.localProject
|
|
10
|
+
? [{ project: binding.localProject, projectKey }] : []);
|
|
11
|
+
const hint = () => ({
|
|
12
|
+
note: "Workspace scope could not be resolved to one confirmed project. Supply project and projectKey from a confirmed checkout, or scope: account for an explicitly account-wide search.",
|
|
13
|
+
candidates,
|
|
14
|
+
});
|
|
15
|
+
try {
|
|
16
|
+
const paths = input.roots === undefined ? [input.cwd] : input.roots.map(({ uri }) => {
|
|
17
|
+
const url = new URL(uri);
|
|
18
|
+
if (url.protocol !== "file:" || url.search || url.hash)
|
|
19
|
+
throw new Error("invalid workspace root");
|
|
20
|
+
return fileURLToPath(url);
|
|
21
|
+
});
|
|
22
|
+
if (!paths.length || paths.length > 32)
|
|
23
|
+
return hint();
|
|
24
|
+
const canonical = [...new Set(await Promise.all(paths.map(async (path) => {
|
|
25
|
+
const resolved = await realpath(path);
|
|
26
|
+
if (!(await stat(resolved)).isDirectory())
|
|
27
|
+
throw new Error("workspace is not a folder");
|
|
28
|
+
return resolved;
|
|
29
|
+
})))];
|
|
30
|
+
// Duplicate representations of one root are harmless; multiple folders are
|
|
31
|
+
// not evidence of which one the current tool call concerns.
|
|
32
|
+
if (canonical.length !== 1)
|
|
33
|
+
return hint();
|
|
34
|
+
let current = canonical[0];
|
|
35
|
+
// Never fall through an unconfirmed nested repository into its parent repo.
|
|
36
|
+
const gitRoot = await promisify(execFile)("git", ["-C", current, "rev-parse", "--show-toplevel"], { timeout: 2000, maxBuffer: 64 * 1024 }).then(async ({ stdout }) => realpath(stdout.trim()), () => null);
|
|
37
|
+
for (let depth = 0; depth < 64; depth++) {
|
|
38
|
+
const identity = await projectFromCwd(current);
|
|
39
|
+
const match = candidates.find((candidate) => candidate.projectKey === identity.projectKey);
|
|
40
|
+
if (match)
|
|
41
|
+
return match;
|
|
42
|
+
const parent = dirname(current);
|
|
43
|
+
if (current === gitRoot || parent === current)
|
|
44
|
+
break;
|
|
45
|
+
current = parent;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch { /* unavailable/unknown roots must not widen the search */ }
|
|
49
|
+
return hint();
|
|
50
|
+
}
|