@litfamily/litopencode 1.0.10 → 1.0.12

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.
@@ -26,6 +26,8 @@ export type ChatMessageActivationOptions = {
26
26
  readonly onSkillActivation?: (sessionID: string, skillId: string, appendedLit?: boolean) => void | Promise<void>;
27
27
  /** Consumes the skill a trusted command activation recorded for this session, if any. */
28
28
  readonly trustedInjectedSkill?: (sessionID: string) => string | undefined;
29
+ /** Restores that skill after the root-turn reset; the command already showed its activation toast. */
30
+ readonly onTrustedSkillResume?: (sessionID: string, skillId: string) => void | Promise<void>;
29
31
  /** Consumes the mark `command.execute.before` left for this session's next message. */
30
32
  readonly isCommandTurn?: (sessionID: string) => boolean;
31
33
  readonly skillHint?: (sessionID: string, promptText: string) => Promise<JevSkillHintPart | undefined>;
@@ -100,7 +100,7 @@ export function createChatMessageActivationHook(projectRoot, options = {}) {
100
100
  if (!childSession) {
101
101
  await options.onRootUserTurn?.(input.sessionID);
102
102
  if (trustedInjectedSkill !== undefined) {
103
- await options.onSkillActivation?.(input.sessionID, trustedInjectedSkill);
103
+ await options.onTrustedSkillResume?.(input.sessionID, trustedInjectedSkill);
104
104
  return;
105
105
  }
106
106
  }
@@ -22,6 +22,8 @@ export type JevSkillHintOptions = {
22
22
  readonly fetch?: JevFetch;
23
23
  readonly catalog?: readonly JevCatalogEntry[];
24
24
  readonly traceFile?: string;
25
+ /** Project root: every folder between it and the trace file must be a real folder, never a symlink. */
26
+ readonly traceRoot?: string;
25
27
  readonly now?: () => number;
26
28
  };
27
29
  export declare function jevSkillHintStatus(env?: NodeJS.ProcessEnv): JevSkillHintStatus;
@@ -155,6 +155,27 @@ function validatedAnswer(raw, allowed, threshold) {
155
155
  return { confidence };
156
156
  return { choice, confidence };
157
157
  }
158
+ /**
159
+ * Walks the trace folder below the project root one level at a time, creating a missing folder
160
+ * without following links. Any symlink or non-folder on the way means no trace.
161
+ */
162
+ async function realTraceFolder(root, folder) {
163
+ const relative = path.relative(root, folder);
164
+ if (relative.startsWith("..") || path.isAbsolute(relative))
165
+ return false;
166
+ let current = root;
167
+ for (const segment of relative.split(path.sep).filter((part) => part !== "")) {
168
+ current = path.join(current, segment);
169
+ let stat = await fs.lstat(current).catch(() => undefined);
170
+ if (stat === undefined) {
171
+ await fs.mkdir(current).catch(() => undefined);
172
+ stat = await fs.lstat(current).catch(() => undefined);
173
+ }
174
+ if (stat === undefined || stat.isSymbolicLink() || !stat.isDirectory())
175
+ return false;
176
+ }
177
+ return true;
178
+ }
158
179
  export function createJevSkillHint(options = {}) {
159
180
  const env = options.env ?? process.env;
160
181
  const fetchImpl = options.fetch ?? ((url, init) => fetch(url, init));
@@ -163,8 +184,11 @@ export function createJevSkillHint(options = {}) {
163
184
  async function trace(record) {
164
185
  if (env[`${jevFlag}_TRACE`] !== "1" || options.traceFile === undefined)
165
186
  return;
187
+ const folder = path.dirname(options.traceFile);
188
+ const root = options.traceRoot ?? path.dirname(folder);
166
189
  try {
167
- await fs.mkdir(path.dirname(options.traceFile), { recursive: true });
190
+ if (!(await realTraceFolder(root, folder)))
191
+ return;
168
192
  // Refuse a symlink or other non-file at the trace path; O_NOFOLLOW closes the lstat race.
169
193
  const existing = await fs.lstat(options.traceFile).catch(() => undefined);
170
194
  if (existing !== undefined && !existing.isFile())
@@ -172,6 +196,9 @@ export function createJevSkillHint(options = {}) {
172
196
  const flags = fsConstants.O_WRONLY | fsConstants.O_APPEND | fsConstants.O_CREAT | (fsConstants.O_NOFOLLOW ?? 0);
173
197
  const handle = await fs.open(options.traceFile, flags, 0o600);
174
198
  try {
199
+ // A folder swapped for a symlink after the first check still gets no record.
200
+ if (!(await realTraceFolder(root, folder)))
201
+ return;
175
202
  await handle.appendFile(`${JSON.stringify(record)}\n`, "utf8");
176
203
  }
177
204
  finally {
package/dist/knowledge.js CHANGED
@@ -340,6 +340,9 @@ async function readRegularFileIfPresent(filePath, paths, options = {}) {
340
340
  try {
341
341
  await assertParentStable(parent, paths);
342
342
  const descriptorStat = await handle.stat();
343
+ if (descriptorStat.isFile() && descriptorStat.nlink === 0) {
344
+ throw new KnowledgeFileDisappearedError(`Knowledge file disappeared after descriptor open at ${filePath}.`);
345
+ }
343
346
  if (!descriptorStat.isFile() || !sameFileIdentity(fileIdentityOf(descriptorStat), fileIdentityOf(pathStat))) {
344
347
  throw new KnowledgeStoreError(`Unsafe knowledge file replacement at ${filePath}.`);
345
348
  }
package/dist/server.js CHANGED
@@ -54,7 +54,10 @@ export function createLitOpenCodePlugin(autoUpdateRunner = runPluginAutoUpdate)
54
54
  ignitionState.activate(sessionID, skillId, !appendedLit);
55
55
  };
56
56
  const boundedAuthorityEvent = createBoundedAuthorityEventHook(root, input?.client, loaded.config.boundedAuthority);
57
- const skillHint = createJevSkillHint({ traceFile: path.join(loaded.paths.logsDir, "jev-skill-hint.jsonl") });
57
+ const skillHint = createJevSkillHint({
58
+ traceFile: path.join(loaded.paths.logsDir, "jev-skill-hint.jsonl"),
59
+ traceRoot: loaded.paths.projectRoot
60
+ });
58
61
  // Keyed by session: the host resolves command parts into a new array before chat.message runs.
59
62
  const pendingCommand = new Set();
60
63
  const trustedCommandActivations = new Map();
@@ -96,6 +99,9 @@ export function createLitOpenCodePlugin(autoUpdateRunner = runPluginAutoUpdate)
96
99
  const skillId = trustedCommandActivations.get(sessionID);
97
100
  trustedCommandActivations.delete(sessionID);
98
101
  return skillId;
102
+ },
103
+ onTrustedSkillResume: (sessionID, skillId) => {
104
+ ignitionState.activate(sessionID, skillId);
99
105
  }
100
106
  }),
101
107
  "command.execute.before": async (commandInput, commandOutput) => {
@@ -1 +1 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" width="146" height="20" role="img" aria-label="release: 1.0.10"><title>release: 1.0.10</title><g shape-rendering="crispEdges"><rect width="49" height="20" fill="#080d14"/><rect x="49" width="97" height="20" fill="#ff6337"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text x="255" y="140" textLength="390" transform="scale(.1)">release</text><text x="965" y="140" textLength="870" transform="scale(.1)" fill="#080D14">1.0.10</text></g></svg>
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="146" height="20" role="img" aria-label="release: 1.0.12"><title>release: 1.0.12</title><g shape-rendering="crispEdges"><rect width="49" height="20" fill="#080d14"/><rect x="49" width="97" height="20" fill="#ff6337"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text x="255" y="140" textLength="390" transform="scale(.1)">release</text><text x="965" y="140" textLength="870" transform="scale(.1)" fill="#080D14">1.0.12</text></g></svg>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@litfamily/litopencode",
3
- "version": "1.0.10",
3
+ "version": "1.0.12",
4
4
  "description": "LitOpenCode OpenCode plugin bootstrap package.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -28,10 +28,12 @@
28
28
  "build": "node tools/run-build.mjs",
29
29
  "check:pack-payload": "npm run build && npm pack --dry-run --json --ignore-scripts | node tools/check-pack-payload.mjs --stdin",
30
30
  "check:version": "node tools/check-version-lockstep.mjs",
31
+ "check:npm-readme": "node tools/readme-for-npm.mjs check",
31
32
  "check:managed-skill-manifest": "node tools/gen-managed-skill-manifest.mjs --check",
32
33
  "gen:managed-skill-manifest": "node tools/gen-managed-skill-manifest.mjs",
33
34
  "prepublishOnly": "node tools/prepublish-test-gate.mjs",
34
- "prepack": "npm run build",
35
+ "prepack": "npm run build && node tools/readme-for-npm.mjs apply",
36
+ "postpack": "node tools/readme-for-npm.mjs restore",
35
37
  "pretest": "npm run build",
36
38
  "qa:behavior-replacement": "node tools/run-behavior-replacement-probes.mjs",
37
39
  "qa:installed-resource-tamper": "node tools/run-installed-resource-tamper-probe.mjs",
@@ -10,7 +10,7 @@
10
10
  "dependencies": {
11
11
  "opentype.js": "1.3.4",
12
12
  "playwright-core": "1.55.1",
13
- "ws": "8.18.3"
13
+ "ws": "8.22.0"
14
14
  }
15
15
  },
16
16
  "node_modules/opentype.js": {
@@ -54,9 +54,9 @@
54
54
  "license": "MIT"
55
55
  },
56
56
  "node_modules/ws": {
57
- "version": "8.18.3",
58
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
59
- "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
57
+ "version": "8.22.0",
58
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz",
59
+ "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==",
60
60
  "license": "MIT",
61
61
  "engines": {
62
62
  "node": ">=10.0.0"
@@ -6,6 +6,6 @@
6
6
  "dependencies": {
7
7
  "opentype.js": "1.3.4",
8
8
  "playwright-core": "1.55.1",
9
- "ws": "8.18.3"
9
+ "ws": "8.22.0"
10
10
  }
11
11
  }
@@ -3273,11 +3273,11 @@
3273
3273
  },
3274
3274
  {
3275
3275
  "path": "package-lock.json",
3276
- "sha256": "20c343e515fe08f7e4b0cf07a5ba0b91462d0f22b4ff3531448206ac24a475f1"
3276
+ "sha256": "141539ca253c3cf4f81988db6f69aaf78574d0c8ca9e9d7e45f56af8f2e96cac"
3277
3277
  },
3278
3278
  {
3279
3279
  "path": "package.json",
3280
- "sha256": "e75e13ba0060e99b21e8e8c945710ecf3a65e44f851f3ebde54ab7f562c96400"
3280
+ "sha256": "5962e5607dba2b2d8e08fbf51e982e8517ee26a84c43ac838934e8394f3551cd"
3281
3281
  },
3282
3282
  {
3283
3283
  "path": "probe.mjs",
@@ -13,6 +13,9 @@ const FORBIDDEN_PATH_RULES = [
13
13
  { label: "repository cover vector", pattern: /^docs\/assets\/cover\.svg$/u },
14
14
  { label: "unapproved README presentation asset", pattern: /^docs\/assets\/readme\/(?!(?:ascii-readme|badge-version|badge-license|lucide-book-open|lucide-play|lucide-shield-check)\.svg$|litopencode-wordmark\.svg$|litopencode-clay-icon\.png$|(?:litfamily-machines|poster)\.png$|ignition-film\.mp4$|ignition-readme\.gif$|(?:Lucide-LICENSE|JetBrainsMono-OFL)\.txt$)/u },
15
15
  { label: "maintainer release checklist", pattern: /^docs\/release-checklist\.md$/u },
16
+ { label: "npm README source (packed as README.md / README-Ko-KR.md)", pattern: /^README-npm(?:-Ko-KR)?\.md$/u },
17
+ { label: "GitHub README backup from the npm README swap", pattern: /^\.readme-npm-backup(?:\/|$)/u },
18
+ { label: "npm README swap script", pattern: /^tools\/readme-for-npm\.mjs$/u },
16
19
  { label: "internal recon/spec document", pattern: /^docs\/(?:recon|spec)(\/|$)/u },
17
20
  { label: "implementation plan", pattern: /(^|\/)plans(\/|$)/u },
18
21
  { label: ".litcodex runtime state", pattern: /(^|\/)\.litcodex(\/|$)/u },
@@ -70,18 +73,33 @@ const REQUIRED_README_PATHS = [
70
73
  "docs/assets/readme/poster.png"
71
74
  ];
72
75
 
73
- // Every image the bilingual READMEs load from the package CDN must ship in the tarball.
76
+ // Every image the READMEs show must ship: the npm pages load pinned package-CDN copies, and the
77
+ // GitHub pages load the same files by relative path. Inside a packed artifact the npm sources
78
+ // are absent and README.md / README-Ko-KR.md already hold the npm pages.
74
79
  async function readmeImagePaths() {
75
80
  const { version } = JSON.parse(await fs.readFile("package.json", "utf8"));
76
81
  const prefix = `https://cdn.jsdelivr.net/npm/@litfamily/litopencode@${version}/`;
77
82
  const paths = new Set();
78
- for (const file of ["README.md", "README-Ko-KR.md"]) {
83
+ const sources = [];
84
+ for (const [npmSource, target] of [["README-npm.md", "README.md"], ["README-npm-Ko-KR.md", "README-Ko-KR.md"]]) {
85
+ sources.push(target);
86
+ try {
87
+ await fs.access(npmSource);
88
+ sources.push(npmSource);
89
+ } catch {
90
+ // Packed artifact: the target already is the npm page.
91
+ }
92
+ }
93
+ for (const file of sources) {
79
94
  const text = await fs.readFile(file, "utf8");
80
95
  const targets = [
81
96
  ...[...text.matchAll(/!\[[^\]]*\]\(([^)\s]+)\)/gu)].map((match) => match[1]),
82
97
  ...[...text.matchAll(/\b(?:src|srcset)="([^"\s]+)"/gu)].map((match) => match[1])
83
98
  ];
84
- for (const target of targets) if (target.startsWith(prefix)) paths.add(target.slice(prefix.length));
99
+ for (const target of targets) {
100
+ if (target.startsWith(prefix)) paths.add(target.slice(prefix.length));
101
+ else if (target.startsWith("./")) paths.add(target.slice(2));
102
+ }
85
103
  }
86
104
  return [...paths];
87
105
  }
@@ -11,14 +11,26 @@
11
11
  {
12
12
  "path": "README-Ko-KR.md",
13
13
  "kind": "pinned",
14
- "occurrences": 102,
15
- "why": "The bilingual landing pages pin artwork and package links to the current package version."
14
+ "occurrences": 2,
15
+ "why": "The GitHub landing pages load assets by relative path; the release badge alt text and the checkout-version note repeat the package version."
16
+ },
17
+ {
18
+ "path": "README-npm-Ko-KR.md",
19
+ "kind": "pinned",
20
+ "occurrences": 18,
21
+ "why": "The npm package pages pin artwork and package links to the current package version; tools/readme-for-npm.mjs copies them over README.md and README-Ko-KR.md at pack time."
22
+ },
23
+ {
24
+ "path": "README-npm.md",
25
+ "kind": "pinned",
26
+ "occurrences": 18,
27
+ "why": "The npm package pages pin artwork and package links to the current package version; tools/readme-for-npm.mjs copies them over README.md and README-Ko-KR.md at pack time."
16
28
  },
17
29
  {
18
30
  "path": "README.md",
19
31
  "kind": "pinned",
20
- "occurrences": 101,
21
- "why": "The bilingual landing pages pin artwork and package links to the current package version."
32
+ "occurrences": 2,
33
+ "why": "The GitHub landing pages load assets by relative path; the release badge alt text and the checkout-version note repeat the package version."
22
34
  },
23
35
  {
24
36
  "path": "docs/assets/readme/badge-version.svg",