@litfamily/litclaude 1.0.6 → 1.0.7

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.
Files changed (45) hide show
  1. package/CHANGELOG.md +5 -5
  2. package/README.md +11 -38
  3. package/README_ko-KR.md +11 -37
  4. package/docs/assets/cover-motion.webp +0 -0
  5. package/docs/assets/readme/badge-version.svg +1 -1
  6. package/docs/privacy.md +12 -17
  7. package/package.json +3 -1
  8. package/plugins/litclaude/.claude-plugin/plugin.json +1 -1
  9. package/plugins/litclaude/bin/litclaude-hook.js +20 -57
  10. package/plugins/litclaude/bin/litclaude-mcp.js +1 -1
  11. package/plugins/litclaude/hooks/hooks.json +4 -4
  12. package/plugins/litclaude/lib/canonical-skill-catalog.mjs +0 -1
  13. package/plugins/litclaude/lib/canonical-skill-resources.mjs +5 -27
  14. package/plugins/litclaude/lib/lit-plan-persistence.mjs +2 -1
  15. package/plugins/litclaude/lib/litgoal/paths.mjs +10 -8
  16. package/plugins/litclaude/lib/project-state-root.mjs +22 -0
  17. package/plugins/litclaude/lib/rules/session-state.mjs +2 -1
  18. package/plugins/litclaude/lib/start-work-cli.mjs +3 -3
  19. package/plugins/litclaude/lib/wikify-knowledge-cli.mjs +3 -1
  20. package/plugins/litclaude/lib/wikify-knowledge.mjs +6 -5
  21. package/plugins/litclaude/scripts/scaffold-plan.mjs +9 -3
  22. package/plugins/litclaude/skills/lit-team/scripts/team.mjs +4 -3
  23. package/scripts/prepublish-test-gate.mjs +26 -0
  24. package/plugins/litclaude/lib/skill-loop/apply.mjs +0 -391
  25. package/plugins/litclaude/lib/skill-loop/authority.mjs +0 -129
  26. package/plugins/litclaude/lib/skill-loop/cli.mjs +0 -67
  27. package/plugins/litclaude/lib/skill-loop/common.mjs +0 -280
  28. package/plugins/litclaude/lib/skill-loop/config.mjs +0 -91
  29. package/plugins/litclaude/lib/skill-loop/curator.mjs +0 -670
  30. package/plugins/litclaude/lib/skill-loop/hook-runtime.mjs +0 -188
  31. package/plugins/litclaude/lib/skill-loop/hook-state.mjs +0 -484
  32. package/plugins/litclaude/lib/skill-loop/ledger.mjs +0 -834
  33. package/plugins/litclaude/lib/skill-loop/mutation-transaction.mjs +0 -529
  34. package/plugins/litclaude/lib/skill-loop/proposal-reader.mjs +0 -26
  35. package/plugins/litclaude/lib/skill-loop/proposal-record.mjs +0 -104
  36. package/plugins/litclaude/lib/skill-loop/proposal-store.mjs +0 -187
  37. package/plugins/litclaude/lib/skill-loop/proposals.mjs +0 -26
  38. package/plugins/litclaude/lib/skill-loop/review-runner.mjs +0 -13
  39. package/plugins/litclaude/lib/skill-loop/review-transaction.mjs +0 -358
  40. package/plugins/litclaude/lib/skill-loop/review.mjs +0 -223
  41. package/plugins/litclaude/lib/skill-loop/usage.mjs +0 -218
  42. package/plugins/litclaude/lib/skill-observer.mjs +0 -693
  43. package/plugins/litclaude/skills/skill-observer/SKILL.md +0 -155
  44. package/plugins/litclaude/skills/skill-observer/references/review-contract.md +0 -77
  45. package/plugins/litclaude/skills/skill-observer/scripts/skill-loop.mjs +0 -10
@@ -1,280 +0,0 @@
1
- import { createHash, randomUUID } from "node:crypto";
2
- import {
3
- closeSync,
4
- constants,
5
- existsSync,
6
- fstatSync,
7
- fsyncSync,
8
- lstatSync,
9
- mkdirSync,
10
- openSync,
11
- readFileSync,
12
- realpathSync,
13
- renameSync,
14
- rmSync,
15
- writeFileSync,
16
- } from "node:fs";
17
- import { dirname, isAbsolute, join, relative, resolve } from "node:path";
18
- import { tmpdir } from "node:os";
19
-
20
- import { acquireOwnerLock, releaseOwnerLock } from "../owner-lock.mjs";
21
- import { directoryIdentity, pathIdentity, sameDirectoryIdentity, samePathIdentity } from "../secure-path-read.mjs";
22
- import { containsSecret } from "../secret-shapes.mjs";
23
- import { decodeStrictUtf8, parseStrictJson } from "../strict-json.mjs";
24
-
25
- export const PROPOSAL_MAX_BYTES = 64 * 1024;
26
- export const SKILL_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u;
27
- export const RECORD_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u;
28
- const RFC3339 = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(Z|([+-])(\d{2}):(\d{2}))$/u;
29
- const PORTABLE_FORBIDDEN = /[\\:*?"<>|\u0000-\u001f\u007f-\u009f]/u;
30
- const RESERVED_BASENAME = /^(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/iu;
31
-
32
- export const isPlainObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
33
-
34
- export function requireExactKeys(value, required, optional = []) {
35
- if (!isPlainObject(value)) return false;
36
- const allowed = new Set([...required, ...optional]);
37
- return required.every((key) => Object.hasOwn(value, key))
38
- && Object.keys(value).every((key) => allowed.has(key));
39
- }
40
-
41
- export function parseTimestamp(value) {
42
- if (typeof value !== "string") return null;
43
- const match = value.match(RFC3339);
44
- if (!match) return null;
45
- const [, year, month, day, hour, minute, second, , , offsetHour, offsetMinute] = match;
46
- const parts = [year, month, day, hour, minute, second].map(Number);
47
- const [yyyy, mm, dd, hh, min, ss] = parts;
48
- const calendarProbe = new Date(Date.UTC(yyyy, mm - 1, dd, hh, min, ss));
49
- if (calendarProbe.getUTCFullYear() !== yyyy || calendarProbe.getUTCMonth() !== mm - 1
50
- || calendarProbe.getUTCDate() !== dd || calendarProbe.getUTCHours() !== hh
51
- || calendarProbe.getUTCMinutes() !== min || calendarProbe.getUTCSeconds() !== ss
52
- || (offsetHour !== undefined && (Number(offsetHour) > 23 || Number(offsetMinute) > 59))) return null;
53
- const milliseconds = Date.parse(value);
54
- return Number.isFinite(milliseconds) ? milliseconds : null;
55
- }
56
-
57
- export function portablePathIdentity(value) {
58
- if (typeof value !== "string" || value === "" || value.length > 1024
59
- || isAbsolute(value) || /^[A-Za-z]:/u.test(value) || value.startsWith("//")
60
- || value.includes("//") || PORTABLE_FORBIDDEN.test(value)) return null;
61
- const segments = value.split("/");
62
- if (segments.some((segment) => segment === "" || segment === "." || segment === ".."
63
- || /[. ]$/u.test(segment) || RESERVED_BASENAME.test(segment.split(".")[0]))) return null;
64
- return segments.map((segment) => segment.normalize("NFC").toLocaleLowerCase("en-US")
65
- .replace(/[. ]+$/u, "")).join("/");
66
- }
67
-
68
- export function assertNoSecretStrings(value, fail) {
69
- const visit = (candidate) => {
70
- if (typeof candidate === "string") {
71
- if (containsSecret(candidate)) fail();
72
- return;
73
- }
74
- if (Array.isArray(candidate)) {
75
- for (const item of candidate) visit(item);
76
- return;
77
- }
78
- if (isPlainObject(candidate)) {
79
- for (const [key, item] of Object.entries(candidate)) {
80
- visit(key);
81
- visit(item);
82
- }
83
- }
84
- };
85
- visit(value);
86
- }
87
-
88
- export const projectStateRoot = (root) => join(resolve(root), ".litclaude");
89
- export const skillRootFor = (root) => join(resolve(root), ".claude", "skills");
90
-
91
- export function canonicalSkillRootFor(root, ErrorType, code = "TARGET_PATH_UNSAFE") {
92
- const workspace = assertWorkspace(root, ErrorType, code);
93
- try {
94
- return join(realpathSync.native(workspace), ".claude", "skills");
95
- } catch {
96
- throw new ErrorType(code, "workspace root cannot be canonicalized");
97
- }
98
- }
99
-
100
- export function skillRootsEquivalent(root, targetRoot) {
101
- const expected = skillRootFor(root);
102
- if (targetRoot === expected) return true;
103
- if (typeof targetRoot !== "string" || !isAbsolute(targetRoot) || resolve(targetRoot) !== targetRoot) return false;
104
- const candidateRoot = dirname(dirname(targetRoot));
105
- if (join(candidateRoot, ".claude", "skills") !== targetRoot) return false;
106
- const expectedRoot = resolve(root);
107
- const expectedDetails = lstatSync(expectedRoot, { throwIfNoEntry: false });
108
- const candidateDetails = lstatSync(candidateRoot, { throwIfNoEntry: false });
109
- if (!expectedDetails?.isDirectory() || expectedDetails.isSymbolicLink()
110
- || !candidateDetails?.isDirectory() || candidateDetails.isSymbolicLink()) return false;
111
- try {
112
- return realpathSync.native(expectedRoot) === realpathSync.native(candidateRoot);
113
- } catch {
114
- return false;
115
- }
116
- }
117
-
118
- export function assertWorkspace(root, ErrorType, code = "TARGET_PATH_UNSAFE") {
119
- const path = resolve(root);
120
- const details = lstatSync(path, { throwIfNoEntry: false });
121
- if (!details?.isDirectory() || details.isSymbolicLink()) throw new ErrorType(code, "workspace root is unsafe");
122
- return path;
123
- }
124
-
125
- export function ensureSafeDirectoryChain(root, parts, { create = false, ErrorType, code }) {
126
- let current = assertWorkspace(root, ErrorType, code);
127
- for (const part of parts) {
128
- current = join(current, part);
129
- let details = lstatSync(current, { throwIfNoEntry: false });
130
- if (!details && create) {
131
- mkdirSync(current, { mode: 0o700 });
132
- details = lstatSync(current, { throwIfNoEntry: false });
133
- }
134
- if (!details) return null;
135
- if (!details.isDirectory() || details.isSymbolicLink()) {
136
- throw new ErrorType(code, "state path contains a non-directory or symbolic link");
137
- }
138
- }
139
- return current;
140
- }
141
-
142
- export function captureDirectoryIdentity(path, ErrorType, code, { allowMissing = false } = {}) {
143
- const details = lstatSync(path, { throwIfNoEntry: false });
144
- if (!details) {
145
- if (allowMissing) return null;
146
- throw new ErrorType(code, "expected directory is missing");
147
- }
148
- if (!details.isDirectory() || details.isSymbolicLink()) {
149
- throw new ErrorType(code, "expected path is not a real directory");
150
- }
151
- return directoryIdentity(details);
152
- }
153
-
154
- export function directoryIdentityMatches(path, expected) {
155
- const details = lstatSync(path, { throwIfNoEntry: false });
156
- if (expected === null) return !details;
157
- return Boolean(details?.isDirectory() && !details.isSymbolicLink()
158
- && sameDirectoryIdentity(directoryIdentity(details), expected));
159
- }
160
-
161
- export function assertRegularSingleLink(path, ErrorType, code, { maxBytes } = {}) {
162
- const named = lstatSync(path, { throwIfNoEntry: false });
163
- if (!named?.isFile() || named.isSymbolicLink() || named.nlink !== 1
164
- || (maxBytes !== undefined && named.size > maxBytes)) {
165
- throw new ErrorType(code, "record must be a bounded regular file with one link");
166
- }
167
- let descriptor;
168
- try {
169
- descriptor = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
170
- const opened = fstatSync(descriptor);
171
- if (!opened.isFile() || opened.nlink !== 1 || !samePathIdentity(pathIdentity(opened), pathIdentity(named))
172
- || opened.size !== named.size || opened.mtimeMs !== named.mtimeMs) {
173
- throw new ErrorType(code, "record identity changed while opening");
174
- }
175
- const bytes = readFileSync(descriptor);
176
- const after = fstatSync(descriptor);
177
- if (!samePathIdentity(pathIdentity(after), pathIdentity(opened)) || after.size !== opened.size
178
- || after.mtimeMs !== opened.mtimeMs) {
179
- throw new ErrorType(code, "record identity changed while reading");
180
- }
181
- return bytes;
182
- } finally {
183
- if (descriptor !== undefined) closeSync(descriptor);
184
- }
185
- }
186
-
187
- export function readStrictJsonFile(path, ErrorType, code, options) {
188
- const bytes = assertRegularSingleLink(path, ErrorType, code, options);
189
- try {
190
- return parseStrictJson(decodeSkillLoopUtf8(bytes));
191
- } catch (error) {
192
- if (error instanceof ErrorType) throw error;
193
- throw new ErrorType(options?.parseCode ?? code, "record is not strict JSON");
194
- }
195
- }
196
-
197
- export function decodeSkillLoopUtf8(bytes) {
198
- if (bytes.length >= 3 && bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf) {
199
- throw new TypeError("UTF-8 BOM is not permitted");
200
- }
201
- return decodeStrictUtf8(bytes);
202
- }
203
-
204
- export function atomicWriteJson(path, value, ErrorType, code) {
205
- return atomicWriteBytes(path, prettyJsonBytes(value), ErrorType, code);
206
- }
207
-
208
- export function prettyJsonBytes(value) {
209
- return Buffer.from(`${JSON.stringify(value, null, 2)}\n`, "utf8");
210
- }
211
-
212
- export function atomicWriteBytes(path, bytes, ErrorType, code) {
213
- const directory = dirname(path);
214
- const current = lstatSync(path, { throwIfNoEntry: false });
215
- if (current && (!current.isFile() || current.isSymbolicLink() || current.nlink !== 1)) {
216
- throw new ErrorType(code, "destination is not a regular single-link file");
217
- }
218
- const temporary = join(directory, `.${randomUUID()}.tmp`);
219
- let descriptor;
220
- try {
221
- writeFileSync(temporary, bytes, { flag: "wx", mode: 0o600 });
222
- descriptor = openSync(temporary, "r");
223
- fsyncSync(descriptor);
224
- closeSync(descriptor);
225
- descriptor = undefined;
226
- renameSync(temporary, path);
227
- try {
228
- descriptor = openSync(directory, "r");
229
- fsyncSync(descriptor);
230
- } catch {
231
- // The file data and rename are already durable on filesystems that reject directory fsync.
232
- }
233
- } catch (error) {
234
- if (error instanceof ErrorType) throw error;
235
- throw new ErrorType(code, "atomic state write failed");
236
- } finally {
237
- if (descriptor !== undefined) closeSync(descriptor);
238
- rmSync(temporary, { force: true });
239
- }
240
- }
241
-
242
- export function withSkillLoopLock(root, scope, operation, ErrorType, code) {
243
- const realRoot = realpathSync.native(assertWorkspace(root, ErrorType, code));
244
- const digest = createHash("sha256").update(`${realRoot}\0${scope}`).digest("hex");
245
- const lockPath = join(tmpdir(), "litclaude-skill-loop-locks", `${digest}.lock`);
246
- let owner;
247
- try {
248
- owner = acquireOwnerLock(lockPath, { timeoutMs: 2_000, staleMs: 0 });
249
- } catch (error) {
250
- if (error instanceof ErrorType) throw error;
251
- throw new ErrorType(code, "skill-loop lock failed");
252
- }
253
- let result;
254
- let operationError;
255
- try {
256
- result = operation();
257
- } catch (error) {
258
- operationError = error;
259
- }
260
- if (!releaseOwnerLock(lockPath, owner)) throw new ErrorType(code, "skill-loop lock release failed");
261
- if (operationError) throw operationError;
262
- return result;
263
- }
264
-
265
- export function isContained(root, child) {
266
- const rel = relative(resolve(root), resolve(child));
267
- return rel === "" || (!isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`));
268
- }
269
-
270
- export function sha256(bytes) {
271
- return createHash("sha256").update(bytes).digest("hex");
272
- }
273
-
274
- export function safeIsoPath(timestamp) {
275
- return timestamp.replace(/[:.]/gu, "-");
276
- }
277
-
278
- export function pathExists(path) {
279
- return existsSync(path);
280
- }
@@ -1,91 +0,0 @@
1
- import { lstatSync } from "node:fs";
2
- import { join } from "node:path";
3
-
4
- import {
5
- atomicWriteJson,
6
- ensureSafeDirectoryChain,
7
- isPlainObject,
8
- parseTimestamp,
9
- prettyJsonBytes,
10
- readStrictJsonFile,
11
- requireExactKeys,
12
- withSkillLoopLock,
13
- } from "./common.mjs";
14
- import { recoverSkillMutationTransactionUnlocked } from "./mutation-transaction.mjs";
15
-
16
- class SkillLoopConfigError extends Error {
17
- constructor(code, message = code) {
18
- super(`${code}: ${message}`);
19
- this.name = "SkillLoopConfigError";
20
- this.code = code;
21
- }
22
- }
23
-
24
- export const DEFAULT_SKILL_LOOP_CONFIG = Object.freeze({
25
- autoApply: false,
26
- nudgeInterval: 10,
27
- curator: Object.freeze({
28
- intervalDays: 7,
29
- minIdleHours: 2,
30
- staleAfterDays: 30,
31
- archiveAfterDays: 90,
32
- delete: false,
33
- consolidation: false,
34
- }),
35
- });
36
-
37
- const cloneDefaults = () => ({ ...DEFAULT_SKILL_LOOP_CONFIG, curator: { ...DEFAULT_SKILL_LOOP_CONFIG.curator } });
38
- const configPath = (root, create) => {
39
- const state = ensureSafeDirectoryChain(root, [".litclaude"], {
40
- create,
41
- ErrorType: SkillLoopConfigError,
42
- code: "CONFIG_STORE_UNSAFE",
43
- });
44
- return state === null ? null : join(state, "skill-loop.json");
45
- };
46
-
47
- export function validateSkillLoopConfig(value) {
48
- let persisted;
49
- try {
50
- persisted = prettyJsonBytes(value);
51
- } catch {
52
- throw new SkillLoopConfigError("CONFIG_INVALID", "skill-loop config is invalid");
53
- }
54
- if (persisted.length > 64 * 1024) throw new SkillLoopConfigError("CONFIG_TOO_LARGE");
55
- if (!requireExactKeys(value, ["autoApply", "nudgeInterval", "curator"], ["lastCuratorRunAt"])
56
- || typeof value.autoApply !== "boolean"
57
- || !Number.isSafeInteger(value.nudgeInterval) || value.nudgeInterval < 1
58
- || !requireExactKeys(value.curator, [
59
- "intervalDays", "minIdleHours", "staleAfterDays", "archiveAfterDays", "delete", "consolidation",
60
- ])
61
- || !["intervalDays", "minIdleHours", "staleAfterDays", "archiveAfterDays"].every(
62
- (field) => Number.isFinite(value.curator[field]) && value.curator[field] >= 0,
63
- )
64
- || typeof value.curator.delete !== "boolean" || typeof value.curator.consolidation !== "boolean"
65
- || value.curator.delete !== false || value.curator.consolidation !== false
66
- || (Object.hasOwn(value, "lastCuratorRunAt") && parseTimestamp(value.lastCuratorRunAt) === null)) {
67
- throw new SkillLoopConfigError("CONFIG_INVALID", "skill-loop config is invalid");
68
- }
69
- return value;
70
- }
71
-
72
- export function readSkillLoopConfig(root) {
73
- const path = configPath(root, false);
74
- if (path === null || !lstatSync(path, { throwIfNoEntry: false })) return cloneDefaults();
75
- const parsed = readStrictJsonFile(path, SkillLoopConfigError, "CONFIG_STORE_UNSAFE", { maxBytes: 64 * 1024 });
76
- if (!isPlainObject(parsed)) throw new SkillLoopConfigError("CONFIG_INVALID", "skill-loop config is invalid");
77
- return validateSkillLoopConfig(parsed);
78
- }
79
-
80
- export function writeSkillLoopConfigUnlocked(root, value) {
81
- validateSkillLoopConfig(value);
82
- atomicWriteJson(configPath(root, true), value, SkillLoopConfigError, "CONFIG_STORE_UNSAFE");
83
- return value;
84
- }
85
-
86
- export function writeSkillLoopConfig(root, value) {
87
- return withSkillLoopLock(root, "transaction", () => {
88
- recoverSkillMutationTransactionUnlocked(root);
89
- return writeSkillLoopConfigUnlocked(root, value);
90
- }, SkillLoopConfigError, "CONFIG_STORE_UNSAFE");
91
- }