oh-my-customcode 1.1.93 → 1.1.94

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/dist/cli/index.js CHANGED
@@ -217,7 +217,7 @@ var init_package = __esm(() => {
217
217
  workspaces: [
218
218
  "packages/*"
219
219
  ],
220
- version: "1.1.93",
220
+ version: "1.1.94",
221
221
  description: "Batteries-included agent harness for Claude Code",
222
222
  type: "module",
223
223
  bin: {
@@ -7353,6 +7353,19 @@ async function writeJsonFile(path, data) {
7353
7353
  const content = JSON.stringify(data, null, 2);
7354
7354
  await fs.writeFile(path, content, "utf-8");
7355
7355
  }
7356
+ function parseJsonText(raw) {
7357
+ const bom = raw.startsWith(UTF8_BOM);
7358
+ const text = bom ? raw.slice(UTF8_BOM.length) : raw;
7359
+ return {
7360
+ data: JSON.parse(text),
7361
+ format: { bom, trailingNewline: text.endsWith(`
7362
+ `) }
7363
+ };
7364
+ }
7365
+ function stringifyJson(data, format) {
7366
+ return `${format.bom ? UTF8_BOM : ""}${JSON.stringify(data, null, 2)}${format.trailingNewline ? `
7367
+ ` : ""}`;
7368
+ }
7356
7369
  async function readTextFile(path) {
7357
7370
  const fs = await import("node:fs/promises");
7358
7371
  return fs.readFile(path, "utf-8");
@@ -7408,6 +7421,7 @@ function matchesPattern(filename, pattern) {
7408
7421
  const regex = new RegExp(`^${regexPattern}$`);
7409
7422
  return regex.test(filename);
7410
7423
  }
7424
+ var UTF8_BOM = "\uFEFF";
7411
7425
  var init_fs = () => {};
7412
7426
 
7413
7427
  // src/cli/projects.ts
@@ -8220,7 +8234,7 @@ var init_theme = __esm(() => {
8220
8234
  });
8221
8235
 
8222
8236
  // node_modules/.bun/@inquirer+core@11.1.7+b219b5910764fa5c/node_modules/@inquirer/core/dist/lib/make-theme.js
8223
- function isPlainObject(value) {
8237
+ function isPlainObject2(value) {
8224
8238
  if (typeof value !== "object" || value === null)
8225
8239
  return false;
8226
8240
  let proto = value;
@@ -8234,7 +8248,7 @@ function deepMerge2(...objects) {
8234
8248
  for (const obj of objects) {
8235
8249
  for (const [key, value] of Object.entries(obj)) {
8236
8250
  const prevValue = output[key];
8237
- output[key] = isPlainObject(prevValue) && isPlainObject(value) ? deepMerge2(prevValue, value) : value;
8251
+ output[key] = isPlainObject2(prevValue) && isPlainObject2(value) ? deepMerge2(prevValue, value) : value;
8238
8252
  }
8239
8253
  }
8240
8254
  return output;
@@ -15616,7 +15630,7 @@ var MESSAGES = {
15616
15630
  "install.entry_md_skipped": "{{entry}} skipped ({{reason}})",
15617
15631
  "install.lockfile_generated": "Lockfile generated ({{files}} files tracked)",
15618
15632
  "install.lockfile_failed": "Failed to generate lockfile: {{error}}",
15619
- "install.hook_commands_migrated": "Migrated {{rewritten}} hook command(s) in settings.local.json",
15633
+ "install.hook_commands_migrated": "Migrated {{rewritten}} hook/statusLine command(s) in settings.local.json",
15620
15634
  "install.hook_commands_migration_failed": "Failed to migrate hook commands in settings.local.json: {{error}}",
15621
15635
  "lockfile.not_found": "Lockfile not found: {{path}}",
15622
15636
  "lockfile.invalid_version": "Invalid lockfile version: {{path}}",
@@ -15639,15 +15653,86 @@ var MESSAGES = {
15639
15653
  "update.lockfile_failed": "Failed to regenerate lockfile: {{error}}",
15640
15654
  "update.protected_file_updated": "⟳ Protected file {{file}} in {{component}} updated: {{hint}}",
15641
15655
  "update.namespace_synced": "Namespace synced: {{file}} ({{component}})",
15642
- "update.hook_commands_migrated": "Migrated {{count}} hook command(s) to CLAUDE_PROJECT_DIR-anchored paths",
15656
+ "update.hook_commands_migrated": "Migrated {{count}} hook/statusLine command(s) to CLAUDE_PROJECT_DIR-anchored paths",
15643
15657
  "update.hook_commands_migration_failed": "Failed to migrate hook commands in {{path}}",
15644
15658
  "config.load_failed": "Failed to load config: {{error}}",
15645
15659
  "config.not_found": "Config not found at {{path}}, using defaults",
15646
15660
  "config.saved": "Config saved to {{path}}",
15647
15661
  "config.deleted": "Config deleted from {{path}}",
15662
+ "config.invalid_preserve_path": "Ignored invalid preserve path: {{path}} ({{reason}})",
15663
+ "codex.already_installed": "Codex CLI is already installed",
15664
+ "codex.installing_brew": "Installing Codex CLI via Homebrew...",
15665
+ "codex.installing_npm": "Installing Codex CLI via npm...",
15666
+ "codex.install_failed": "Failed to install Codex CLI: {{error}}",
15667
+ "codex.unsupported_os": "Codex CLI auto-install is not supported on {{os}}",
15668
+ "rtk.already_installed": "RTK is already installed",
15669
+ "rtk.installing_brew": "Installing RTK via Homebrew...",
15670
+ "rtk.installing_curl": "Installing RTK via install script...",
15671
+ "rtk.install_failed": "Failed to install RTK: {{error}}",
15672
+ "rtk.unsupported_os": "RTK auto-install is not supported on {{os}}",
15673
+ "mcp.ontology_rag_configured": "ontology-rag MCP server configured successfully",
15674
+ "install.codex_installing": "Installing Codex CLI...",
15675
+ "install.codex_success": "Codex CLI installed successfully",
15676
+ "install.codex_already": "Codex CLI is already installed",
15677
+ "install.rtk_installing": "Installing RTK...",
15678
+ "install.rtk_success": "RTK installed successfully",
15679
+ "install.rtk_already": "RTK is already installed",
15680
+ "install.preserved": "Preserved {{files}} file(s) and {{dirs}} directory(ies) from the existing installation",
15681
+ "install.restored": "Restored {{files}} file(s) and {{dirs}} directory(ies) after installation",
15682
+ "install.backup_failed": "Failed to back up {{path}}: {{error}}",
15683
+ "install.hooks_settings_failed": "Failed to merge hooks into settings.local.json: {{error}}",
15684
+ "preserve.cleanup_failed": "Failed to clean up preservation directory {{dir}}: {{error}}",
15685
+ "preserve.extract_dir_failed": "Failed to preserve directory {{dir}}: {{error}}",
15686
+ "preserve.extract_failed": "Failed to preserve file {{file}}: {{error}}",
15687
+ "preserve.restore_dir_failed": "Failed to restore directory {{dir}}: {{error}}",
15688
+ "preserve.restore_failed": "Failed to restore file {{file}}: {{error}}",
15689
+ "preserve_files.invalid_path": "Ignored invalid preserve_files entry from {{source}}: {{path}} ({{reason}})",
15690
+ "update.codex_missing": "Codex CLI is not installed",
15691
+ "update.rtk_missing": "RTK is not installed",
15692
+ "update.self_update_skipped": "Skipped update: source project cannot update itself",
15693
+ "update.deprecated_file_invalid_path": "Skipped deprecated file with invalid path: {{path}} ({{reason}})",
15694
+ "update.deprecated_file_removed": "Removed deprecated file: {{path}} ({{reason}})",
15695
+ "update.entry_doc_created": "Entry document created: {{path}}",
15696
+ "update.entry_doc_force_updated": "Entry document force-updated: {{path}}",
15697
+ "update.entry_merge_warning": "Entry document merge warning: {{warning}}",
15698
+ "update.entry_template_not_found": "Entry document template not found: {{template}}",
15699
+ "update.protected_file_force_overwrite": "Protected file {{file}} in {{component}} overwritten: {{hint}}",
15700
+ "update.protected_file_skipped": "Protected file {{file}} in {{component}} skipped: {{hint}}",
15701
+ "update.settings_local_backfill_failed": "Failed to backfill statusLine refreshInterval in {{path}}",
15648
15702
  "general.done": "Done!",
15649
15703
  "general.failed": "Failed",
15650
- "general.skipped": "Skipped"
15704
+ "general.skipped": "Skipped",
15705
+ "install.agent_domain_excluded": "Excluded agent {{agent}} (domain: {{domain}})",
15706
+ "install.backed_up": "Backed up {{from}} to {{to}}",
15707
+ "install.hooks_settings_merged": "Merged hooks wiring into settings.local.json",
15708
+ "install.hooks_settings_skipped": "Skipped hooks wiring in settings.local.json: {{reason}}",
15709
+ "install.schemas_not_found": "schemas/tool-inputs.json template not found: {{path}}",
15710
+ "install.schemas_skipped": "Skipped schemas/tool-inputs.json: {{reason}}",
15711
+ "install.schemas_installed": "Installed schemas/tool-inputs.json",
15712
+ "install.settings_local_created": "Created settings.local.json",
15713
+ "install.settings_local_merged": "Merged statusLine into settings.local.json",
15714
+ "install.settings_local_refreshInterval_added": "Added statusLine refreshInterval to settings.local.json",
15715
+ "install.settings_local_skipped": "Skipped settings.local.json statusLine: {{reason}}",
15716
+ "install.settings_local_statusline_migrated": "Re-anchored default statusLine command in settings.local.json",
15717
+ "install.skill_scope_excluded": "Excluded skill {{skill}} (scope: {{scope}})",
15718
+ "install.statusline_installed": "Installed statusline.sh",
15719
+ "install.statusline_not_found": "statusline.sh template not found: {{path}}",
15720
+ "install.statusline_skipped": "Skipped statusline.sh: {{reason}}",
15721
+ "install.tests_config_installed": "Installed tests/tsconfig.json",
15722
+ "install.tests_config_not_found": "tests/tsconfig.json template not found: {{path}}",
15723
+ "install.tests_config_skipped": "Skipped tests/tsconfig.json: {{reason}}",
15724
+ "preserve.cleanup": "Cleaned up preservation directory: {{dir}}",
15725
+ "preserve.copied_json": "Copied JSON file: {{file}}",
15726
+ "preserve.extracted_dir": "Preserved directory: {{dir}}",
15727
+ "preserve.extracted_file": "Preserved file: {{file}}",
15728
+ "preserve.merged_json": "Merged JSON file: {{file}}",
15729
+ "preserve.restored_dir": "Restored directory: {{dir}}",
15730
+ "preserve.restored_file": "Restored file: {{file}}",
15731
+ "update.deprecated_files_cleaned": "Removed {{count}} deprecated file(s)",
15732
+ "update.entry_doc_merged": "Entry document merged: {{path}} ({{managed}} managed, {{custom}} custom sections)",
15733
+ "update.file_backed_up": "Backed up {{path}} to {{backup}}",
15734
+ "update.root_files_synced": "Root-level files synced: {{files}}",
15735
+ "update.settings_local_refreshInterval_backfilled": "Backfilled statusLine refreshInterval in settings.local.json"
15651
15736
  },
15652
15737
  ko: {
15653
15738
  "install.start": "oh-my-customcode 초기화 중...",
@@ -15666,7 +15751,7 @@ var MESSAGES = {
15666
15751
  "install.entry_md_skipped": "{{entry}} 건너뜀 ({{reason}})",
15667
15752
  "install.lockfile_generated": "잠금 파일 생성 완료 ({{files}}개 파일 추적)",
15668
15753
  "install.lockfile_failed": "잠금 파일 생성 실패: {{error}}",
15669
- "install.hook_commands_migrated": "settings.local.json의 훅 명령 {{rewritten}}개를 마이그레이션했습니다",
15754
+ "install.hook_commands_migrated": "settings.local.json의 훅/상태줄 명령 {{rewritten}}개를 마이그레이션했습니다",
15670
15755
  "install.hook_commands_migration_failed": "settings.local.json의 훅 명령 마이그레이션에 실패했습니다: {{error}}",
15671
15756
  "lockfile.not_found": "잠금 파일 없음: {{path}}",
15672
15757
  "lockfile.invalid_version": "잠금 파일 버전 유효하지 않음: {{path}}",
@@ -15689,15 +15774,86 @@ var MESSAGES = {
15689
15774
  "update.lockfile_failed": "잠금 파일 재생성 실패: {{error}}",
15690
15775
  "update.protected_file_updated": "⟳ 보호 파일 {{file}} ({{component}}) 업데이트됨: {{hint}}",
15691
15776
  "update.namespace_synced": "네임스페이스 동기화: {{file}} ({{component}})",
15692
- "update.hook_commands_migrated": "CLAUDE_PROJECT_DIR 기준 경로로 훅 명령 {{count}}개를 마이그레이션했습니다",
15777
+ "update.hook_commands_migrated": "CLAUDE_PROJECT_DIR 기준 경로로 훅/상태줄 명령 {{count}}개를 마이그레이션했습니다",
15693
15778
  "update.hook_commands_migration_failed": "{{path}}의 훅 명령 마이그레이션에 실패했습니다",
15694
15779
  "config.load_failed": "설정 로드 실패: {{error}}",
15695
15780
  "config.not_found": "{{path}}에 설정 없음, 기본값 사용",
15696
15781
  "config.saved": "설정 저장: {{path}}",
15697
15782
  "config.deleted": "설정 삭제: {{path}}",
15783
+ "config.invalid_preserve_path": "유효하지 않은 보존 경로를 무시했습니다: {{path}} ({{reason}})",
15784
+ "codex.already_installed": "Codex CLI가 이미 설치되어 있습니다",
15785
+ "codex.installing_brew": "Homebrew로 Codex CLI를 설치합니다...",
15786
+ "codex.installing_npm": "npm으로 Codex CLI를 설치합니다...",
15787
+ "codex.install_failed": "Codex CLI 설치에 실패했습니다: {{error}}",
15788
+ "codex.unsupported_os": "{{os}}에서는 Codex CLI 자동 설치를 지원하지 않습니다",
15789
+ "rtk.already_installed": "RTK가 이미 설치되어 있습니다",
15790
+ "rtk.installing_brew": "Homebrew로 RTK를 설치합니다...",
15791
+ "rtk.installing_curl": "설치 스크립트로 RTK를 설치합니다...",
15792
+ "rtk.install_failed": "RTK 설치에 실패했습니다: {{error}}",
15793
+ "rtk.unsupported_os": "{{os}}에서는 RTK 자동 설치를 지원하지 않습니다",
15794
+ "mcp.ontology_rag_configured": "ontology-rag MCP 서버 구성을 완료했습니다",
15795
+ "install.codex_installing": "Codex CLI를 설치합니다...",
15796
+ "install.codex_success": "Codex CLI를 설치했습니다",
15797
+ "install.codex_already": "Codex CLI가 이미 설치되어 있습니다",
15798
+ "install.rtk_installing": "RTK를 설치합니다...",
15799
+ "install.rtk_success": "RTK를 설치했습니다",
15800
+ "install.rtk_already": "RTK가 이미 설치되어 있습니다",
15801
+ "install.preserved": "기존 설치에서 파일 {{files}}개와 디렉토리 {{dirs}}개를 보존했습니다",
15802
+ "install.restored": "설치 후 파일 {{files}}개와 디렉토리 {{dirs}}개를 복원했습니다",
15803
+ "install.backup_failed": "{{path}} 백업에 실패했습니다: {{error}}",
15804
+ "install.hooks_settings_failed": "settings.local.json에 훅을 병합하지 못했습니다: {{error}}",
15805
+ "preserve.cleanup_failed": "보존 디렉토리 {{dir}} 정리에 실패했습니다: {{error}}",
15806
+ "preserve.extract_dir_failed": "디렉토리 {{dir}} 보존에 실패했습니다: {{error}}",
15807
+ "preserve.extract_failed": "파일 {{file}} 보존에 실패했습니다: {{error}}",
15808
+ "preserve.restore_dir_failed": "디렉토리 {{dir}} 복원에 실패했습니다: {{error}}",
15809
+ "preserve.restore_failed": "파일 {{file}} 복원에 실패했습니다: {{error}}",
15810
+ "preserve_files.invalid_path": "{{source}}의 유효하지 않은 preserve_files 항목을 무시했습니다: {{path}} ({{reason}})",
15811
+ "update.codex_missing": "Codex CLI가 설치되어 있지 않습니다",
15812
+ "update.rtk_missing": "RTK가 설치되어 있지 않습니다",
15813
+ "update.self_update_skipped": "업데이트를 건너뜁니다: 소스 프로젝트는 스스로 업데이트할 수 없습니다",
15814
+ "update.deprecated_file_invalid_path": "경로가 유효하지 않아 지원 중단 파일을 건너뜁니다: {{path}} ({{reason}})",
15815
+ "update.deprecated_file_removed": "지원 중단 파일을 삭제했습니다: {{path}} ({{reason}})",
15816
+ "update.entry_doc_created": "진입 문서를 생성했습니다: {{path}}",
15817
+ "update.entry_doc_force_updated": "진입 문서를 강제로 업데이트했습니다: {{path}}",
15818
+ "update.entry_merge_warning": "진입 문서 병합 경고: {{warning}}",
15819
+ "update.entry_template_not_found": "진입 문서 템플릿을 찾을 수 없습니다: {{template}}",
15820
+ "update.protected_file_force_overwrite": "{{component}}의 보호 파일 {{file}}을(를) 덮어썼습니다: {{hint}}",
15821
+ "update.protected_file_skipped": "{{component}}의 보호 파일 {{file}}을(를) 건너뜁니다: {{hint}}",
15822
+ "update.settings_local_backfill_failed": "{{path}}의 statusLine refreshInterval 보충에 실패했습니다",
15698
15823
  "general.done": "완료!",
15699
15824
  "general.failed": "실패",
15700
- "general.skipped": "건너뜀"
15825
+ "general.skipped": "건너뜀",
15826
+ "install.agent_domain_excluded": "에이전트 {{agent}}을(를) 제외했습니다 (도메인: {{domain}})",
15827
+ "install.backed_up": "{{from}}을(를) {{to}}(으)로 백업했습니다",
15828
+ "install.hooks_settings_merged": "settings.local.json에 훅 연결을 병합했습니다",
15829
+ "install.hooks_settings_skipped": "settings.local.json의 훅 연결을 건너뜁니다: {{reason}}",
15830
+ "install.schemas_not_found": "schemas/tool-inputs.json 템플릿을 찾을 수 없습니다: {{path}}",
15831
+ "install.schemas_skipped": "schemas/tool-inputs.json 설치를 건너뜁니다: {{reason}}",
15832
+ "install.schemas_installed": "schemas/tool-inputs.json을 설치했습니다",
15833
+ "install.settings_local_created": "settings.local.json을 생성했습니다",
15834
+ "install.settings_local_merged": "settings.local.json에 statusLine을 병합했습니다",
15835
+ "install.settings_local_refreshInterval_added": "settings.local.json에 statusLine refreshInterval을 추가했습니다",
15836
+ "install.settings_local_skipped": "settings.local.json의 statusLine을 건너뜁니다: {{reason}}",
15837
+ "install.settings_local_statusline_migrated": "settings.local.json의 기본 statusLine 명령을 프로젝트 루트 기준으로 변경했습니다",
15838
+ "install.skill_scope_excluded": "스킬 {{skill}}을(를) 제외했습니다 (범위: {{scope}})",
15839
+ "install.statusline_installed": "statusline.sh를 설치했습니다",
15840
+ "install.statusline_not_found": "statusline.sh 템플릿을 찾을 수 없습니다: {{path}}",
15841
+ "install.statusline_skipped": "statusline.sh 설치를 건너뜁니다: {{reason}}",
15842
+ "install.tests_config_installed": "tests/tsconfig.json을 설치했습니다",
15843
+ "install.tests_config_not_found": "tests/tsconfig.json 템플릿을 찾을 수 없습니다: {{path}}",
15844
+ "install.tests_config_skipped": "tests/tsconfig.json 설치를 건너뜁니다: {{reason}}",
15845
+ "preserve.cleanup": "보존 디렉토리를 정리했습니다: {{dir}}",
15846
+ "preserve.copied_json": "JSON 파일을 복사했습니다: {{file}}",
15847
+ "preserve.extracted_dir": "디렉토리를 보존했습니다: {{dir}}",
15848
+ "preserve.extracted_file": "파일을 보존했습니다: {{file}}",
15849
+ "preserve.merged_json": "JSON 파일을 병합했습니다: {{file}}",
15850
+ "preserve.restored_dir": "디렉토리를 복원했습니다: {{dir}}",
15851
+ "preserve.restored_file": "파일을 복원했습니다: {{file}}",
15852
+ "update.deprecated_files_cleaned": "지원 중단 파일 {{count}}개를 삭제했습니다",
15853
+ "update.entry_doc_merged": "진입 문서를 병합했습니다: {{path}} (관리 섹션 {{managed}}개, 사용자 섹션 {{custom}}개)",
15854
+ "update.file_backed_up": "{{path}}을(를) {{backup}}(으)로 백업했습니다",
15855
+ "update.root_files_synced": "루트 수준 파일을 동기화했습니다: {{files}}",
15856
+ "update.settings_local_refreshInterval_backfilled": "settings.local.json의 statusLine refreshInterval을 보충했습니다"
15701
15857
  }
15702
15858
  };
15703
15859
  function getMessage(key, params) {
@@ -16961,11 +17117,122 @@ import {
16961
17117
  rename,
16962
17118
  stat as stat2
16963
17119
  } from "node:fs/promises";
16964
- import { basename as basename3, join as join8 } from "node:path";
17120
+ import { basename as basename3, dirname as dirname4, join as join8 } from "node:path";
16965
17121
 
16966
17122
  // src/core/file-preservation.ts
16967
17123
  init_fs();
16968
17124
  import { basename as basename2, join as join7 } from "node:path";
17125
+
17126
+ // src/core/hook-group-merge.ts
17127
+ var LEGACY_OMCUSTOM_DESCRIPTIONS = [
17128
+ "Record agent outcomes + auto-continue workflow + R007/R008 drift advisory on subagent completion (autonomous-loop re-entry, #1545)",
17129
+ 'Re-inject project CLAUDE.md into model context on session start/resume/clear and compact re-entry — matcher "*" covers all SessionStart sources including "compact" (#1617)'
17130
+ ];
17131
+ var STANDALONE_SCRIPT_COMMAND = /^(?:bash )?(?:"(?:\$\{CLAUDE_PROJECT_DIR:-\.\}\/|\.\/)?\.claude\/hooks\/((?:scripts\/)?[A-Za-z0-9._-]+\.sh)"|(?:\.\/)?\.claude\/hooks\/((?:scripts\/)?[A-Za-z0-9._-]+\.sh))$/;
17132
+ var SCRIPT_REFERENCE = /\.claude\/hooks\/((?:scripts\/)?[A-Za-z0-9._-]+\.sh)/g;
17133
+ function matcherKey(group) {
17134
+ return typeof group.matcher === "string" ? group.matcher : "";
17135
+ }
17136
+ function isRecord(value) {
17137
+ return typeof value === "object" && value !== null && !Array.isArray(value);
17138
+ }
17139
+ function addHookOwnership(hook, owned) {
17140
+ if (!isRecord(hook) || typeof hook.command !== "string") {
17141
+ return;
17142
+ }
17143
+ owned.commands.add(hook.command);
17144
+ for (const match of hook.command.matchAll(SCRIPT_REFERENCE)) {
17145
+ owned.scripts.add(match[1]);
17146
+ }
17147
+ }
17148
+ function addGroupOwnership(group, owned, descriptions) {
17149
+ if (!isRecord(group)) {
17150
+ return;
17151
+ }
17152
+ const key = matcherKey(group);
17153
+ let forMatcher = owned.get(key);
17154
+ if (forMatcher === undefined) {
17155
+ forMatcher = { commands: new Set, scripts: new Set };
17156
+ owned.set(key, forMatcher);
17157
+ }
17158
+ if (typeof group.description === "string") {
17159
+ descriptions.add(group.description);
17160
+ }
17161
+ if (Array.isArray(group.hooks)) {
17162
+ for (const hook of group.hooks) {
17163
+ addHookOwnership(hook, forMatcher);
17164
+ }
17165
+ }
17166
+ }
17167
+ function collectOwnership(generated) {
17168
+ const descriptions = new Set(LEGACY_OMCUSTOM_DESCRIPTIONS);
17169
+ const events = new Map;
17170
+ for (const [event, groups] of Object.entries(generated)) {
17171
+ const owned = new Map;
17172
+ events.set(event, owned);
17173
+ for (const group of groups) {
17174
+ addGroupOwnership(group, owned, descriptions);
17175
+ }
17176
+ }
17177
+ return { descriptions, events };
17178
+ }
17179
+ function isOwnedCommand(hook, owned) {
17180
+ if (owned === undefined || !isRecord(hook) || hook.type !== "command") {
17181
+ return false;
17182
+ }
17183
+ const command = hook.command;
17184
+ if (typeof command !== "string") {
17185
+ return false;
17186
+ }
17187
+ if (owned.commands.has(command)) {
17188
+ return true;
17189
+ }
17190
+ const match = STANDALONE_SCRIPT_COMMAND.exec(command);
17191
+ const script = match?.[1] ?? match?.[2];
17192
+ return script !== undefined && owned.scripts.has(script);
17193
+ }
17194
+ function userResidual(group, owned, ownership) {
17195
+ if (!isRecord(group) || !Array.isArray(group.hooks)) {
17196
+ return [group];
17197
+ }
17198
+ if (typeof group.description === "string" && ownership.descriptions.has(group.description)) {
17199
+ return [];
17200
+ }
17201
+ const ownedForMatcher = owned?.get(matcherKey(group));
17202
+ const remaining = group.hooks.filter((hook) => !isOwnedCommand(hook, ownedForMatcher));
17203
+ if (remaining.length === group.hooks.length) {
17204
+ return [group];
17205
+ }
17206
+ return remaining.length === 0 ? [] : [{ ...group, hooks: remaining }];
17207
+ }
17208
+ function mergeHookBlocks(existing, generated) {
17209
+ const result = {};
17210
+ for (const [event, groups] of Object.entries(generated)) {
17211
+ result[event] = [...groups];
17212
+ }
17213
+ if (!isRecord(existing)) {
17214
+ return result;
17215
+ }
17216
+ const ownership = collectOwnership(generated);
17217
+ for (const [event, value] of Object.entries(existing)) {
17218
+ if (!Array.isArray(value)) {
17219
+ if (!ownership.events.has(event)) {
17220
+ result[event] = value;
17221
+ }
17222
+ continue;
17223
+ }
17224
+ const owned = ownership.events.get(event);
17225
+ const residual = value.flatMap((group) => userResidual(group, owned, ownership));
17226
+ if (residual.length === 0) {
17227
+ continue;
17228
+ }
17229
+ const current = result[event];
17230
+ result[event] = Array.isArray(current) ? [...current, ...residual] : residual;
17231
+ }
17232
+ return result;
17233
+ }
17234
+
17235
+ // src/core/file-preservation.ts
16969
17236
  var DEFAULT_CRITICAL_FILES = ["settings.json", "settings.local.json"];
16970
17237
  var DEFAULT_CRITICAL_DIRECTORIES = ["agent-memory", "agent-memory-local"];
16971
17238
  var PROTECTED_FRAMEWORK_FILES = ["CLAUDE.md", "AGENTS.md"];
@@ -17075,12 +17342,19 @@ async function restoreCriticalFiles(rootDir, preservation) {
17075
17342
  }
17076
17343
  return result;
17077
17344
  }
17345
+ var HOOKS_MERGE_FILES = new Set(["settings.json", "settings.local.json"]);
17346
+ function isGeneratedHookBlock(value) {
17347
+ return typeof value === "object" && value !== null && !Array.isArray(value) && Object.values(value).every((groups) => Array.isArray(groups));
17348
+ }
17078
17349
  async function mergeJsonFile(preservedPath, targetPath) {
17079
- const preservedData = await readJsonFile(preservedPath);
17350
+ const { data: preservedData, format } = parseJsonText(await readTextFile(preservedPath));
17080
17351
  if (await fileExists(targetPath)) {
17081
- const targetData = await readJsonFile(targetPath);
17352
+ const { data: targetData } = parseJsonText(await readTextFile(targetPath));
17082
17353
  const merged = deepMerge(targetData, preservedData);
17083
- await writeJsonFile(targetPath, merged);
17354
+ if (HOOKS_MERGE_FILES.has(basename2(targetPath)) && isGeneratedHookBlock(targetData.hooks)) {
17355
+ merged.hooks = mergeHookBlocks(preservedData.hooks, targetData.hooks);
17356
+ }
17357
+ await writeTextFile(targetPath, stringifyJson(merged, format));
17084
17358
  debug("preserve.merged_json", { file: basename2(targetPath) });
17085
17359
  } else {
17086
17360
  await copyFile(preservedPath, targetPath);
@@ -17368,14 +17642,14 @@ function rewriteRelativeHookCommand(command) {
17368
17642
  const interpreter = match[1] ?? "";
17369
17643
  return `${interpreter}"\${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/${match[2]}"`;
17370
17644
  }
17371
- function isRecord(value) {
17645
+ function isRecord2(value) {
17372
17646
  return typeof value === "object" && value !== null && !Array.isArray(value);
17373
17647
  }
17374
17648
  function rewriteNode(node, counter) {
17375
17649
  if (Array.isArray(node)) {
17376
17650
  return node.map((item) => rewriteNode(item, counter));
17377
17651
  }
17378
- if (!isRecord(node)) {
17652
+ if (!isRecord2(node)) {
17379
17653
  return node;
17380
17654
  }
17381
17655
  const out = {};
@@ -17392,16 +17666,32 @@ function rewriteNode(node, counter) {
17392
17666
  }
17393
17667
  return out;
17394
17668
  }
17395
- function migrateHookCommands(settings) {
17396
- if (!isRecord(settings.hooks)) {
17397
- return { settings, rewritten: 0 };
17669
+ var OLD_STATUSLINE_COMMAND = ".claude/statusline.sh";
17670
+ var NEW_STATUSLINE_COMMAND = `bash "\${CLAUDE_PROJECT_DIR:-.}/.claude/statusline.sh"`;
17671
+ function migrateStatusLine(statusLine) {
17672
+ if (!isRecord2(statusLine) || statusLine.command !== OLD_STATUSLINE_COMMAND) {
17673
+ return null;
17398
17674
  }
17675
+ return { ...statusLine, command: NEW_STATUSLINE_COMMAND };
17676
+ }
17677
+ function migrateHookCommands(settings) {
17399
17678
  const counter = { count: 0 };
17400
- const hooks = rewriteNode(settings.hooks, counter);
17679
+ const updates = {};
17680
+ if (isRecord2(settings.hooks)) {
17681
+ const hooks = rewriteNode(settings.hooks, counter);
17682
+ if (counter.count > 0) {
17683
+ updates.hooks = hooks;
17684
+ }
17685
+ }
17686
+ const statusLine = migrateStatusLine(settings.statusLine);
17687
+ if (statusLine !== null) {
17688
+ counter.count++;
17689
+ updates.statusLine = statusLine;
17690
+ }
17401
17691
  if (counter.count === 0) {
17402
17692
  return { settings, rewritten: 0 };
17403
17693
  }
17404
- return { settings: { ...settings, hooks }, rewritten: counter.count };
17694
+ return { settings: { ...settings, ...updates }, rewritten: counter.count };
17405
17695
  }
17406
17696
 
17407
17697
  // src/core/hooks-settings.ts
@@ -17874,20 +18164,44 @@ function convertHooksJson(hooksJson) {
17874
18164
  }
17875
18165
  return { hooks: result, warnings };
17876
18166
  }
17877
- async function mergeHooksIntoSettings(settingsPath, hooksJsonPath) {
17878
- const hooksJson = await readJsonFile(hooksJsonPath);
17879
- const { hooks, warnings } = convertHooksJson(hooksJson);
17880
- let settings = {};
18167
+ var UTF8_BOM2 = "\uFEFF";
18168
+ function isPlainObject(value) {
18169
+ return typeof value === "object" && value !== null && !Array.isArray(value);
18170
+ }
18171
+ async function readExistingSettings(settingsPath) {
18172
+ let raw;
17881
18173
  try {
17882
- const existingRaw = await readTextFile(settingsPath);
17883
- settings = JSON.parse(existingRaw);
18174
+ raw = await readTextFile(settingsPath);
17884
18175
  } catch {
17885
- settings = {};
18176
+ return { ok: true, settings: {}, bom: false };
18177
+ }
18178
+ const bom = raw.startsWith(UTF8_BOM2);
18179
+ if ((bom ? raw.slice(UTF8_BOM2.length) : raw).trim() === "") {
18180
+ return { ok: true, settings: {}, bom };
18181
+ }
18182
+ try {
18183
+ const { data } = parseJsonText(raw);
18184
+ if (isPlainObject(data)) {
18185
+ return { ok: true, settings: data, bom };
18186
+ }
18187
+ return { ok: false, reason: "the top-level JSON value is not a JSON object", bom };
18188
+ } catch (err) {
18189
+ return { ok: false, reason: err instanceof Error ? err.message : String(err), bom };
17886
18190
  }
17887
- settings.hooks = hooks;
18191
+ }
18192
+ async function mergeHooksIntoSettings(settingsPath, hooksJsonPath, options = {}) {
18193
+ const hooksJson = await readJsonFile(hooksJsonPath);
18194
+ const { hooks, warnings } = convertHooksJson(hooksJson);
18195
+ const preserve = options.preserveUserHooks === true;
18196
+ const existing = await readExistingSettings(settingsPath);
18197
+ if (!existing.ok && preserve) {
18198
+ warnings.push(`hooks: could not parse existing ${settingsPath} (${existing.reason}); the file was left unchanged and hooks were NOT installed — fix or move the file, then re-run \`omcustom init\``);
18199
+ return { warnings };
18200
+ }
18201
+ const settings = existing.ok ? existing.settings : {};
18202
+ settings.hooks = preserve ? mergeHookBlocks(settings.hooks, hooks) : hooks;
17888
18203
  await ensureDirectory(dirname3(settingsPath));
17889
- await writeTextFile(settingsPath, `${JSON.stringify(settings, null, 2)}
17890
- `);
18204
+ await writeTextFile(settingsPath, stringifyJson(settings, { bom: existing.bom, trailingNewline: true }));
17891
18205
  return { warnings };
17892
18206
  }
17893
18207
 
@@ -18019,6 +18333,25 @@ async function installStatusline(targetDir, options, _result) {
18019
18333
  await fs.chmod(destPath, 493);
18020
18334
  debug("install.statusline_installed", {});
18021
18335
  }
18336
+ async function installSchemas(targetDir, options, _result) {
18337
+ const layout = getProviderLayout();
18338
+ const relPath = join8(layout.rootDir, "schemas", "tool-inputs.json");
18339
+ const srcPath = resolveTemplatePath(relPath);
18340
+ const destPath = join8(targetDir, relPath);
18341
+ if (!await fileExists(srcPath)) {
18342
+ debug("install.schemas_not_found", { path: srcPath });
18343
+ return;
18344
+ }
18345
+ if (await fileExists(destPath)) {
18346
+ if (!options.force && !options.backup) {
18347
+ debug("install.schemas_skipped", { reason: "exists" });
18348
+ return;
18349
+ }
18350
+ }
18351
+ await ensureDirectory(dirname4(destPath));
18352
+ await copyFile(srcPath, destPath);
18353
+ debug("install.schemas_installed", {});
18354
+ }
18022
18355
  async function installTestsConfig(targetDir, options, _result) {
18023
18356
  const srcPath = resolveTemplatePath(join8("tests", "tsconfig.json"));
18024
18357
  const destPath = join8(targetDir, "tests", "tsconfig.json");
@@ -18035,34 +18368,55 @@ async function installTestsConfig(targetDir, options, _result) {
18035
18368
  await copyFile(srcPath, destPath);
18036
18369
  debug("install.tests_config_installed", {});
18037
18370
  }
18371
+ var STATUSLINE_REFRESH_INTERVAL = 10;
18372
+ function updateExistingStatusLine(existing) {
18373
+ const migrated = migrateHookCommands({ statusLine: existing.statusLine });
18374
+ const commandMigrated = migrated.rewritten > 0;
18375
+ if (commandMigrated) {
18376
+ existing.statusLine = migrated.settings.statusLine;
18377
+ }
18378
+ const sl = existing.statusLine;
18379
+ const refreshIntervalAdded = sl.refreshInterval === undefined;
18380
+ if (refreshIntervalAdded) {
18381
+ sl.refreshInterval = STATUSLINE_REFRESH_INTERVAL;
18382
+ }
18383
+ return { commandMigrated, refreshIntervalAdded };
18384
+ }
18385
+ async function mergeStatusLineIntoExisting(settingsPath, existing, format, defaults) {
18386
+ if (!existing.statusLine) {
18387
+ existing.statusLine = defaults.statusLine;
18388
+ await writeTextFile(settingsPath, stringifyJson(existing, format));
18389
+ debug("install.settings_local_merged", {});
18390
+ return;
18391
+ }
18392
+ const { commandMigrated, refreshIntervalAdded } = updateExistingStatusLine(existing);
18393
+ if (!commandMigrated && !refreshIntervalAdded) {
18394
+ debug("install.settings_local_skipped", { reason: "statusLine exists" });
18395
+ return;
18396
+ }
18397
+ await writeTextFile(settingsPath, stringifyJson(existing, format));
18398
+ if (commandMigrated) {
18399
+ debug("install.settings_local_statusline_migrated", {});
18400
+ }
18401
+ if (refreshIntervalAdded) {
18402
+ debug("install.settings_local_refreshInterval_added", {});
18403
+ }
18404
+ }
18038
18405
  async function installSettingsLocal(targetDir, result) {
18039
18406
  const layout = getProviderLayout();
18040
18407
  const settingsPath = join8(targetDir, layout.rootDir, "settings.local.json");
18041
18408
  const statusLineConfig = {
18042
18409
  statusLine: {
18043
18410
  type: "command",
18044
- command: ".claude/statusline.sh",
18411
+ command: NEW_STATUSLINE_COMMAND,
18045
18412
  padding: 0,
18046
- refreshInterval: 10
18413
+ refreshInterval: STATUSLINE_REFRESH_INTERVAL
18047
18414
  }
18048
18415
  };
18049
18416
  if (await fileExists(settingsPath)) {
18050
18417
  try {
18051
- const existing = await readJsonFile(settingsPath);
18052
- if (!existing.statusLine) {
18053
- existing.statusLine = statusLineConfig.statusLine;
18054
- await writeJsonFile(settingsPath, existing);
18055
- debug("install.settings_local_merged", {});
18056
- } else {
18057
- const sl = existing.statusLine;
18058
- if (sl.refreshInterval === undefined) {
18059
- sl.refreshInterval = statusLineConfig.statusLine.refreshInterval;
18060
- await writeJsonFile(settingsPath, existing);
18061
- debug("install.settings_local_refreshInterval_added", {});
18062
- } else {
18063
- debug("install.settings_local_skipped", { reason: "statusLine exists" });
18064
- }
18065
- }
18418
+ const { data: existing, format } = parseJsonText(await readTextFile(settingsPath));
18419
+ await mergeStatusLineIntoExisting(settingsPath, existing, format, statusLineConfig);
18066
18420
  } catch {
18067
18421
  result.warnings.push("Failed to parse existing settings.local.json, skipping statusLine config");
18068
18422
  }
@@ -18080,7 +18434,9 @@ async function installHooksSettings(targetDir, _options, result) {
18080
18434
  return;
18081
18435
  }
18082
18436
  try {
18083
- const mergeResult = await mergeHooksIntoSettings(settingsPath, hooksJsonPath);
18437
+ const mergeResult = await mergeHooksIntoSettings(settingsPath, hooksJsonPath, {
18438
+ preserveUserHooks: true
18439
+ });
18084
18440
  if (mergeResult && Array.isArray(mergeResult.warnings) && mergeResult.warnings.length > 0) {
18085
18441
  result.warnings.push(...mergeResult.warnings);
18086
18442
  }
@@ -18173,6 +18529,7 @@ async function install(options) {
18173
18529
  await verifyTemplateDirectory();
18174
18530
  await installAllComponents(options.targetDir, options, result);
18175
18531
  await installStatusline(options.targetDir, options, result);
18532
+ await installSchemas(options.targetDir, options, result);
18176
18533
  await installTestsConfig(options.targetDir, options, result);
18177
18534
  await installSettingsLocal(options.targetDir, result);
18178
18535
  await installHooksSettings(options.targetDir, options, result);
@@ -18430,7 +18787,7 @@ async function generateMCPConfig(targetDir) {
18430
18787
  await writeFile2(mcpConfigPath, `${JSON.stringify(config, null, 2)}
18431
18788
  `);
18432
18789
  }
18433
- info("ontology-rag MCP server configured successfully");
18790
+ info("mcp.ontology_rag_configured");
18434
18791
  }
18435
18792
 
18436
18793
  // src/core/ontology-rag-setup.ts
@@ -19660,7 +20017,7 @@ async function initCommand(options) {
19660
20017
  }
19661
20018
 
19662
20019
  // src/cli/list.ts
19663
- import { basename as basename5, dirname as dirname4, join as join14, relative as relative3 } from "node:path";
20020
+ import { basename as basename5, dirname as dirname5, join as join14, relative as relative3 } from "node:path";
19664
20021
  init_fs();
19665
20022
  var ALLOWED_TOP_LEVEL_KEYS = new Set(["name", "type", "description", "version", "category"]);
19666
20023
  function parseKeyValue(line) {
@@ -19861,7 +20218,7 @@ async function getSkills(targetDir, rootDir = ".claude", config) {
19861
20218
  const customSkillPaths = new Set(customComponents.filter((c) => c.type === "skill").map((c) => c.path));
19862
20219
  const skillMdFiles = await listFiles(skillsDir, { recursive: true, pattern: "SKILL.md" });
19863
20220
  const skills = await Promise.all(skillMdFiles.map(async (skillMdPath) => {
19864
- const skillDir = dirname4(skillMdPath);
20221
+ const skillDir = dirname5(skillMdPath);
19865
20222
  const indexYamlPath = join14(skillDir, "index.yaml");
19866
20223
  const { description, version } = await tryReadIndexYamlMetadata(indexYamlPath);
19867
20224
  const relativePath = relative3(targetDir, skillDir);
@@ -21061,11 +21418,12 @@ async function migrateHookCommandsInSettingsLocal(targetDir, options) {
21061
21418
  });
21062
21419
  }
21063
21420
  }
21064
- async function runFullUpdatePostProcessing(options, result, config) {
21421
+ async function runFullUpdatePostProcessing(options, result, config, customizations) {
21065
21422
  const isFullUpdate = !options.components || options.components.length === 0;
21066
21423
  if (isFullUpdate) {
21067
- const synced = await syncRootLevelFiles(options.targetDir, options);
21424
+ const { synced, preserved } = await syncRootLevelFiles(options.targetDir, options, customizations);
21068
21425
  result.syncedRootFiles = synced;
21426
+ result.preservedFiles.push(...preserved);
21069
21427
  const removed = await removeDeprecatedFiles(options.targetDir, options);
21070
21428
  result.removedDeprecatedFiles = removed;
21071
21429
  if (!options.dryRun) {
@@ -21182,7 +21540,7 @@ async function update(options) {
21182
21540
  const lockfile = await readLockfile(options.targetDir);
21183
21541
  const components = options.components || getAllUpdateComponents();
21184
21542
  await updateAllComponents(options.targetDir, components, updateCheck, customizations, options, result, config, lockfile);
21185
- await runFullUpdatePostProcessing(options, result, config);
21543
+ await runFullUpdatePostProcessing(options, result, config, customizations);
21186
21544
  await regenerateLockfile(options.targetDir, result);
21187
21545
  checkAndInstallRtkAfterUpdate();
21188
21546
  checkAndInstallCodexAfterUpdate();
@@ -21373,19 +21731,31 @@ async function updateComponent(targetDir, component, customizations, options, co
21373
21731
  });
21374
21732
  return preservedFiles;
21375
21733
  }
21376
- var ROOT_LEVEL_FILES = ["statusline.sh", "install-hooks.sh", "uninstall-hooks.sh"];
21377
- async function syncRootLevelFiles(targetDir, options) {
21734
+ var ROOT_LEVEL_FILES = [
21735
+ "statusline.sh",
21736
+ "install-hooks.sh",
21737
+ "uninstall-hooks.sh",
21738
+ "schemas/tool-inputs.json"
21739
+ ];
21740
+ async function syncRootLevelFiles(targetDir, options, customizations) {
21378
21741
  if (options.dryRun) {
21379
- return ROOT_LEVEL_FILES;
21742
+ return { synced: ROOT_LEVEL_FILES, preserved: [] };
21380
21743
  }
21381
21744
  const fs = await import("node:fs/promises");
21382
21745
  const layout = getProviderLayout();
21383
21746
  const synced = [];
21747
+ const preserved = [];
21748
+ const preserveSet = new Set(customizations && !options.forceOverwriteAll ? customizations.preserveFiles : []);
21384
21749
  for (const fileName of ROOT_LEVEL_FILES) {
21385
21750
  const srcPath = resolveTemplatePath(join18(layout.rootDir, fileName));
21386
21751
  if (!await fileExists(srcPath)) {
21387
21752
  continue;
21388
21753
  }
21754
+ const relPath = `${layout.rootDir}/${fileName}`;
21755
+ if (preserveSet.has(relPath)) {
21756
+ preserved.push(relPath);
21757
+ continue;
21758
+ }
21389
21759
  const destPath = join18(targetDir, layout.rootDir, fileName);
21390
21760
  await ensureDirectory(join18(destPath, ".."));
21391
21761
  await fs.copyFile(srcPath, destPath);
@@ -21397,7 +21767,7 @@ async function syncRootLevelFiles(targetDir, options) {
21397
21767
  if (synced.length > 0) {
21398
21768
  debug("update.root_files_synced", { files: synced.join(", ") });
21399
21769
  }
21400
- return synced;
21770
+ return { synced, preserved };
21401
21771
  }
21402
21772
  async function removeDeprecatedFiles(targetDir, options) {
21403
21773
  const manifestPath = resolveTemplatePath("deprecated-files.json");