project-librarian 0.2.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/LICENSE +21 -0
- package/README.ja.md +227 -0
- package/README.ko.md +229 -0
- package/README.md +280 -0
- package/README.zh.md +227 -0
- package/SKILL.md +335 -0
- package/agents/openai.yaml +3 -0
- package/dist/args.js +235 -0
- package/dist/code-index-db.js +34 -0
- package/dist/code-index-file-policy.js +120 -0
- package/dist/code-index-sql.js +9 -0
- package/dist/code-index.js +1856 -0
- package/dist/hooks.js +321 -0
- package/dist/init-project-wiki.js +246 -0
- package/dist/install-skill.js +142 -0
- package/dist/migration.js +356 -0
- package/dist/modes.js +823 -0
- package/dist/templates.js +552 -0
- package/dist/types.js +2 -0
- package/dist/wiki-files.js +289 -0
- package/dist/workspace.js +212 -0
- package/package.json +71 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.ignoredDirs = exports.standardWikiFiles = void 0;
|
|
37
|
+
exports.walkMarkdownFiles = walkMarkdownFiles;
|
|
38
|
+
exports.firstHeading = firstHeading;
|
|
39
|
+
exports.compactSummary = compactSummary;
|
|
40
|
+
exports.splitMarkdownRow = splitMarkdownRow;
|
|
41
|
+
exports.parseMarkdownTableRows = parseMarkdownTableRows;
|
|
42
|
+
exports.wikiMarkdownFiles = wikiMarkdownFiles;
|
|
43
|
+
exports.wikiLinkForFile = wikiLinkForFile;
|
|
44
|
+
exports.normalizeWikiLinkTarget = normalizeWikiLinkTarget;
|
|
45
|
+
exports.extractWikiLinks = extractWikiLinks;
|
|
46
|
+
exports.wikiTitleForFile = wikiTitleForFile;
|
|
47
|
+
exports.metadataSummary = metadataSummary;
|
|
48
|
+
exports.stripMarkedSection = stripMarkedSection;
|
|
49
|
+
exports.extractMarkedSection = extractMarkedSection;
|
|
50
|
+
exports.withPreservedMarkedSections = withPreservedMarkedSections;
|
|
51
|
+
exports.hasGlossaryNeedSignal = hasGlossaryNeedSignal;
|
|
52
|
+
exports.hasGlossaryTable = hasGlossaryTable;
|
|
53
|
+
exports.canonicalBodyForLint = canonicalBodyForLint;
|
|
54
|
+
const fs = __importStar(require("node:fs"));
|
|
55
|
+
const path = __importStar(require("node:path"));
|
|
56
|
+
const workspace_1 = require("./workspace");
|
|
57
|
+
exports.standardWikiFiles = new Set([
|
|
58
|
+
"AGENTS.md",
|
|
59
|
+
"wiki/AGENTS.md",
|
|
60
|
+
".githooks/prepare-commit-msg",
|
|
61
|
+
".githooks/wiki-commit-trailers.js",
|
|
62
|
+
".codex/hooks.json",
|
|
63
|
+
".codex/hooks/wiki-session-start.js",
|
|
64
|
+
".claude/settings.json",
|
|
65
|
+
".claude/hooks/wiki-session-start.js",
|
|
66
|
+
"wiki/README.md",
|
|
67
|
+
"wiki/startup.md",
|
|
68
|
+
"wiki/index.md",
|
|
69
|
+
"wiki/inbox/project-candidates.md",
|
|
70
|
+
"wiki/migration/inventory.md",
|
|
71
|
+
"wiki/migration/plan.md",
|
|
72
|
+
"wiki/migration/review.md",
|
|
73
|
+
"wiki/migration/verification.md",
|
|
74
|
+
"wiki/canonical/project-brief.md",
|
|
75
|
+
"wiki/canonical/glossary.md",
|
|
76
|
+
"wiki/canonical/open-questions.md",
|
|
77
|
+
"wiki/canonical/assumptions.md",
|
|
78
|
+
"wiki/canonical/risks.md",
|
|
79
|
+
"wiki/canonical/migration-inbox.md",
|
|
80
|
+
"wiki/decisions/README.md",
|
|
81
|
+
"wiki/decisions/log.md",
|
|
82
|
+
"wiki/decisions/recent.md",
|
|
83
|
+
"wiki/decisions/decision-pack-template.md",
|
|
84
|
+
"wiki/decisions/full-adr-template.md",
|
|
85
|
+
"wiki/decisions/migration-inbox.md",
|
|
86
|
+
"wiki/meta/operating-model.md",
|
|
87
|
+
"wiki/meta/decision-policy.md",
|
|
88
|
+
"wiki/meta/wiki-ops-v1-decisions.md",
|
|
89
|
+
"wiki/sources/karpathy-llm-wiki.md",
|
|
90
|
+
"wiki/sources/migration-inbox.md",
|
|
91
|
+
"tools/project-librarian/SKILL.md",
|
|
92
|
+
"tools/project-librarian/agents/openai.yaml",
|
|
93
|
+
"tools/project-librarian/dist/init-project-wiki.js",
|
|
94
|
+
]);
|
|
95
|
+
exports.ignoredDirs = new Set([".git", ".codex", ".claude", "node_modules", ".next", "dist", "build", "coverage", "vendor", "tmp", "temp"]);
|
|
96
|
+
function walkMarkdownFiles(dir = workspace_1.root, acc = [], baseDir = workspace_1.root) {
|
|
97
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
98
|
+
const fullPath = path.join(dir, entry.name);
|
|
99
|
+
const relativePath = (0, workspace_1.normalizePath)(path.relative(workspace_1.root, fullPath));
|
|
100
|
+
const basePath = (0, workspace_1.normalizePath)(path.relative(baseDir, fullPath));
|
|
101
|
+
if (!relativePath || relativePath.startsWith(".."))
|
|
102
|
+
continue;
|
|
103
|
+
if (entry.isDirectory()) {
|
|
104
|
+
if (exports.ignoredDirs.has(entry.name))
|
|
105
|
+
continue;
|
|
106
|
+
if (relativePath === "tools/project-librarian")
|
|
107
|
+
continue;
|
|
108
|
+
if (relativePath.startsWith("wiki/migration"))
|
|
109
|
+
continue;
|
|
110
|
+
walkMarkdownFiles(fullPath, acc, baseDir);
|
|
111
|
+
}
|
|
112
|
+
else if (entry.isFile() && /\.(md|mdx)$/i.test(entry.name) && !exports.standardWikiFiles.has(relativePath)) {
|
|
113
|
+
acc.push({ path: relativePath, basePath });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return acc.sort((a, b) => a.path.localeCompare(b.path));
|
|
117
|
+
}
|
|
118
|
+
function firstHeading(text, fallback) {
|
|
119
|
+
const heading = text.match(/^#{1,3}\s+(.+)$/m);
|
|
120
|
+
if (heading?.[1])
|
|
121
|
+
return heading[1].trim().replace(/\s+/g, " ");
|
|
122
|
+
return fallback.replace(/\.(md|mdx)$/i, "").split("/").pop() ?? fallback;
|
|
123
|
+
}
|
|
124
|
+
function compactSummary(text) {
|
|
125
|
+
return text
|
|
126
|
+
.replace(/```[\s\S]*?```/g, " ")
|
|
127
|
+
.replace(/[#*_`>\-[\]()]/g, " ")
|
|
128
|
+
.replace(/\s+/g, " ")
|
|
129
|
+
.trim()
|
|
130
|
+
.slice(0, 180);
|
|
131
|
+
}
|
|
132
|
+
function splitMarkdownRow(line) {
|
|
133
|
+
const trimmed = line.trim();
|
|
134
|
+
const row = trimmed.replace(/^\|/, "").replace(/\|$/, "");
|
|
135
|
+
const cells = [];
|
|
136
|
+
let current = "";
|
|
137
|
+
let escaped = false;
|
|
138
|
+
for (const char of row) {
|
|
139
|
+
if (escaped) {
|
|
140
|
+
current += char === "|" ? "|" : `\\${char}`;
|
|
141
|
+
escaped = false;
|
|
142
|
+
}
|
|
143
|
+
else if (char === "\\") {
|
|
144
|
+
escaped = true;
|
|
145
|
+
}
|
|
146
|
+
else if (char === "|") {
|
|
147
|
+
cells.push(current.trim());
|
|
148
|
+
current = "";
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
current += char;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (escaped)
|
|
155
|
+
current += "\\";
|
|
156
|
+
cells.push(current.trim());
|
|
157
|
+
return cells;
|
|
158
|
+
}
|
|
159
|
+
function parseMarkdownTableRows(text, expectedColumns) {
|
|
160
|
+
return text
|
|
161
|
+
.split(/\r?\n/)
|
|
162
|
+
.filter((line) => /^\|.+\|$/.test(line.trim()))
|
|
163
|
+
.map(splitMarkdownRow)
|
|
164
|
+
.filter((cells) => cells.length >= expectedColumns)
|
|
165
|
+
.filter((cells) => !cells.every((cell) => /^-+$/.test(cell.replace(/\s/g, ""))))
|
|
166
|
+
.filter((cells) => !/^(source|legacy source|document)$/i.test(cells[0] ?? ""))
|
|
167
|
+
.filter((cells) => cells[0] !== "none");
|
|
168
|
+
}
|
|
169
|
+
function wikiMarkdownFiles() {
|
|
170
|
+
return (0, workspace_1.walkFilesUnder)("wiki", (file) => /\.(md|mdx)$/i.test(file) && file !== "wiki/AGENTS.md").sort();
|
|
171
|
+
}
|
|
172
|
+
function wikiLinkForFile(relativePath) {
|
|
173
|
+
return `[[${relativePath.replace(/^wiki\//, "").replace(/\.(md|mdx)$/i, "")}]]`;
|
|
174
|
+
}
|
|
175
|
+
function stripIgnoredMarkdownBlocks(text) {
|
|
176
|
+
return text
|
|
177
|
+
.replace(/```[\s\S]*?```/g, "")
|
|
178
|
+
.replace(/`[^`\n]*`/g, "");
|
|
179
|
+
}
|
|
180
|
+
function normalizeWikiLinkTarget(sourceFile, rawTarget, relativeToSource = false) {
|
|
181
|
+
let target = rawTarget
|
|
182
|
+
.trim()
|
|
183
|
+
.split("|", 1)[0] ?? "";
|
|
184
|
+
target = target.split("#", 1)[0]?.split("?", 1)[0]?.trim() ?? "";
|
|
185
|
+
if (!target || /^(https?:|mailto:|tel:)/i.test(target))
|
|
186
|
+
return "";
|
|
187
|
+
if (target.startsWith("/wiki/")) {
|
|
188
|
+
target = target.replace(/^\//, "");
|
|
189
|
+
}
|
|
190
|
+
else if (target.startsWith("/")) {
|
|
191
|
+
return "";
|
|
192
|
+
}
|
|
193
|
+
if (target.startsWith("./") || target.startsWith("../") || (relativeToSource && !target.startsWith("wiki/"))) {
|
|
194
|
+
const sourceDir = path.dirname(sourceFile);
|
|
195
|
+
target = (0, workspace_1.normalizePath)(path.normalize(path.join(sourceDir, target)));
|
|
196
|
+
}
|
|
197
|
+
else if (!target.startsWith("wiki/")) {
|
|
198
|
+
target = `wiki/${target}`;
|
|
199
|
+
}
|
|
200
|
+
if (!/\.(md|mdx)$/i.test(target))
|
|
201
|
+
target = `${target}.md`;
|
|
202
|
+
return (0, workspace_1.normalizePath)(target);
|
|
203
|
+
}
|
|
204
|
+
function markdownLinkTarget(rawTarget) {
|
|
205
|
+
const trimmed = rawTarget.trim();
|
|
206
|
+
if (trimmed.startsWith("<")) {
|
|
207
|
+
const end = trimmed.indexOf(">");
|
|
208
|
+
return end > 0 ? trimmed.slice(1, end).trim() : "";
|
|
209
|
+
}
|
|
210
|
+
return trimmed.split(/\s+/, 1)[0] ?? "";
|
|
211
|
+
}
|
|
212
|
+
function isMarkdownDocumentTarget(rawTarget) {
|
|
213
|
+
const target = rawTarget.split("#", 1)[0]?.split("?", 1)[0]?.trim() ?? "";
|
|
214
|
+
if (!target)
|
|
215
|
+
return false;
|
|
216
|
+
const ext = path.extname(target).toLowerCase();
|
|
217
|
+
return !ext || ext === ".md" || ext === ".mdx";
|
|
218
|
+
}
|
|
219
|
+
function extractWikiLinks(file, text) {
|
|
220
|
+
const body = stripIgnoredMarkdownBlocks((0, workspace_1.stripMetadataHeader)(text));
|
|
221
|
+
const links = [];
|
|
222
|
+
for (const match of body.matchAll(/\[\[([^\]\n]+)\]\]/g)) {
|
|
223
|
+
const target = match[1]?.trim() ?? "";
|
|
224
|
+
const normalizedTarget = normalizeWikiLinkTarget(file, target);
|
|
225
|
+
if (normalizedTarget)
|
|
226
|
+
links.push({ file, target, normalizedTarget, kind: "wikilink" });
|
|
227
|
+
}
|
|
228
|
+
for (const match of body.matchAll(/\[[^\]\n]*\]\(([^)\n]+)\)/g)) {
|
|
229
|
+
if (match.index && body[match.index - 1] === "!")
|
|
230
|
+
continue;
|
|
231
|
+
const target = markdownLinkTarget(match[1] ?? "");
|
|
232
|
+
if (!target || /^(https?:|mailto:|tel:|#)/i.test(target))
|
|
233
|
+
continue;
|
|
234
|
+
if (!isMarkdownDocumentTarget(target))
|
|
235
|
+
continue;
|
|
236
|
+
const normalizedTarget = normalizeWikiLinkTarget(file, target, true);
|
|
237
|
+
if (normalizedTarget.startsWith("wiki/"))
|
|
238
|
+
links.push({ file, target, normalizedTarget, kind: "markdown" });
|
|
239
|
+
}
|
|
240
|
+
return links;
|
|
241
|
+
}
|
|
242
|
+
function wikiTitleForFile(relativePath, text) {
|
|
243
|
+
return firstHeading((0, workspace_1.stripMetadataHeader)(text), relativePath);
|
|
244
|
+
}
|
|
245
|
+
function metadataSummary(relativePath, text) {
|
|
246
|
+
return {
|
|
247
|
+
status: (0, workspace_1.metadataValue)(text, "status") || "-",
|
|
248
|
+
scope: (0, workspace_1.metadataValue)(text, "scope") || "-",
|
|
249
|
+
budget: (0, workspace_1.metadataValue)(text, "read_budget") || "-",
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function stripMarkedSection(text, startMarker, endMarker) {
|
|
253
|
+
const start = text.indexOf(startMarker);
|
|
254
|
+
const end = text.indexOf(endMarker);
|
|
255
|
+
if (start < 0 || end <= start)
|
|
256
|
+
return text;
|
|
257
|
+
return `${text.slice(0, start).trimEnd()}\n\n${text.slice(end + endMarker.length).trimStart()}`.trim() + "\n";
|
|
258
|
+
}
|
|
259
|
+
function extractMarkedSection(text, startMarker, endMarker) {
|
|
260
|
+
const start = text.indexOf(startMarker);
|
|
261
|
+
const end = text.indexOf(endMarker);
|
|
262
|
+
if (start < 0 || end <= start)
|
|
263
|
+
return "";
|
|
264
|
+
return text.slice(start, end + endMarker.length).trim();
|
|
265
|
+
}
|
|
266
|
+
function withPreservedMarkedSections(relativePath, base, markerPairs) {
|
|
267
|
+
if (!(0, workspace_1.exists)(relativePath))
|
|
268
|
+
return base;
|
|
269
|
+
const current = (0, workspace_1.read)(relativePath);
|
|
270
|
+
const preserved = markerPairs
|
|
271
|
+
.map(([startMarker, endMarker]) => extractMarkedSection(current, startMarker, endMarker))
|
|
272
|
+
.filter(Boolean)
|
|
273
|
+
.filter((section) => !base.includes(section));
|
|
274
|
+
if (preserved.length === 0)
|
|
275
|
+
return base;
|
|
276
|
+
return `${base.trimEnd()}\n\n${preserved.join("\n\n")}\n`;
|
|
277
|
+
}
|
|
278
|
+
function hasGlossaryNeedSignal(text) {
|
|
279
|
+
return /(^|\n)##\s+(Glossary|Terms|Roles|Entities|Data Model|State Model|Permissions|Events|용어|역할|엔티티|상태 모델|권한|이벤트)(\s|$)|`[^`]+`\s*(term|role|state|permission|event|entity|API|DB|UI|용어|역할|상태|권한|이벤트|엔티티)/i.test(text);
|
|
280
|
+
}
|
|
281
|
+
function hasGlossaryTable(text) {
|
|
282
|
+
const body = (0, workspace_1.stripMetadataHeader)(text);
|
|
283
|
+
return /\|\s*Term\s*\|\s*Definition\s*\|\s*Avoid\s*\|\s*Related Canonical Doc\s*\|\s*Status\s*\|/.test(body);
|
|
284
|
+
}
|
|
285
|
+
function canonicalBodyForLint() {
|
|
286
|
+
return (0, workspace_1.walkFilesUnder)("wiki/canonical", (file) => /\.(md|mdx)$/i.test(file) && file !== "wiki/canonical/glossary.md")
|
|
287
|
+
.map((file) => (0, workspace_1.stripMetadataHeader)((0, workspace_1.read)(file)))
|
|
288
|
+
.join("\n");
|
|
289
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.today = exports.root = void 0;
|
|
37
|
+
exports.abs = abs;
|
|
38
|
+
exports.exists = exists;
|
|
39
|
+
exports.read = read;
|
|
40
|
+
exports.write = write;
|
|
41
|
+
exports.mkdirp = mkdirp;
|
|
42
|
+
exports.writeManaged = writeManaged;
|
|
43
|
+
exports.writeStarter = writeStarter;
|
|
44
|
+
exports.upsertMarkedSection = upsertMarkedSection;
|
|
45
|
+
exports.deleteIfGenerated = deleteIfGenerated;
|
|
46
|
+
exports.parseJson = parseJson;
|
|
47
|
+
exports.hasMetadataHeader = hasMetadataHeader;
|
|
48
|
+
exports.metadataValue = metadataValue;
|
|
49
|
+
exports.stripMetadataHeader = stripMetadataHeader;
|
|
50
|
+
exports.normalizePath = normalizePath;
|
|
51
|
+
exports.commandOk = commandOk;
|
|
52
|
+
exports.isGitRepository = isGitRepository;
|
|
53
|
+
exports.makeExecutable = makeExecutable;
|
|
54
|
+
exports.walkFilesUnder = walkFilesUnder;
|
|
55
|
+
const fs = __importStar(require("node:fs"));
|
|
56
|
+
const path = __importStar(require("node:path"));
|
|
57
|
+
const childProcess = __importStar(require("node:child_process"));
|
|
58
|
+
exports.root = process.cwd();
|
|
59
|
+
exports.today = new Date().toISOString().slice(0, 10);
|
|
60
|
+
function abs(relativePath) {
|
|
61
|
+
return path.join(exports.root, relativePath);
|
|
62
|
+
}
|
|
63
|
+
function exists(relativePath) {
|
|
64
|
+
return fs.existsSync(abs(relativePath));
|
|
65
|
+
}
|
|
66
|
+
function read(relativePath) {
|
|
67
|
+
return fs.readFileSync(abs(relativePath), "utf8");
|
|
68
|
+
}
|
|
69
|
+
function write(relativePath, content) {
|
|
70
|
+
const filePath = abs(relativePath);
|
|
71
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
72
|
+
fs.writeFileSync(filePath, content);
|
|
73
|
+
}
|
|
74
|
+
function mkdirp(relativePath) {
|
|
75
|
+
fs.mkdirSync(abs(relativePath), { recursive: true });
|
|
76
|
+
}
|
|
77
|
+
function writeManaged(relativePath, content) {
|
|
78
|
+
const previous = exists(relativePath) ? read(relativePath) : "";
|
|
79
|
+
if (previous === content)
|
|
80
|
+
return "exists";
|
|
81
|
+
write(relativePath, content);
|
|
82
|
+
return previous ? "updated" : "created";
|
|
83
|
+
}
|
|
84
|
+
function writeStarter(relativePath, content) {
|
|
85
|
+
if (!exists(relativePath)) {
|
|
86
|
+
write(relativePath, content);
|
|
87
|
+
return "created";
|
|
88
|
+
}
|
|
89
|
+
const current = read(relativePath);
|
|
90
|
+
if (current === content)
|
|
91
|
+
return "exists";
|
|
92
|
+
if (hasMetadataHeader(current))
|
|
93
|
+
return "exists";
|
|
94
|
+
const generatedSignals = [
|
|
95
|
+
"This file is the current project-planning truth",
|
|
96
|
+
"This wiki keeps project planning knowledge",
|
|
97
|
+
"This page tracks unresolved project questions",
|
|
98
|
+
"# <Topic> v<N> Decisions",
|
|
99
|
+
"# ADR: <Title>",
|
|
100
|
+
"# Karpathy LLM Wiki",
|
|
101
|
+
"# Glossary",
|
|
102
|
+
"아직 제품/서비스 주제는 정해지지 않았다",
|
|
103
|
+
];
|
|
104
|
+
if (!generatedSignals.some((signal) => current.includes(signal)))
|
|
105
|
+
return "manual-review";
|
|
106
|
+
write(relativePath, content);
|
|
107
|
+
return "updated";
|
|
108
|
+
}
|
|
109
|
+
function upsertMarkedSection(relativePath, startMarker, endMarker, section) {
|
|
110
|
+
if (!exists(relativePath)) {
|
|
111
|
+
write(relativePath, `${section.trim()}\n`);
|
|
112
|
+
return "created";
|
|
113
|
+
}
|
|
114
|
+
const current = read(relativePath);
|
|
115
|
+
const start = current.indexOf(startMarker);
|
|
116
|
+
const end = current.indexOf(endMarker);
|
|
117
|
+
if ((start >= 0) !== (end >= 0)) {
|
|
118
|
+
throw new Error(`${relativePath} has a malformed managed section: expected both ${startMarker} and ${endMarker}`);
|
|
119
|
+
}
|
|
120
|
+
if (start >= 0 && end > start) {
|
|
121
|
+
const next = `${current.slice(0, start).trimEnd()}\n\n${section.trim()}\n\n${current.slice(end + endMarker.length).trimStart()}`.trim() + "\n";
|
|
122
|
+
if (next === current)
|
|
123
|
+
return "exists";
|
|
124
|
+
write(relativePath, next);
|
|
125
|
+
return "updated";
|
|
126
|
+
}
|
|
127
|
+
if (start >= 0) {
|
|
128
|
+
throw new Error(`${relativePath} has a malformed managed section: ${endMarker} appears before ${startMarker}`);
|
|
129
|
+
}
|
|
130
|
+
write(relativePath, `${current.trimEnd()}\n\n${section.trim()}\n`);
|
|
131
|
+
return "updated";
|
|
132
|
+
}
|
|
133
|
+
function deleteIfGenerated(relativePath, sentinels) {
|
|
134
|
+
if (!exists(relativePath))
|
|
135
|
+
return "absent";
|
|
136
|
+
const current = read(relativePath);
|
|
137
|
+
if (!sentinels.some((sentinel) => current.includes(sentinel)))
|
|
138
|
+
return "manual-review";
|
|
139
|
+
fs.unlinkSync(abs(relativePath));
|
|
140
|
+
return "removed";
|
|
141
|
+
}
|
|
142
|
+
function parseJson(relativePath, fallback) {
|
|
143
|
+
if (!exists(relativePath))
|
|
144
|
+
return fallback;
|
|
145
|
+
try {
|
|
146
|
+
return JSON.parse(read(relativePath));
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
150
|
+
throw new Error(`${relativePath} is not valid JSON: ${message}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function hasMetadataHeader(text) {
|
|
154
|
+
return /^---\n[\s\S]*?\n---\n/.test(text);
|
|
155
|
+
}
|
|
156
|
+
function metadataValue(text, key) {
|
|
157
|
+
const header = text.match(/^---\n([\s\S]*?)\n---\n/);
|
|
158
|
+
if (!header)
|
|
159
|
+
return "";
|
|
160
|
+
const headerBody = header[1] ?? "";
|
|
161
|
+
const match = headerBody.match(new RegExp(`^${key}:\\s*(.+)$`, "m"));
|
|
162
|
+
return match?.[1]?.trim() ?? "";
|
|
163
|
+
}
|
|
164
|
+
function stripMetadataHeader(text) {
|
|
165
|
+
return text.replace(/^---\n[\s\S]*?\n---\n/, "");
|
|
166
|
+
}
|
|
167
|
+
function normalizePath(filePath) {
|
|
168
|
+
return filePath.split(path.sep).join("/");
|
|
169
|
+
}
|
|
170
|
+
function commandOk(command, commandArgs, options = {}) {
|
|
171
|
+
try {
|
|
172
|
+
childProcess.execFileSync(command, commandArgs, { stdio: "ignore", ...options });
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function isGitRepository() {
|
|
180
|
+
try {
|
|
181
|
+
return childProcess.execFileSync("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
182
|
+
cwd: exports.root,
|
|
183
|
+
encoding: "utf8",
|
|
184
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
185
|
+
}).trim() === "true";
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function makeExecutable(relativePath) {
|
|
192
|
+
if (!exists(relativePath))
|
|
193
|
+
return;
|
|
194
|
+
const currentMode = fs.statSync(abs(relativePath)).mode;
|
|
195
|
+
fs.chmodSync(abs(relativePath), currentMode | 0o755);
|
|
196
|
+
}
|
|
197
|
+
function walkFilesUnder(relativePath, predicate, acc = []) {
|
|
198
|
+
const dirPath = abs(relativePath);
|
|
199
|
+
if (!fs.existsSync(dirPath))
|
|
200
|
+
return acc;
|
|
201
|
+
for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) {
|
|
202
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
203
|
+
const childRelative = normalizePath(path.relative(exports.root, fullPath));
|
|
204
|
+
if (entry.isDirectory()) {
|
|
205
|
+
walkFilesUnder(childRelative, predicate, acc);
|
|
206
|
+
}
|
|
207
|
+
else if (entry.isFile() && predicate(childRelative)) {
|
|
208
|
+
acc.push(childRelative);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return acc.sort();
|
|
212
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "project-librarian",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Create and maintain compact project context for humans and LLM coding agents.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "commonjs",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"agents",
|
|
9
|
+
"claude-code",
|
|
10
|
+
"codex",
|
|
11
|
+
"llm",
|
|
12
|
+
"project-planning",
|
|
13
|
+
"wiki"
|
|
14
|
+
],
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/kkjk1176/project-librarian.git"
|
|
18
|
+
},
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/kkjk1176/project-librarian/issues"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://github.com/kkjk1176/project-librarian#readme",
|
|
23
|
+
"files": [
|
|
24
|
+
"agents/",
|
|
25
|
+
"dist/",
|
|
26
|
+
"LICENSE",
|
|
27
|
+
"README.md",
|
|
28
|
+
"README.ko.md",
|
|
29
|
+
"README.ja.md",
|
|
30
|
+
"README.zh.md",
|
|
31
|
+
"SKILL.md"
|
|
32
|
+
],
|
|
33
|
+
"bin": {
|
|
34
|
+
"project-librarian": "dist/init-project-wiki.js"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=22.13"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"benchmark": "node benchmarks/project-metrics.js",
|
|
41
|
+
"benchmark:baseline": "npm run build && node benchmarks/project-metrics.js --sample-repo benchmarks/samples/web-service --sample-repo benchmarks/samples/python-cli --sample-repo benchmarks/samples/mixed-monorepo --require-clean --save-baseline --markdown",
|
|
42
|
+
"benchmark:ci": "npm run benchmark:ci-smoke",
|
|
43
|
+
"benchmark:ci-smoke": "npm run build && node benchmarks/project-metrics.js --quick --runs 2 --warmup-runs 1 --sample-repo benchmarks/samples/web-service --sample-repo benchmarks/samples/python-cli --sample-repo benchmarks/samples/mixed-monorepo --out benchmarks/reports/ci-current.json --markdown benchmarks/reports/ci-current.md",
|
|
44
|
+
"benchmark:release": "npm run build && node benchmarks/project-metrics.js --sample-repo benchmarks/samples/web-service --sample-repo benchmarks/samples/python-cli --sample-repo benchmarks/samples/mixed-monorepo --fail-on-regression --require-clean --markdown",
|
|
45
|
+
"benchmark:trend": "node benchmarks/project-metrics.js --trend-markdown",
|
|
46
|
+
"build": "tsc && chmod +x dist/init-project-wiki.js",
|
|
47
|
+
"typecheck": "tsc --noEmit",
|
|
48
|
+
"unit": "node --test tests/unit/*.test.js",
|
|
49
|
+
"test": "npm run build && npm run typecheck && npm run unit && bash tests/smoke.sh",
|
|
50
|
+
"prepack": "npm run build"
|
|
51
|
+
},
|
|
52
|
+
"optionalDependencies": {
|
|
53
|
+
"@sengac/tree-sitter": "^0.25.15",
|
|
54
|
+
"@sengac/tree-sitter-c": "^0.25.15",
|
|
55
|
+
"@sengac/tree-sitter-c-sharp": "^0.25.15",
|
|
56
|
+
"@sengac/tree-sitter-cpp": "^0.25.15",
|
|
57
|
+
"@sengac/tree-sitter-go": "^0.25.15",
|
|
58
|
+
"@sengac/tree-sitter-java": "^0.25.15",
|
|
59
|
+
"@sengac/tree-sitter-javascript": "^0.25.15",
|
|
60
|
+
"@sengac/tree-sitter-kotlin": "^0.4.6",
|
|
61
|
+
"@sengac/tree-sitter-php": "^0.25.15",
|
|
62
|
+
"@sengac/tree-sitter-python": "^0.25.15",
|
|
63
|
+
"@sengac/tree-sitter-rust": "^0.25.15",
|
|
64
|
+
"@sengac/tree-sitter-swift": "^0.25.15",
|
|
65
|
+
"@sengac/tree-sitter-typescript": "^0.25.15"
|
|
66
|
+
},
|
|
67
|
+
"devDependencies": {
|
|
68
|
+
"@types/node": "^25.9.2",
|
|
69
|
+
"typescript": "^6.0.3"
|
|
70
|
+
}
|
|
71
|
+
}
|