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/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
- return fs.readFileSync(abs(relativePath), "utf8");
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 = abs(relativePath);
71
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
72
- fs.writeFileSync(filePath, content);
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
- fs.mkdirSync(abs(relativePath), { recursive: true });
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) ? read(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(abs(relativePath));
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 currentMode = fs.statSync(abs(relativePath)).mode;
195
- fs.chmodSync(abs(relativePath), currentMode | 0o755);
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 = abs(relativePath);
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",
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": "^25.9.2"
90
+ "@types/node": "^22.20.0"
89
91
  }
90
92
  }