project-librarian 0.5.3 → 0.5.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/README.ko.md +16 -2
- package/README.md +16 -2
- package/SKILL.md +14 -1
- package/dist/args.js +66 -2
- package/dist/code-index/ownership.js +8 -6
- package/dist/code-index-db.js +20 -12
- package/dist/code-index-file-policy.js +8 -10
- package/dist/code-index.js +4 -3
- package/dist/hooks.js +77 -5
- package/dist/init-project-wiki.js +58 -1
- package/dist/install-skill.js +99 -6
- package/dist/mcp-server.js +4 -4
- package/dist/migration.js +27 -2
- package/dist/modes.js +12 -5
- package/dist/session-handoff.js +627 -0
- package/dist/templates.js +5 -3
- package/dist/workspace.js +137 -10
- package/package.json +4 -2
package/dist/workspace.js
CHANGED
|
@@ -51,31 +51,105 @@ exports.normalizePath = normalizePath;
|
|
|
51
51
|
exports.commandOk = commandOk;
|
|
52
52
|
exports.isGitRepository = isGitRepository;
|
|
53
53
|
exports.makeExecutable = makeExecutable;
|
|
54
|
+
exports.containedProjectFileStat = containedProjectFileStat;
|
|
55
|
+
exports.containedProjectDirectoryStat = containedProjectDirectoryStat;
|
|
56
|
+
exports.requireContainedProjectFile = requireContainedProjectFile;
|
|
54
57
|
exports.walkFilesUnder = walkFilesUnder;
|
|
55
58
|
const fs = __importStar(require("node:fs"));
|
|
56
59
|
const path = __importStar(require("node:path"));
|
|
57
60
|
const childProcess = __importStar(require("node:child_process"));
|
|
58
61
|
exports.root = process.cwd();
|
|
59
62
|
exports.today = new Date().toISOString().slice(0, 10);
|
|
63
|
+
const projectRoot = path.resolve(exports.root);
|
|
60
64
|
function abs(relativePath) {
|
|
61
65
|
return path.join(exports.root, relativePath);
|
|
62
66
|
}
|
|
67
|
+
function isInsideProject(absolutePath) {
|
|
68
|
+
const resolved = path.resolve(absolutePath);
|
|
69
|
+
return resolved === projectRoot || resolved.startsWith(`${projectRoot}${path.sep}`);
|
|
70
|
+
}
|
|
71
|
+
function resolveProjectPath(relativePath, label = "path") {
|
|
72
|
+
const resolved = path.isAbsolute(relativePath) ? path.resolve(relativePath) : path.resolve(exports.root, relativePath);
|
|
73
|
+
if (!isInsideProject(resolved)) {
|
|
74
|
+
throw new Error(`${label} must stay inside the project root: ${relativePath}`);
|
|
75
|
+
}
|
|
76
|
+
return resolved;
|
|
77
|
+
}
|
|
78
|
+
function assertNoSymlinkInProjectPath(relativePath, includeLeaf, label = "path") {
|
|
79
|
+
const target = resolveProjectPath(relativePath, label);
|
|
80
|
+
const relative = path.relative(projectRoot, target);
|
|
81
|
+
if (!relative)
|
|
82
|
+
return target;
|
|
83
|
+
const parts = relative.split(path.sep).filter(Boolean);
|
|
84
|
+
const checkedParts = includeLeaf ? parts : parts.slice(0, -1);
|
|
85
|
+
let current = projectRoot;
|
|
86
|
+
for (const part of checkedParts) {
|
|
87
|
+
current = path.join(current, part);
|
|
88
|
+
if (!fs.existsSync(current))
|
|
89
|
+
continue;
|
|
90
|
+
const stat = fs.lstatSync(current);
|
|
91
|
+
if (stat.isSymbolicLink()) {
|
|
92
|
+
throw new Error(`${label} refuses to follow symlink: ${normalizePath(path.relative(projectRoot, current))}`);
|
|
93
|
+
}
|
|
94
|
+
if (current !== target && !stat.isDirectory()) {
|
|
95
|
+
throw new Error(`${label} has a non-directory path component: ${normalizePath(path.relative(projectRoot, current))}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return target;
|
|
99
|
+
}
|
|
100
|
+
function mkdirpAbsolute(target, label = "path") {
|
|
101
|
+
if (!isInsideProject(target)) {
|
|
102
|
+
throw new Error(`${label} must stay inside the project root: ${target}`);
|
|
103
|
+
}
|
|
104
|
+
const relative = path.relative(projectRoot, target);
|
|
105
|
+
if (!relative)
|
|
106
|
+
return;
|
|
107
|
+
let current = projectRoot;
|
|
108
|
+
for (const part of relative.split(path.sep).filter(Boolean)) {
|
|
109
|
+
current = path.join(current, part);
|
|
110
|
+
if (fs.existsSync(current)) {
|
|
111
|
+
const stat = fs.lstatSync(current);
|
|
112
|
+
if (stat.isSymbolicLink()) {
|
|
113
|
+
throw new Error(`${label} refuses to follow symlink: ${normalizePath(path.relative(projectRoot, current))}`);
|
|
114
|
+
}
|
|
115
|
+
if (!stat.isDirectory()) {
|
|
116
|
+
throw new Error(`${label} has a non-directory path component: ${normalizePath(path.relative(projectRoot, current))}`);
|
|
117
|
+
}
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
fs.mkdirSync(current);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function writeFileNoFollow(filePath, content) {
|
|
124
|
+
const noFollow = typeof fs.constants.O_NOFOLLOW === "number" ? fs.constants.O_NOFOLLOW : 0;
|
|
125
|
+
const fd = fs.openSync(filePath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_TRUNC | noFollow, 0o666);
|
|
126
|
+
try {
|
|
127
|
+
fs.writeFileSync(fd, content);
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
fs.closeSync(fd);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
63
133
|
function exists(relativePath) {
|
|
64
134
|
return fs.existsSync(abs(relativePath));
|
|
65
135
|
}
|
|
66
136
|
function read(relativePath) {
|
|
67
|
-
|
|
137
|
+
const filePath = assertNoSymlinkInProjectPath(relativePath, true, "managed read");
|
|
138
|
+
return fs.readFileSync(filePath, "utf8");
|
|
68
139
|
}
|
|
69
140
|
function write(relativePath, content) {
|
|
70
|
-
const filePath =
|
|
71
|
-
|
|
72
|
-
|
|
141
|
+
const filePath = assertNoSymlinkInProjectPath(relativePath, true, "managed write");
|
|
142
|
+
mkdirpAbsolute(path.dirname(filePath), "managed write");
|
|
143
|
+
writeFileNoFollow(filePath, content);
|
|
73
144
|
}
|
|
74
145
|
function mkdirp(relativePath) {
|
|
75
|
-
|
|
146
|
+
const dirPath = assertNoSymlinkInProjectPath(relativePath, true, "managed directory");
|
|
147
|
+
mkdirpAbsolute(dirPath, "managed directory");
|
|
76
148
|
}
|
|
77
149
|
function writeManaged(relativePath, content) {
|
|
78
|
-
const previous = exists(relativePath)
|
|
150
|
+
const previous = exists(relativePath)
|
|
151
|
+
? (assertNoSymlinkInProjectPath(relativePath, true, "managed read"), read(relativePath))
|
|
152
|
+
: "";
|
|
79
153
|
if (previous === content)
|
|
80
154
|
return "exists";
|
|
81
155
|
write(relativePath, content);
|
|
@@ -86,6 +160,7 @@ function writeStarter(relativePath, content) {
|
|
|
86
160
|
write(relativePath, content);
|
|
87
161
|
return "created";
|
|
88
162
|
}
|
|
163
|
+
assertNoSymlinkInProjectPath(relativePath, true, "managed read");
|
|
89
164
|
const current = read(relativePath);
|
|
90
165
|
if (current === content)
|
|
91
166
|
return "exists";
|
|
@@ -111,6 +186,7 @@ function upsertMarkedSection(relativePath, startMarker, endMarker, section) {
|
|
|
111
186
|
write(relativePath, `${section.trim()}\n`);
|
|
112
187
|
return "created";
|
|
113
188
|
}
|
|
189
|
+
assertNoSymlinkInProjectPath(relativePath, true, "managed read");
|
|
114
190
|
const current = read(relativePath);
|
|
115
191
|
const start = current.indexOf(startMarker);
|
|
116
192
|
const end = current.indexOf(endMarker);
|
|
@@ -133,15 +209,17 @@ function upsertMarkedSection(relativePath, startMarker, endMarker, section) {
|
|
|
133
209
|
function deleteIfGenerated(relativePath, sentinels) {
|
|
134
210
|
if (!exists(relativePath))
|
|
135
211
|
return "absent";
|
|
212
|
+
const filePath = assertNoSymlinkInProjectPath(relativePath, true, "managed delete");
|
|
136
213
|
const current = read(relativePath);
|
|
137
214
|
if (!sentinels.some((sentinel) => current.includes(sentinel)))
|
|
138
215
|
return "manual-review";
|
|
139
|
-
fs.unlinkSync(
|
|
216
|
+
fs.unlinkSync(filePath);
|
|
140
217
|
return "removed";
|
|
141
218
|
}
|
|
142
219
|
function parseJson(relativePath, fallback) {
|
|
143
220
|
if (!exists(relativePath))
|
|
144
221
|
return fallback;
|
|
222
|
+
assertNoSymlinkInProjectPath(relativePath, true, "managed read");
|
|
145
223
|
try {
|
|
146
224
|
return JSON.parse(read(relativePath));
|
|
147
225
|
}
|
|
@@ -191,11 +269,60 @@ function isGitRepository() {
|
|
|
191
269
|
function makeExecutable(relativePath) {
|
|
192
270
|
if (!exists(relativePath))
|
|
193
271
|
return;
|
|
194
|
-
const
|
|
195
|
-
|
|
272
|
+
const filePath = assertNoSymlinkInProjectPath(relativePath, true, "managed chmod");
|
|
273
|
+
const currentMode = fs.statSync(filePath).mode;
|
|
274
|
+
fs.chmodSync(filePath, currentMode | 0o755);
|
|
275
|
+
}
|
|
276
|
+
function containedProjectFileStat(relativePath) {
|
|
277
|
+
const filePath = resolveProjectPath(relativePath, "project file");
|
|
278
|
+
let stat;
|
|
279
|
+
try {
|
|
280
|
+
stat = fs.lstatSync(filePath);
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
if (stat.isSymbolicLink() || !stat.isFile())
|
|
286
|
+
return null;
|
|
287
|
+
let realPath = "";
|
|
288
|
+
try {
|
|
289
|
+
realPath = fs.realpathSync(filePath);
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
return isInsideProject(realPath) ? stat : null;
|
|
295
|
+
}
|
|
296
|
+
function containedProjectDirectoryStat(relativePath) {
|
|
297
|
+
const dirPath = resolveProjectPath(relativePath, "project directory");
|
|
298
|
+
let stat;
|
|
299
|
+
try {
|
|
300
|
+
stat = fs.lstatSync(dirPath);
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
306
|
+
return null;
|
|
307
|
+
let realPath = "";
|
|
308
|
+
try {
|
|
309
|
+
realPath = fs.realpathSync(dirPath);
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
return isInsideProject(realPath) ? stat : null;
|
|
315
|
+
}
|
|
316
|
+
function requireContainedProjectFile(relativePath, label = "project file") {
|
|
317
|
+
const filePath = resolveProjectPath(relativePath, label);
|
|
318
|
+
const stat = containedProjectFileStat(relativePath);
|
|
319
|
+
if (!stat) {
|
|
320
|
+
throw new Error(`${label} must be a regular file inside the project root and must not be a symlink: ${relativePath}`);
|
|
321
|
+
}
|
|
322
|
+
return { absolutePath: filePath, stat };
|
|
196
323
|
}
|
|
197
324
|
function walkFilesUnder(relativePath, predicate, acc = []) {
|
|
198
|
-
const dirPath =
|
|
325
|
+
const dirPath = assertNoSymlinkInProjectPath(relativePath, true, "managed walk");
|
|
199
326
|
if (!fs.existsSync(dirPath))
|
|
200
327
|
return acc;
|
|
201
328
|
for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "project-librarian",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.5",
|
|
4
4
|
"description": "Create and maintain compact project context for humans and LLM coding agents.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"benchmark:guidance": "npm run build && node benchmarks/tools/guidance-probe-runner.js",
|
|
50
50
|
"benchmark:guidance:dry-run": "npm run build && node benchmarks/tools/guidance-probe-runner.js --dry-run --markdown",
|
|
51
51
|
"benchmark:injection-sentinel": "node benchmarks/tools/injection-sentinel.js",
|
|
52
|
+
"benchmark:handoff-resume:preview": "node benchmarks/tools/session-handoff-resume-preview.js",
|
|
52
53
|
"benchmark:agent-surface-smoke": "node benchmarks/tools/agent-surface-smoke.js",
|
|
53
54
|
"benchmark:claim-ledger": "node benchmarks/tools/benchmark-claim-ledger.js benchmarks/llm/samples/codex-measured-report.json benchmarks/reports/llm/payload-preview.json",
|
|
54
55
|
"benchmark:real-corpus:demo": "npm run build && node benchmarks/tools/real-corpus-offline-demo.js",
|
|
@@ -57,6 +58,7 @@
|
|
|
57
58
|
"benchmark:release": "npm run benchmark:llm -- --sanitized-pack --full-matrix --runs 3 --warmup-runs 1 --min-runs-for-claim 3 --require-clean --require-claimable --model gpt-5.5 --out benchmarks/reports/llm/current.json --markdown benchmarks/reports/llm/current.md",
|
|
58
59
|
"build": "tsc && chmod +x dist/init-project-wiki.js",
|
|
59
60
|
"check:dist": "node benchmarks/tools/release-readiness.js --only-dist-parity",
|
|
61
|
+
"audit:supply-chain": "npm audit --omit=dev",
|
|
60
62
|
"perf:code-efficiency": "node benchmarks/tools/code-performance-efficiency.js --full",
|
|
61
63
|
"release:check": "node benchmarks/tools/release-readiness.js",
|
|
62
64
|
"typecheck": "tsc --noEmit",
|
|
@@ -85,6 +87,6 @@
|
|
|
85
87
|
"@sengac/tree-sitter-typescript": "^0.25.15"
|
|
86
88
|
},
|
|
87
89
|
"devDependencies": {
|
|
88
|
-
"@types/node": "^
|
|
90
|
+
"@types/node": "^22.20.0"
|
|
89
91
|
}
|
|
90
92
|
}
|