{"_id":"@adhd/sox-install-engine","_rev":"4-55ba5a87ca5c61af04b8f50177e1916f","name":"@adhd/sox-install-engine","dist-tags":{"latest":"0.4.0"},"versions":{"0.2.0":{"name":"@adhd/sox-install-engine","version":"0.2.0","license":"MIT","_id":"@adhd/sox-install-engine@0.2.0","maintainers":[{"name":"pseudosky","email":"skywinston.sk@gmail.com"}],"dist":{"shasum":"74734df5dd7d697a5196202a9208792a1ca47d9e","tarball":"https://registry.npmjs.org/@adhd/sox-install-engine/-/sox-install-engine-0.2.0.tgz","fileCount":78,"integrity":"sha512-QREpXTn0HouKwU4HK4tDMOxOt4NAJPuj8aVlnZgkNxiymkfMzO3n23Gw4Cvguh4l+S8NE556Luj/MxL0ml+qlA==","signatures":[{"sig":"MEUCIDKYgvfTFKefRerNub/LB1lPipYX85xKgnjP9j1Vk3UyAiEAqFlMc1XSpzzzWLwgWHl3WQRXXv4eCdFMRng2D66uQAE=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":465705},"main":"./dist/index.js","_from":"file:adhd-sox-install-engine-0.2.0.tgz","types":"./dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.js"}},"_npmUser":{"name":"pseudosky","email":"skywinston.sk@gmail.com"},"_resolved":"/private/var/folders/yg/cfczgtx54bzfh74lx2_mv0z80000gp/T/14a87601210cb83eafff59746116b685/adhd-sox-install-engine-0.2.0.tgz","_integrity":"sha512-QREpXTn0HouKwU4HK4tDMOxOt4NAJPuj8aVlnZgkNxiymkfMzO3n23Gw4Cvguh4l+S8NE556Luj/MxL0ml+qlA==","_npmVersion":"11.6.2","description":"Sox install engine — parseArgs, install, cascade, build-index, provider-capabilities","directories":{},"_nodeVersion":"24.11.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/sox-install-engine_0.2.0_1782451185591_0.9966042256839094","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@adhd/sox-install-engine","version":"0.3.0","license":"MIT","_id":"@adhd/sox-install-engine@0.3.0","maintainers":[{"name":"pseudosky","email":"skywinston.sk@gmail.com"}],"dist":{"shasum":"2643cc5b57f9222e0f670d421df57672029b57bc","tarball":"https://registry.npmjs.org/@adhd/sox-install-engine/-/sox-install-engine-0.3.0.tgz","fileCount":86,"integrity":"sha512-mFcJGLnHjTbWCze+pNAD3XFhTTb1L9Ozzn7w4JFaIMcNYVuWUm/SpzaGMjo6cjDoO++WXOg1w5TmqycTShk6WA==","signatures":[{"sig":"MEUCIHquibLr+dswjTliaKVLB4J3/tNnDhoBuk+tMZUYh+kYAiEAiBL/ZxlckOM2e7oqS35/uC3BTF6ZdlocFvhMMn1bq8Y=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":544787},"main":"./dist/index.js","_from":"file:adhd-sox-install-engine-0.3.0.tgz","types":"./dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.js"}},"_npmUser":{"name":"pseudosky","email":"skywinston.sk@gmail.com"},"_resolved":"/private/var/folders/yg/cfczgtx54bzfh74lx2_mv0z80000gp/T/90b41dfec1cfc8acfa5e77c1f6bb5d19/adhd-sox-install-engine-0.3.0.tgz","_integrity":"sha512-mFcJGLnHjTbWCze+pNAD3XFhTTb1L9Ozzn7w4JFaIMcNYVuWUm/SpzaGMjo6cjDoO++WXOg1w5TmqycTShk6WA==","_npmVersion":"11.6.2","description":"Sox install engine — parseArgs, install, cascade, build-index, provider-capabilities","directories":{},"_nodeVersion":"24.11.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/sox-install-engine_0.3.0_1786144738320_0.002291640186216748","host":"s3://npm-registry-packages-npm-production"}},"0.3.1":{"name":"@adhd/sox-install-engine","version":"0.3.1","license":"MIT","_id":"@adhd/sox-install-engine@0.3.1","maintainers":[{"name":"pseudosky","email":"skywinston.sk@gmail.com"}],"dist":{"shasum":"7bb38dc14606075ec070765b935a201ad96d8d86","tarball":"https://registry.npmjs.org/@adhd/sox-install-engine/-/sox-install-engine-0.3.1.tgz","fileCount":90,"integrity":"sha512-ATzKbq2pU/2WOPNUYrC2oyI9tLeo/ff2RKkUsZ5f8wKc6rXO/VZaR/iQoCA7WhR2EKeGk/s+ANcDqho/quHBQg==","signatures":[{"sig":"MEYCIQD23m00YA9FQLqAl7yTOdd+a/JDSTqtznle2V97haLj9AIhAJCjm40LZI1uutahUnuO6SOkJmGYJoF5BcE0RGYE74NN","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":567780},"main":"./dist/index.js","_from":"file:adhd-sox-install-engine-0.3.1.tgz","types":"./dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.js"}},"_npmUser":{"name":"pseudosky","email":"skywinston.sk@gmail.com"},"_resolved":"/private/var/folders/yg/cfczgtx54bzfh74lx2_mv0z80000gp/T/ba1b1d0aa27b3757f9dccc389647254b/adhd-sox-install-engine-0.3.1.tgz","_integrity":"sha512-ATzKbq2pU/2WOPNUYrC2oyI9tLeo/ff2RKkUsZ5f8wKc6rXO/VZaR/iQoCA7WhR2EKeGk/s+ANcDqho/quHBQg==","_npmVersion":"11.6.2","description":"Sox install engine — parseArgs, install, cascade, build-index, provider-capabilities","directories":{},"_nodeVersion":"24.11.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/sox-install-engine_0.3.1_1787113047922_0.8134379213575187","host":"s3://npm-registry-packages-npm-production"}},"0.4.0":{"name":"@adhd/sox-install-engine","version":"0.4.0","description":"Sox install engine — parseArgs, install, cascade, build-index, provider-capabilities","license":"MIT","publishConfig":{"access":"public"},"engines":{"node":">=20"},"main":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.js"}},"keywords":["install","registry","mcp","typescript"],"repository":{"type":"git","url":"git+https://github.com/PseudoSky/adhd.git"},"homepage":"https://github.com/PseudoSky/adhd","_id":"@adhd/sox-install-engine@0.4.0","bugs":{"url":"https://github.com/PseudoSky/adhd/issues"},"_integrity":"sha512-eIwc353Tjez/+XbzvC2c5xoUx5WqT9QPJs3U/Kh8zymtdjbecxPc24/ymKPU8NSu8pEsiVThifPkukvpsP6cGg==","_resolved":"/private/var/folders/yg/cfczgtx54bzfh74lx2_mv0z80000gp/T/4fa5194f67207c39679a030f26c4207e/adhd-sox-install-engine-0.4.0.tgz","_from":"file:adhd-sox-install-engine-0.4.0.tgz","_nodeVersion":"24.11.1","_npmVersion":"11.6.2","dist":{"integrity":"sha512-eIwc353Tjez/+XbzvC2c5xoUx5WqT9QPJs3U/Kh8zymtdjbecxPc24/ymKPU8NSu8pEsiVThifPkukvpsP6cGg==","shasum":"e91d0d6ca2ac036c1c78a7445c88a317ef952790","tarball":"https://registry.npmjs.org/@adhd/sox-install-engine/-/sox-install-engine-0.4.0.tgz","fileCount":92,"unpackedSize":621940,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCmS5aCOZ2zhFkXgFQWT+lL64dvyuDT8FGqnIoS9n+FwQIhAPp/4Tu9kNQFGhnYzUg9poTFXL0tBYTDG+551rLA94Pt"}]},"_npmUser":{"name":"pseudosky","email":"skywinston.sk@gmail.com"},"directories":{},"maintainers":[{"name":"pseudosky","email":"skywinston.sk@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/sox-install-engine_0.4.0_1788566485524_0.7990705171878694"},"_hasShrinkwrap":false}},"time":{"created":"2026-06-26T05:19:45.368Z","modified":"2026-09-05T00:01:25.844Z","0.2.0":"2026-06-26T05:19:45.731Z","0.3.0":"2026-08-07T23:18:58.518Z","0.3.1":"2026-08-19T04:17:28.078Z","0.4.0":"2026-09-05T00:01:25.671Z"},"license":"MIT","description":"Sox install engine — parseArgs, install, cascade, build-index, provider-capabilities","maintainers":[{"name":"pseudosky","email":"skywinston.sk@gmail.com"}],"readme":"# @adhd/sox-install-engine\n\nThe pure, CLI-free engine behind `soxe install` / `update` / `uninstall`:\nscope-cascade config resolution, content-addressed integrity checking, registry\nindex building, ledger-driven install/update/uninstall with reversible\nownership tracking, provider capability checks, and the Claude Code\nproject/trust MCP-sync fixes. No `@nx/devkit` import and no `process.exit` —\nevery function here is a pure or side-effect-scoped operation a host (a CLI, a\nserver, a test) drives directly.\n\n```bash\npnpm add @adhd/sox-install-engine\n```\n\n## Quick start\n\n`install()` resolves a scope's `extensions.json` against a registry index,\nfetches/verifies the artifact for anything newly requested, and writes an\natomic, content-addressed lockfile. `verifyIntegrity()` is the read-only\ncounterpart: is what's on disk still what the lockfile says it is?\n\n```typescript\nimport * as fs from 'node:fs';\nimport * as os from 'node:os';\nimport * as path from 'node:path';\nimport * as crypto from 'node:crypto';\nimport { install, loadLockfile, verifyIntegrity, type Scope } from '@adhd/sox-install-engine';\n\nfunction sha256File(p: string): string {\n  return 'sha256:' + crypto.createHash('sha256').update(fs.readFileSync(p)).digest('hex');\n}\n\n// A workspace containing one built extension plus a registry index pointing at it.\nconst root = fs.mkdtempSync(path.join(os.tmpdir(), 'sox-demo-'));\nconst extDir = path.join(root, 'extensions', 'skills', 'hello');\nfs.mkdirSync(path.join(extDir, 'dist'), { recursive: true });\nconst artifactPath = path.join(extDir, 'dist', 'index.js');\nfs.writeFileSync(artifactPath, 'module.exports = { run: () => \"hi\" };\\n');\nfs.writeFileSync(\n  path.join(extDir, 'extension.json'),\n  JSON.stringify({\n    id: 'hello', type: 'skill', title: 'Hello', description: 'demo skill',\n    compatibility: { host: '>=1.0.0' }, license: 'MIT', entrypoint: 'dist/index.js',\n  }, null, 2),\n);\n\nconst checksum = sha256File(artifactPath);\nfs.mkdirSync(path.join(root, 'registry'), { recursive: true });\nfs.writeFileSync(\n  path.join(root, 'registry', 'index.json'),\n  JSON.stringify([{\n    id: 'hello', type: 'skill', title: 'Hello', description: 'demo skill',\n    source: `file://${extDir}`, checksum, compatibility: { host: '>=1.0.0' },\n  }]),\n);\n\n// A scope config declaring what's wanted, and where the lockfile lives.\nconst configPath = path.join(root, 'extensions.json');\nconst lockfilePath = path.join(root, 'extensions.lock');\nfs.writeFileSync(configPath, JSON.stringify({ install: [{ id: 'hello', enabled: true }] }));\n\nconst scope: Scope = 'project';\nawait install({ scope, mode: 'default', configPath, lockfilePath, root });\n\nconst lockfile = loadLockfile(lockfilePath);\nconsole.log(lockfile?.resolved['hello']?.checksum === checksum); // true\n\nconst status = await verifyIntegrity(scope, 'hello', { lockfilePath });\nconsole.log(status.status); // 'current'\n\n// Mutate the built artifact without re-installing -> integrity now reports 'stale'.\nfs.writeFileSync(artifactPath, 'module.exports = { run: () => \"changed\" };\\n');\nconsole.log((await verifyIntegrity(scope, 'hello', { lockfilePath })).status); // 'stale'\n```\n\n## API reference\n\n### Scope cascade (pure config merge)\n\n```typescript\n// Exported as CascadeScopeConfig (the bare `ScopeConfig` name is install()'s own,\n// structurally identical, scope-config type — see \"Install / lockfile\" below).\nfunction cascade(scopes: CascadeScopeConfig[]): ResolvedConfigMap;\nfunction deepMerge(base: Record<string, unknown>, override: Record<string, unknown>): Record<string, unknown>;\n\ninterface CascadeScopeConfig {\n  extends?: string;\n  strict_capabilities?: boolean;\n  providers?: Record<string, { base_url?: string; api_key?: string }>;\n  install?: Array<{ id: string; version?: string; enabled?: boolean; source?: string }>;\n  config?: Record<string, Record<string, unknown>>;\n  enabled?: Record<string, boolean>;\n  private?: boolean;\n}\n\ninterface ResolvedConfigEntry {\n  version: string | undefined;\n  enabled: boolean;\n  config: Record<string, unknown>;\n  configOnly?: boolean; // true when only config/enabled blocks named this id, with no install: directive anywhere\n}\n\ntype ResolvedConfigMap = Record<string, ResolvedConfigEntry>;\n```\n\n`cascade` merges an ordered widest→narrowest list of scope configs (`[org, user,\nproject, local]`) into one flat resolved map. It is a pure function with three\nfixed merge rules: primitives and **arrays are replaced entirely** by the\nnarrower scope (never concatenated), plain objects deep-merge key by key, and\n`enabled: false` at a narrower scope always suppresses a wider `true`.\n\n```typescript\nimport { cascade } from '@adhd/sox-install-engine';\n\nconst resolved = cascade([\n  { install: [{ id: 'formatter', version: '1.0.0' }] },        // org\n  { config: { formatter: { style: 'compact' } } },              // user\n  { install: [{ id: 'formatter', enabled: false }] },           // project — suppresses org's install\n]);\n// resolved.formatter -> { version: '1.0.0', enabled: false, config: { style: 'compact' } }\n```\n\n### Install / lockfile\n\n```typescript\ntype Scope = 'org' | 'user' | 'project' | 'local';\nconst SCOPES: Scope[];\ntype InstallMode = 'default' | 'frozen' | 'update';\nconst LOCKFILE_VERSION: 2;\n\nfunction getScopePath(scope: Scope): { config: string; lockfile: string };\nfunction loadConfig(configPath: string): ScopeConfig | null;\nfunction loadLockfile(lockPath: string): Lockfile | null;\nfunction normalizeLockfile(lock: Lockfile): Lockfile; // upgrades a legacy v1 (`id@version`-keyed) lockfile in memory\nfunction resolveEnvRef(value: string): string;\nfunction loadRegistryIndex(root: string): IndexEntry[];\nfunction resolveFromRegistry(id: string, index: IndexEntry[]): IndexEntry | null;\nfunction fetchArtifact(source: string, expectedChecksum?: string, opts?: { storeDir?: string }): Promise<{ bytes: Buffer; checksum: string; source: string }>;\nfunction writeLockfileAtomic(lockPath: string, lockfile: Lockfile): void; // temp-file + rename; rejects a zero-entry lockfile\nfunction findLocalExtension(root: string, id: string): string | null;\nfunction loadExtensionManifest(root: string, id: string): ExtensionManifest | null;\nfunction install(opts: InstallOptions): Promise<ResolvedSet>;\n\ninterface InstallOptions {\n  scope: Scope;\n  mode: InstallMode;\n  configPath?: string;\n  lockfilePath?: string;\n  root?: string;\n  overrideProvider?: string;\n  registryIndex?: IndexEntry[]; // inject an already-resolved index (e.g. a bundled CLI with no repo checkout under root)\n  onMissingConfig?: (extId: string, key: string, prompt: string, defaultVal: unknown) => Promise<string | undefined>;\n}\n\ninterface Lockfile {\n  lockfileVersion: 1 | 2;\n  extends?: LockfileExtendsPin;\n  resolved: Record<string, LockfileEntry>; // keyed by bare id (v2) — see normalizeLockfile for v1 back-compat\n}\n\ninterface LockfileEntry {\n  source: string;\n  checksum: string;\n  resolved_at: string;\n  bundle_id?: string;\n}\n\ntype ResolvedSet = Record<string, ResolvedEntry>;\ninterface ResolvedEntry {\n  version: string;\n  enabled: boolean;\n  config: Record<string, unknown>;\n  source: string;\n  checksum: string;\n}\n```\n\nIdentity is content-addressed (`id` + `checksum` of the built artifact) — there\nis no version-range resolution; `resolveFromRegistry` is a plain `id` lookup\nover the one registry build. `mode: 'frozen'` verifies every resolved entry's\nchecksum against the existing lockfile and fails on drift instead of\nre-resolving.\n\n### Declarative install (descriptor-driven placement)\n\n```typescript\nfunction declarativeInstall(\n  descriptor: InstallDescriptor,\n  scope: 'project' | 'user' | 'local' | 'org',\n  workspaceRoot: string,\n  scopeRoot: string,\n  opts?: { isProject?: boolean; ledger?: Ledger; dryRun?: boolean; force?: boolean },\n): Promise<DeclarativeInstallResult[]>;\n// `Ledger` (here and in LifecycleCtx/diff below) is an internal test-injection\n// type, not part of this package's public export surface — omit `ledger` in\n// normal use and the engine resolves its own from `scopeRoot`.\n\nclass DeclarativeDeniedError extends Error {\n  readonly reason: string;\n  readonly ext: string;\n  readonly host: string;\n  readonly scope: string;\n}\n\ninterface InstallDescriptor {\n  ext: string;\n  type: string;\n  hosts: string[];\n  bundleId?: string;\n  srcPath?: string;               // file-drop source\n  configKeyPath?: string;          // config-merge target key path\n  configValue?: unknown;\n  configValues?: string[];         // array-merge values\n  configEntries?: Record<string, unknown>[]; // object-array-merge entries\n  configIdentityField?: string;\n  configIdentityValue?: string;\n  transport?: 'stdio' | 'sse' | 'http';\n  profile?: string;\n  resolvedConfig?: Record<string, unknown>;\n}\n\ninterface DeclarativeInstallResult {\n  host: string;\n  scope: string;\n  capability: string;\n  target: string;\n  applied: boolean;\n  denied?: boolean;\n  denialReason?: string;\n  hints?: string[];\n  dryRun?: boolean; // true = plan only, nothing written\n}\n```\n\n`declarativeInstall` resolves the host-specific discovery path from the\nmanifest's `install` block, runs a policy check (a `stdio` mcp-server can never\nbe declared directly in `.mcp.json` — it is denied with\n`DeclarativeDeniedError`), applies the capability, and records the result in\nthe scope's ledger so it can be reversed later.\n\n### Integrity (the \"is this current?\" primitive)\n\n```typescript\ntype IntegrityStatus = 'current' | 'stale' | 'not-installed' | 'unresolvable';\n\ninterface IntegrityResult {\n  id: string;\n  status: IntegrityStatus;\n  current: boolean;       // status === 'current'\n  expected: string | null; // checksum recorded in the lockfile\n  actual: string | null;   // freshly-computed sha256 of the artifact on disk\n  source: string | null;\n  error?: string;\n}\n\nfunction verifyIntegrity(scope: Scope, id: string, opts?: { lockfilePath?: string }): Promise<IntegrityResult>;\n```\n\nPure read-only check: hash the artifact at the lockfile's recorded `source` and\ncompare it to the recorded `checksum`. No version comparison anywhere — this is\nthe single primitive `install(mode: 'frozen')`, `update`, and an `upgrade --all`\nCLI all call rather than re-implementing the comparison themselves.\n\n### Registry index building\n\n```typescript\n// buildIndex's own entry type is exported as BuildIndexEntry (the bare `IndexEntry`\n// name belongs to install()'s registry-index type — see \"Install / lockfile\" above;\n// the two shapes are structurally close but declared separately).\nfunction buildIndex(opts: { root: string }): BuildIndexEntry[];\nfunction checksumUrl(url: string): Promise<string>;\n\ninterface BuildIndexEntry {\n  id: string;\n  type: string;\n  version?: string; // display-only, never an identity input\n  title: string;\n  description: string;\n  source: string;\n  checksum: string;\n  compatibility: { host: string };\n  requires?: { tool_calling?: boolean; structured_output?: boolean; min_context_tokens?: number };\n  members?: Array<{ id: string }>;\n}\n```\n\nWalks a workspace's extension manifests and produces the registry index\n`resolveFromRegistry`/`loadRegistryIndex` consume — the `checksum` for each\nentry is the sha256 of its built entrypoint, computed at index-build time.\n\n### Provider capability checks\n\n```typescript\nfunction checkProviderCapabilities(model: string, requires: RequiresBlock): CapabilityResult;\nfunction loadCapabilityTable(): Record<string, ModelCapabilityEntry>;\n\ninterface RequiresBlock {\n  tool_calling?: boolean;\n  structured_output?: boolean;\n  min_context_tokens?: number;\n}\n\ninterface CapabilityResult {\n  ok: boolean;\n  warnings: string[];\n}\n\ninterface ModelCapabilityEntry {\n  supports_function_calling?: boolean;\n  supports_tool_choice?: boolean;\n  supports_response_schema?: boolean;\n  max_input_tokens?: number;\n  max_tokens?: number;\n  tool_calling?: boolean;\n  function_calling?: boolean;\n  structured_output?: boolean;\n}\n```\n\nA pure lookup against a vendored model-capability table: given a model name and\nan extension's `requires` block, reports whether the model can actually run\nit — e.g. installing a tool-calling agent against a model with no function\ncalling support produces a warning here rather than a confusing runtime\nfailure.\n\n```typescript\nimport { checkProviderCapabilities } from '@adhd/sox-install-engine';\n\nconst result = checkProviderCapabilities('gpt-4o-mini', { tool_calling: true, min_context_tokens: 128000 });\nif (!result.ok) console.warn(result.warnings.join('\\n'));\n```\n\n### Update / uninstall lifecycle\n\n```typescript\nfunction uninstall(ctx: LifecycleCtx): Promise<void>;\nfunction update(ctx: UpdateCtx): Promise<UpdateResult>;\n\nclass ReverseAbortError extends Error {\n  readonly capability: string;\n  readonly file: string;\n  readonly reason: string;\n}\n\n// Exported as LifecycleHostScope (the bare `HostScope` name is not exported here).\ntype LifecycleHostScope = 'project' | 'user' | 'local' | 'org';\n\ninterface LifecycleCtx {\n  ext: string;\n  host: string;\n  scope: LifecycleHostScope;\n  scopeRoot: string;\n  isProject?: boolean;\n  ledger?: Ledger;\n}\n\ninterface UpdateCtx extends LifecycleCtx {\n  workspaceRoot: string;\n  newSrcPath?: string;\n  newPayload?: { keyPath: string; value: unknown };\n}\n\ninterface UpdateResult {\n  kind: 'none' | 'updated' | 'added';\n  actions: string[];\n}\n```\n\n`uninstall` reverses every ledger-recorded action for `(ext, host, scope)` —\ndeleting file-drops, removing config-merge keys, removing array-merge\nvalues — touching only sox-owned entries and leaving foreign content byte-clean.\nA capability that cannot cleanly reverse throws `ReverseAbortError` rather than\nleaving a partial, silently-broken uninstall.\n\n### Ownership index\n\n```typescript\nclass OwnershipIndex {\n  static loadFromFile(filePath: string, opts?: { strict?: boolean }): OwnershipIndex;\n  static load(scope: DataScope, root?: string, opts?: { strict?: boolean }): OwnershipIndex;\n  get path(): string;\n  get(extId: string, scope: string): OwnershipRecord | undefined;\n  all(): OwnershipRecord[];\n  record(opts: { extId: string; scope: string; host?: string; bundleId?: string; artifactChecksum?: string; entries: OwnedEntry[] }): void;\n  static dedupeEntries(entries: OwnedEntry[]): OwnedEntry[];\n  compact(): void;\n  addEntries(extId: string, scope: string, entries: OwnedEntry[], meta?: { host?: string; bundleId?: string; artifactChecksum?: string }): void;\n  remove(extId: string, scope: string): void;\n  save(): void;\n  static upsertOsUnitEntry(filePath: string, opts: { extId: string; scope: string; label: string; unitPath: string; supervisor: 'launchd' | 'systemd'; appliedHash: string }): void;\n  static removeOsUnitEntry(filePath: string, opts: { extId: string; scope: string; label: string }): boolean;\n}\n\nfunction readOwnership(filePath: string, opts?: { strict?: boolean }): OwnershipFile;\nfunction writeOwnershipAtomic(filePath: string, data: OwnershipFile): void;\nfunction supersededEntries(oldEntries: OwnedEntry[], newEntries: OwnedEntry[]): OwnedEntry[];\n\nclass OwnershipCorruptError extends Error { readonly filePath: string; }\nclass OwnershipConflictError extends Error { readonly filePath: string; }\n\ninterface OwnershipRecord {\n  extId: string;\n  scope: string;\n  host?: string;\n  bundleId?: string;\n  artifactChecksum?: string;\n  installedAt: string;\n  updatedAt: string;\n  entries: OwnedEntry[];\n}\n\ninterface OwnershipFile {\n  version: 1;\n  owned: OwnershipRecord[];\n}\n\n// OwnedEntry: a discriminated union on `kind` — one variant per reversible action:\ntype OwnedEntry =\n  | { kind: 'file-drop'; path: string }\n  | { kind: 'materialize'; path: string }\n  | { kind: 'config-key'; file: string; keyPath: string; appliedHash?: string }\n  | { kind: 'array-values'; file: string; keyPath: string; values: string[] }\n  | { kind: 'object-array-values'; file: string; keyPath: string; entries: Array<Record<string, unknown>>; identityField: string; identityValue: string }\n  | { kind: 'lockfile-key'; file: string; keyPath: string }\n  | { kind: 'registry-record'; extId: string; scope: string; root: string }\n  | { kind: 'os-unit'; label: string; unitPath: string; supervisor: 'launchd' | 'systemd'; appliedHash: string };\n```\n\nThe complete, machine-local record of every filesystem location and config key\nan install owns, keyed by `(extId, scope)`. `save()` is optimistic-concurrency\nguarded: it re-stats the file before renaming and throws\n`OwnershipConflictError` if another process wrote it out from under you,\nrather than silently clobbering that write. A structurally invalid file under\n`strict: true` throws `OwnershipCorruptError` instead of being read back as\nempty.\n\n### Claude Code project MCP sync\n\n```typescript\nfunction resolveUserMcpConfigPath(host: string): string | undefined;\nfunction resolveProjectMcpConfigPath(host: string, projectRoot: string): string | undefined;\nfunction readGlobalServerEntry(host: string, extId: string): unknown;\nfunction knownProjectRoots(): string[];\n\nfunction registerUserMcpServer(opts: {\n  extId: string;\n  serverEntry: unknown;\n  host?: string;\n  scopeRoot?: string;\n  workspaceRoot?: string;\n}): Promise<'registered' | 'no-surface'>;\n\nfunction syncUserMcpToProjects(opts: SyncMcpOptions): Promise<ProjectSyncResult[]>;\nfunction reverseUserMcpFromProjects(opts: { extId: string; host?: string }): Promise<string[]>;\n\ninterface SyncMcpOptions {\n  extId: string;\n  host?: string;          // default \"claude\"\n  serverEntry?: unknown;  // defaults to the current global registration if omitted\n  dryRun?: boolean;\n  onlyRoot?: string;\n}\n\ninterface ProjectSyncResult {\n  projectRoot: string;\n  mcpJsonPath: string;\n  action: 'merged' | 'up-to-date' | 'would-merge' | 'skipped';\n  reason?: string;\n}\n```\n\nFixes a real Claude Code gap: a project's `.mcp.json` *overrides* rather than\ninherits user-scope MCP servers, so a globally-registered server is invisible\nin any project with its own `.mcp.json`. `syncUserMcpToProjects` propagates a\nuser-scope server's entry into every project sox knows about (from the install\nregistry — never a filesystem scan), fully tracked in the ownership index so\n`reverseUserMcpFromProjects` can remove exactly what was added, leaving every\nother server byte-clean.\n\n### Claude Code trust sync\n\n```typescript\nfunction syncMcpTrustToProjects(opts: SyncTrustOptions): Promise<TrustSyncResult[]>;\nfunction reverseMcpTrustFromProjects(opts: { extId: string; host?: string }): Promise<string[]>;\n\ninterface SyncTrustOptions {\n  extId: string;\n  host?: string;   // default \"claude\"; no-op for any other host\n  dryRun?: boolean;\n  roots?: string[]; // defaults to every known project root\n}\n\ninterface TrustSyncResult {\n  projectRoot: string;\n  claudeJsonPath: string;\n  action: 'trusted' | 'up-to-date' | 'would-trust' | 'skipped';\n  reason?: string;\n}\n```\n\nClaude Code gates every `.mcp.json` remote MCP server behind a per-project\ntrust list (`~/.claude.json` → `projects[\"<root>\"].enabledMcpjsonServers`),\nnormally approved through an interactive prompt. A headless install has no one\nto answer that prompt, so a correctly-installed server silently serves zero\ntools. `syncMcpTrustToProjects` appends exactly the one extension id just\ninstalled to that list for the relevant project roots — never a blanket\ntrust-everything flag — and `reverseMcpTrustFromProjects` removes only what it\nadded.\n\n### Data-root path resolver\n\n```typescript\ntype DataScope = 'org' | 'user' | 'project' | 'local';\n\nfunction userDataRoot(): string;                                    // $SOX_ECOSYSTEM_HOME or ~/.adhd/sox-ecosystem/\nfunction dataRoot(scope: DataScope, root?: string): string;\nfunction scopeConfigPaths(scope: DataScope, root?: string): { config: string; lockfile: string };\nfunction ledgerPathFor(scope: DataScope, root?: string): string;\nfunction ownershipPathFor(scope: DataScope, root?: string): string;\nfunction storeRootFor(scope: DataScope, root?: string): string;     // <dataRoot>/ext/\nfunction installRegistryPath(): string;                              // $userDataRoot/install-registry.json\n```\n\n### Drift diff (ledger vs. disk)\n\n```typescript\ntype DiffKind = 'up-to-date' | 'drifted' | 'missing' | 'will-change';\n\nfunction diff(ext: string, host: string, scope: string, scopeRoot: string, opts?: { ledger?: Ledger; isProject?: boolean }): ExtensionDiff;\nfunction diffAll(scopeRoot: string, opts?: { ledger?: Ledger; isProject?: boolean }): ExtensionDiff[];\n\ninterface ActionDiff {\n  // one of the seven ledger capability identifiers (LedgerAction is internal, not exported)\n  cap: 'config-merge' | 'array-merge' | 'object-array-merge' | 'materialize' | 'file-drop' | 'bin-link' | 'run-service';\n  file: string;\n  keyPath: string;\n  kind: DiffKind;\n  currentHash?: string;\n  appliedHash?: string;\n}\n\ninterface ExtensionDiff {\n  ext: string;\n  host: string;\n  scope: string;\n  actions: ActionDiff[];\n  clean: boolean; // true iff every action is up-to-date\n}\n```\n\nCompares what the ledger says was placed against what is actually on disk for\neach action. An externally-edited file that the ledger placed reports\n`'drifted'` rather than silently being treated as fine — verification tops out\nat \"present and matching the recorded hash\"; it never asserts the host\nactually *ran* the content.\n\n### Global install registry\n\n```typescript\nfunction resolveInstallRegistryPath(): string;\nfunction readInstallRegistry(registryPath: string): InstallRegistry;\nfunction writeInstallRegistryAtomic(registryPath: string, registry: InstallRegistry): void;\nfunction upsertInstallRecord(opts: UpsertInstallRecordOpts): void;\nfunction removeInstallRecord(extId: string, scope: string, root: string): void;\n\ninterface InstallRecord {\n  extId: string;\n  version: string;\n  scope: 'user' | 'project' | 'local';\n  root: string;\n  installedAt: string;\n  updatedAt: string;\n  source: string;\n}\n\ninterface InstallRegistry {\n  version: 1;\n  installs: InstallRecord[];\n}\n\ninterface UpsertInstallRecordOpts {\n  extId: string;\n  version: string;\n  scope: string;\n  root: string;\n  source: string;\n}\n```\n\nThe single machine-wide record of every `soxe install` across every project —\nnatural key `(extId, scope, root)`. This is what `knownProjectRoots()` (above)\nreads to find every project sox should propagate a user-scope MCP server into,\nwithout ever scanning the filesystem. All writes here are best-effort; callers\nwrap them in `try/catch` so a registry write failure never fails the install\nitself.\n\n### CLI argument parsing\n\n```typescript\nfunction parseArgs(argv: string[]): Record<string, string>;\n```\n\nHandles both `--flag value` and `--flag=value` forms for every flag, plus bare\nboolean flags (`--flag` → `'true'`), the `-s value` short alias for `--scope`,\nand positional arguments (stored as `_`, `_2`, `_3`, ...).\n\n```typescript\nimport { parseArgs } from '@adhd/sox-install-engine';\n\nparseArgs(['hello', '--scope=project', '--dry-run']);\n// { _: 'hello', scope: 'project', 'dry-run': 'true' }\nparseArgs(['-s', 'user']);\n// { scope: 'user' }\n```\n\n## Invariants / gotchas\n\n- **Identity is content-addressed, not version-addressed.** `id` + the sha256\n  checksum of the built artifact is the only identity; there is no\n  version-range resolution anywhere in `install`/`verifyIntegrity`.\n- **Arrays replace entirely on cascade — never concatenate.** A narrower\n  scope's `install:`/array config value completely replaces a wider scope's,\n  by design (per the cascade contract).\n- **`ownership.json` saves are conflict-detected, not last-write-wins.**\n  `OwnershipIndex.save()` throws `OwnershipConflictError` rather than\n  silently overwriting a concurrent writer's update.\n- **Reversal that can't be done cleanly aborts.** `uninstall`/`update` throw\n  `ReverseAbortError` instead of leaving a partially-reversed, inconsistent\n  install on disk.\n- **MCP trust-sync is scoped to one named extension, never a blanket flag.**\n  `syncMcpTrustToProjects` only ever appends the specific `extId` just\n  installed — it is not an `enableAllProjectMcpServers`-style bypass.\n- **No filesystem scanning for \"known projects.\"** `knownProjectRoots()` and\n  every default-target resolution in the MCP sync functions read the install\n  registry — they never crawl disk for `.mcp.json` files.\n","readmeFilename":"README.md","homepage":"https://github.com/PseudoSky/adhd","keywords":["install","registry","mcp","typescript"],"repository":{"type":"git","url":"git+https://github.com/PseudoSky/adhd.git"},"bugs":{"url":"https://github.com/PseudoSky/adhd/issues"}}