@amsterdamdatalabs/enact-extensions 0.1.54 → 0.1.56

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.
@@ -14,16 +14,17 @@
14
14
  * match; every merged JSON entry /
15
15
  * TOML block present; list drifted
16
16
  * or missing)
17
- * - per-host status: claude (marketplace + settings present; trust +
18
- * "start a new session" note), codex (files present; read-only
17
+ * - per-host status: direct native files recorded in the lock (never a
18
+ * plugin root or marketplace), codex (files present; read-only
19
19
  * ~/.codex/config.toml projects."<repo>".trust_level; hooks-review-
20
20
  * pending note), cursor (files present; MCP approval note), kimi
21
- * (.kimi-code/mcp.json present; global hooks are enact-vps-owned, so we
22
- * only report whether ~/.kimi-code/config.toml has the
21
+ * (native files present; global hooks are enact-vps-owned, so we only
22
+ * report whether ~/.kimi-code/config.toml has the
23
23
  * `# BEGIN ENACT KIMI HOOKS` block; PLUS a read-only Kimi Code
24
24
  * workspace-trust check — Kimi only loads project MCP servers, including
25
25
  * `.kimi-code/mcp.json`, once the workspace is trusted), opencode
26
- * (plugin file present and generated from the current bundle)
26
+ * (native agents/skills and root `opencode.json` MCP entries when the
27
+ * selected bundle declares them; hooks are global enact-vps-owned)
27
28
  * - required global binaries on PATH (enact-hook, lean-ctx,
28
29
  * code-review-graph/crg, azure-devops-mcp) with versions
29
30
  * - root `enact-config.toml` marker: present, parses as TOML, has our
@@ -332,14 +333,9 @@ function checkLockIntegrity(repo, name, lock) {
332
333
  const found = existsSync(path) ? findBlock(readFileSync(path, "utf8"), name) : null;
333
334
  if (!found || sha256(found.block) !== record.sha256) tomlIssues.push(rel);
334
335
  }
335
- // Managed symlinks (today: the Claude plugin's skills -> ../../.agents/skills
336
- // link). These carry no content hash, so `lock.files`-style verification does
337
- // not reach them: a broken or retargeted link is invisible to every other
338
- // check here. It is not a cosmetic gap -- a dangling link makes Claude Code
339
- // report `Skills (0)` while the repo still looks installed, which is exactly
340
- // the "presence is not armedness" failure AGENTS.md exists to catch. Verify
341
- // by the link's own mechanism: it must BE a symlink, point where the lock
342
- // says, and actually resolve.
336
+ // Managed symlinks carry no content hash, so `lock.files`-style verification
337
+ // does not reach them. Verify by the link's own mechanism: it must BE a
338
+ // symlink, point where the lock says, and actually resolve.
343
339
  //
344
340
  // lstatSync, never existsSync: existsSync follows the link and returns false
345
341
  // for a dangling one, so it cannot tell "absent" from "present but broken".
@@ -374,48 +370,76 @@ function checkLockIntegrity(repo, name, lock) {
374
370
  // ---------------------------------------------------------------------------
375
371
  // per-host status
376
372
  // ---------------------------------------------------------------------------
377
- function checkHosts(repo, lock, home, contentOutdated) {
373
+ const NATIVE_HOST_PATHS = {
374
+ claude: { prefixes: [".claude/"], exact: [".mcp.json"] },
375
+ codex: { prefixes: [".codex/", ".agents/skills/"], exact: [] },
376
+ cursor: { prefixes: [".cursor/"], exact: [] },
377
+ kimi: { prefixes: [".kimi-code/"], exact: [] },
378
+ opencode: { prefixes: [".opencode/"], exact: ["opencode.json"] },
379
+ };
380
+
381
+ function isNativeHostPath(host, rel) {
382
+ const { prefixes, exact } = NATIVE_HOST_PATHS[host];
383
+ return exact.includes(rel) || prefixes.some((prefix) => rel.startsWith(prefix));
384
+ }
385
+
386
+ // The lock is the authoritative ownership record. A host is present only
387
+ // when it owns at least one direct native path and every such path still
388
+ // exists. This deliberately does not infer installation from a plugin root,
389
+ // marketplace, or a host config file that another extension/user may own.
390
+ function checkNativeHostPresence(repo, lock, host) {
391
+ const rels = [
392
+ ...Object.keys(lock.files ?? {}),
393
+ ...Object.keys(lock.merged ?? {}),
394
+ ...Object.keys(lock.toml ?? {}),
395
+ ...Object.keys(lock.symlinks ?? {}),
396
+ ].filter((rel) => isNativeHostPath(host, rel));
397
+ const nativePaths = [...new Set(rels)].sort();
398
+ return {
399
+ nativePaths,
400
+ present: nativePaths.length > 0 && nativePaths.every((rel) => existsSync(abs(repo, rel))),
401
+ };
402
+ }
403
+
404
+ function checkHosts(repo, lock, home) {
378
405
  const hosts = {};
379
406
  for (const host of REPO_HOSTS) {
380
407
  const targeted = lock.hosts.includes(host);
408
+ const native = targeted ? checkNativeHostPresence(repo, lock, host) : { nativePaths: [], present: true };
381
409
  if (host === "claude") {
382
- const present =
383
- !targeted ||
384
- (existsSync(abs(repo, ".claude-plugin/marketplace.json")) && existsSync(abs(repo, ".claude/settings.json")));
385
410
  hosts.claude = {
386
411
  targeted,
387
- present,
412
+ present: native.present,
413
+ nativePaths: native.nativePaths,
388
414
  note: targeted
389
- ? "loads only after you TRUST this folder in Claude Code -- the marketplace registers on first launch; if hooks/skills are missing in that FIRST session, start a NEW session in this folder"
415
+ ? "direct native Claude files are installed; Claude Code loads them after you TRUST this folder"
390
416
  : "not targeted by this install",
391
417
  };
392
418
  } else if (host === "codex") {
393
- const present =
394
- !targeted || existsSync(abs(repo, ".codex/hooks.json")) || existsSync(abs(repo, ".codex/config.toml"));
395
419
  hosts.codex = {
396
420
  targeted,
397
- present,
421
+ present: native.present,
422
+ nativePaths: native.nativePaths,
398
423
  trust: checkCodexTrust(repo, home),
399
424
  note: targeted
400
425
  ? "config/hooks load only after you trust this project; hooks review (/hooks) is pending until you review them in Codex"
401
426
  : "not targeted by this install",
402
427
  };
403
428
  } else if (host === "cursor") {
404
- const present =
405
- !targeted || existsSync(abs(repo, ".cursor/hooks.json")) || existsSync(abs(repo, ".cursor/mcp.json"));
406
429
  hosts.cursor = {
407
430
  targeted,
408
- present,
431
+ present: native.present,
432
+ nativePaths: native.nativePaths,
409
433
  note: targeted
410
434
  ? "Cursor prompts for MCP server approval on first use -- approve code-review-graph/lean-ctx in Cursor's MCP settings"
411
435
  : "not targeted by this install",
412
436
  };
413
437
  } else if (host === "kimi") {
414
- const present = !targeted || existsSync(abs(repo, ".kimi-code/mcp.json"));
415
438
  const globalHooks = checkKimiGlobalHooks(home);
416
439
  hosts.kimi = {
417
440
  targeted,
418
- present,
441
+ present: native.present,
442
+ nativePaths: native.nativePaths,
419
443
  globalHooksInstalled: globalHooks.hooksBlockPresent,
420
444
  globalHooksNote: !globalHooks.configFound
421
445
  ? `${globalHooks.configPath} not found`
@@ -425,15 +449,12 @@ function checkHosts(repo, lock, home, contentOutdated) {
425
449
  workspaceTrust: checkKimiWorkspaceTrust(repo),
426
450
  };
427
451
  } else if (host === "opencode") {
428
- const rel = `.opencode/plugins/${lock.plugin}.ts`;
429
- const present = !targeted || existsSync(abs(repo, rel));
430
- const generatedFromCurrentBundle = targeted && present ? !contentOutdated.includes(rel) : null;
431
452
  hosts.opencode = {
432
453
  targeted,
433
- present,
434
- generatedFromCurrentBundle,
454
+ present: native.present,
455
+ nativePaths: native.nativePaths,
435
456
  note: targeted
436
- ? "one generated plugin file; re-run install after a bundle update to regenerate it"
457
+ ? "direct native OpenCode files are installed; repo-local hooks are global enact-vps-owned"
437
458
  : "not targeted by this install",
438
459
  };
439
460
  }
@@ -567,7 +588,7 @@ export function runRepoDoctor(pluginRoot, options = {}) {
567
588
  );
568
589
  }
569
590
 
570
- const hosts = checkHosts(repo, lock, home, contentOutdated);
591
+ const hosts = checkHosts(repo, lock, home);
571
592
 
572
593
  const binaries = {};
573
594
  for (const bin of REQUIRED_BINARIES) binaries[bin] = checkBinary(bin);