oh-my-customcode 1.1.92 → 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/index.js CHANGED
@@ -151,6 +151,19 @@ async function writeJsonFile(path, data) {
151
151
  const content = JSON.stringify(data, null, 2);
152
152
  await fs.writeFile(path, content, "utf-8");
153
153
  }
154
+ function parseJsonText(raw) {
155
+ const bom = raw.startsWith(UTF8_BOM);
156
+ const text = bom ? raw.slice(UTF8_BOM.length) : raw;
157
+ return {
158
+ data: JSON.parse(text),
159
+ format: { bom, trailingNewline: text.endsWith(`
160
+ `) }
161
+ };
162
+ }
163
+ function stringifyJson(data, format) {
164
+ return `${format.bom ? UTF8_BOM : ""}${JSON.stringify(data, null, 2)}${format.trailingNewline ? `
165
+ ` : ""}`;
166
+ }
154
167
  async function readTextFile(path) {
155
168
  const fs = await import("node:fs/promises");
156
169
  return fs.readFile(path, "utf-8");
@@ -188,6 +201,7 @@ function matchesPattern(filename, pattern) {
188
201
  const regex = new RegExp(`^${regexPattern}$`);
189
202
  return regex.test(filename);
190
203
  }
204
+ var UTF8_BOM = "\uFEFF";
191
205
  var init_fs = () => {};
192
206
 
193
207
  // src/core/registry.ts
@@ -330,7 +344,7 @@ var MESSAGES = {
330
344
  "install.entry_md_skipped": "{{entry}} skipped ({{reason}})",
331
345
  "install.lockfile_generated": "Lockfile generated ({{files}} files tracked)",
332
346
  "install.lockfile_failed": "Failed to generate lockfile: {{error}}",
333
- "install.hook_commands_migrated": "Migrated {{rewritten}} hook command(s) in settings.local.json",
347
+ "install.hook_commands_migrated": "Migrated {{rewritten}} hook/statusLine command(s) in settings.local.json",
334
348
  "install.hook_commands_migration_failed": "Failed to migrate hook commands in settings.local.json: {{error}}",
335
349
  "lockfile.not_found": "Lockfile not found: {{path}}",
336
350
  "lockfile.invalid_version": "Invalid lockfile version: {{path}}",
@@ -353,15 +367,86 @@ var MESSAGES = {
353
367
  "update.lockfile_failed": "Failed to regenerate lockfile: {{error}}",
354
368
  "update.protected_file_updated": "⟳ Protected file {{file}} in {{component}} updated: {{hint}}",
355
369
  "update.namespace_synced": "Namespace synced: {{file}} ({{component}})",
356
- "update.hook_commands_migrated": "Migrated {{count}} hook command(s) to CLAUDE_PROJECT_DIR-anchored paths",
370
+ "update.hook_commands_migrated": "Migrated {{count}} hook/statusLine command(s) to CLAUDE_PROJECT_DIR-anchored paths",
357
371
  "update.hook_commands_migration_failed": "Failed to migrate hook commands in {{path}}",
358
372
  "config.load_failed": "Failed to load config: {{error}}",
359
373
  "config.not_found": "Config not found at {{path}}, using defaults",
360
374
  "config.saved": "Config saved to {{path}}",
361
375
  "config.deleted": "Config deleted from {{path}}",
376
+ "config.invalid_preserve_path": "Ignored invalid preserve path: {{path}} ({{reason}})",
377
+ "codex.already_installed": "Codex CLI is already installed",
378
+ "codex.installing_brew": "Installing Codex CLI via Homebrew...",
379
+ "codex.installing_npm": "Installing Codex CLI via npm...",
380
+ "codex.install_failed": "Failed to install Codex CLI: {{error}}",
381
+ "codex.unsupported_os": "Codex CLI auto-install is not supported on {{os}}",
382
+ "rtk.already_installed": "RTK is already installed",
383
+ "rtk.installing_brew": "Installing RTK via Homebrew...",
384
+ "rtk.installing_curl": "Installing RTK via install script...",
385
+ "rtk.install_failed": "Failed to install RTK: {{error}}",
386
+ "rtk.unsupported_os": "RTK auto-install is not supported on {{os}}",
387
+ "mcp.ontology_rag_configured": "ontology-rag MCP server configured successfully",
388
+ "install.codex_installing": "Installing Codex CLI...",
389
+ "install.codex_success": "Codex CLI installed successfully",
390
+ "install.codex_already": "Codex CLI is already installed",
391
+ "install.rtk_installing": "Installing RTK...",
392
+ "install.rtk_success": "RTK installed successfully",
393
+ "install.rtk_already": "RTK is already installed",
394
+ "install.preserved": "Preserved {{files}} file(s) and {{dirs}} directory(ies) from the existing installation",
395
+ "install.restored": "Restored {{files}} file(s) and {{dirs}} directory(ies) after installation",
396
+ "install.backup_failed": "Failed to back up {{path}}: {{error}}",
397
+ "install.hooks_settings_failed": "Failed to merge hooks into settings.local.json: {{error}}",
398
+ "preserve.cleanup_failed": "Failed to clean up preservation directory {{dir}}: {{error}}",
399
+ "preserve.extract_dir_failed": "Failed to preserve directory {{dir}}: {{error}}",
400
+ "preserve.extract_failed": "Failed to preserve file {{file}}: {{error}}",
401
+ "preserve.restore_dir_failed": "Failed to restore directory {{dir}}: {{error}}",
402
+ "preserve.restore_failed": "Failed to restore file {{file}}: {{error}}",
403
+ "preserve_files.invalid_path": "Ignored invalid preserve_files entry from {{source}}: {{path}} ({{reason}})",
404
+ "update.codex_missing": "Codex CLI is not installed",
405
+ "update.rtk_missing": "RTK is not installed",
406
+ "update.self_update_skipped": "Skipped update: source project cannot update itself",
407
+ "update.deprecated_file_invalid_path": "Skipped deprecated file with invalid path: {{path}} ({{reason}})",
408
+ "update.deprecated_file_removed": "Removed deprecated file: {{path}} ({{reason}})",
409
+ "update.entry_doc_created": "Entry document created: {{path}}",
410
+ "update.entry_doc_force_updated": "Entry document force-updated: {{path}}",
411
+ "update.entry_merge_warning": "Entry document merge warning: {{warning}}",
412
+ "update.entry_template_not_found": "Entry document template not found: {{template}}",
413
+ "update.protected_file_force_overwrite": "Protected file {{file}} in {{component}} overwritten: {{hint}}",
414
+ "update.protected_file_skipped": "Protected file {{file}} in {{component}} skipped: {{hint}}",
415
+ "update.settings_local_backfill_failed": "Failed to backfill statusLine refreshInterval in {{path}}",
362
416
  "general.done": "Done!",
363
417
  "general.failed": "Failed",
364
- "general.skipped": "Skipped"
418
+ "general.skipped": "Skipped",
419
+ "install.agent_domain_excluded": "Excluded agent {{agent}} (domain: {{domain}})",
420
+ "install.backed_up": "Backed up {{from}} to {{to}}",
421
+ "install.hooks_settings_merged": "Merged hooks wiring into settings.local.json",
422
+ "install.hooks_settings_skipped": "Skipped hooks wiring in settings.local.json: {{reason}}",
423
+ "install.schemas_not_found": "schemas/tool-inputs.json template not found: {{path}}",
424
+ "install.schemas_skipped": "Skipped schemas/tool-inputs.json: {{reason}}",
425
+ "install.schemas_installed": "Installed schemas/tool-inputs.json",
426
+ "install.settings_local_created": "Created settings.local.json",
427
+ "install.settings_local_merged": "Merged statusLine into settings.local.json",
428
+ "install.settings_local_refreshInterval_added": "Added statusLine refreshInterval to settings.local.json",
429
+ "install.settings_local_skipped": "Skipped settings.local.json statusLine: {{reason}}",
430
+ "install.settings_local_statusline_migrated": "Re-anchored default statusLine command in settings.local.json",
431
+ "install.skill_scope_excluded": "Excluded skill {{skill}} (scope: {{scope}})",
432
+ "install.statusline_installed": "Installed statusline.sh",
433
+ "install.statusline_not_found": "statusline.sh template not found: {{path}}",
434
+ "install.statusline_skipped": "Skipped statusline.sh: {{reason}}",
435
+ "install.tests_config_installed": "Installed tests/tsconfig.json",
436
+ "install.tests_config_not_found": "tests/tsconfig.json template not found: {{path}}",
437
+ "install.tests_config_skipped": "Skipped tests/tsconfig.json: {{reason}}",
438
+ "preserve.cleanup": "Cleaned up preservation directory: {{dir}}",
439
+ "preserve.copied_json": "Copied JSON file: {{file}}",
440
+ "preserve.extracted_dir": "Preserved directory: {{dir}}",
441
+ "preserve.extracted_file": "Preserved file: {{file}}",
442
+ "preserve.merged_json": "Merged JSON file: {{file}}",
443
+ "preserve.restored_dir": "Restored directory: {{dir}}",
444
+ "preserve.restored_file": "Restored file: {{file}}",
445
+ "update.deprecated_files_cleaned": "Removed {{count}} deprecated file(s)",
446
+ "update.entry_doc_merged": "Entry document merged: {{path}} ({{managed}} managed, {{custom}} custom sections)",
447
+ "update.file_backed_up": "Backed up {{path}} to {{backup}}",
448
+ "update.root_files_synced": "Root-level files synced: {{files}}",
449
+ "update.settings_local_refreshInterval_backfilled": "Backfilled statusLine refreshInterval in settings.local.json"
365
450
  },
366
451
  ko: {
367
452
  "install.start": "oh-my-customcode 초기화 중...",
@@ -380,7 +465,7 @@ var MESSAGES = {
380
465
  "install.entry_md_skipped": "{{entry}} 건너뜀 ({{reason}})",
381
466
  "install.lockfile_generated": "잠금 파일 생성 완료 ({{files}}개 파일 추적)",
382
467
  "install.lockfile_failed": "잠금 파일 생성 실패: {{error}}",
383
- "install.hook_commands_migrated": "settings.local.json의 훅 명령 {{rewritten}}개를 마이그레이션했습니다",
468
+ "install.hook_commands_migrated": "settings.local.json의 훅/상태줄 명령 {{rewritten}}개를 마이그레이션했습니다",
384
469
  "install.hook_commands_migration_failed": "settings.local.json의 훅 명령 마이그레이션에 실패했습니다: {{error}}",
385
470
  "lockfile.not_found": "잠금 파일 없음: {{path}}",
386
471
  "lockfile.invalid_version": "잠금 파일 버전 유효하지 않음: {{path}}",
@@ -403,15 +488,86 @@ var MESSAGES = {
403
488
  "update.lockfile_failed": "잠금 파일 재생성 실패: {{error}}",
404
489
  "update.protected_file_updated": "⟳ 보호 파일 {{file}} ({{component}}) 업데이트됨: {{hint}}",
405
490
  "update.namespace_synced": "네임스페이스 동기화: {{file}} ({{component}})",
406
- "update.hook_commands_migrated": "CLAUDE_PROJECT_DIR 기준 경로로 훅 명령 {{count}}개를 마이그레이션했습니다",
491
+ "update.hook_commands_migrated": "CLAUDE_PROJECT_DIR 기준 경로로 훅/상태줄 명령 {{count}}개를 마이그레이션했습니다",
407
492
  "update.hook_commands_migration_failed": "{{path}}의 훅 명령 마이그레이션에 실패했습니다",
408
493
  "config.load_failed": "설정 로드 실패: {{error}}",
409
494
  "config.not_found": "{{path}}에 설정 없음, 기본값 사용",
410
495
  "config.saved": "설정 저장: {{path}}",
411
496
  "config.deleted": "설정 삭제: {{path}}",
497
+ "config.invalid_preserve_path": "유효하지 않은 보존 경로를 무시했습니다: {{path}} ({{reason}})",
498
+ "codex.already_installed": "Codex CLI가 이미 설치되어 있습니다",
499
+ "codex.installing_brew": "Homebrew로 Codex CLI를 설치합니다...",
500
+ "codex.installing_npm": "npm으로 Codex CLI를 설치합니다...",
501
+ "codex.install_failed": "Codex CLI 설치에 실패했습니다: {{error}}",
502
+ "codex.unsupported_os": "{{os}}에서는 Codex CLI 자동 설치를 지원하지 않습니다",
503
+ "rtk.already_installed": "RTK가 이미 설치되어 있습니다",
504
+ "rtk.installing_brew": "Homebrew로 RTK를 설치합니다...",
505
+ "rtk.installing_curl": "설치 스크립트로 RTK를 설치합니다...",
506
+ "rtk.install_failed": "RTK 설치에 실패했습니다: {{error}}",
507
+ "rtk.unsupported_os": "{{os}}에서는 RTK 자동 설치를 지원하지 않습니다",
508
+ "mcp.ontology_rag_configured": "ontology-rag MCP 서버 구성을 완료했습니다",
509
+ "install.codex_installing": "Codex CLI를 설치합니다...",
510
+ "install.codex_success": "Codex CLI를 설치했습니다",
511
+ "install.codex_already": "Codex CLI가 이미 설치되어 있습니다",
512
+ "install.rtk_installing": "RTK를 설치합니다...",
513
+ "install.rtk_success": "RTK를 설치했습니다",
514
+ "install.rtk_already": "RTK가 이미 설치되어 있습니다",
515
+ "install.preserved": "기존 설치에서 파일 {{files}}개와 디렉토리 {{dirs}}개를 보존했습니다",
516
+ "install.restored": "설치 후 파일 {{files}}개와 디렉토리 {{dirs}}개를 복원했습니다",
517
+ "install.backup_failed": "{{path}} 백업에 실패했습니다: {{error}}",
518
+ "install.hooks_settings_failed": "settings.local.json에 훅을 병합하지 못했습니다: {{error}}",
519
+ "preserve.cleanup_failed": "보존 디렉토리 {{dir}} 정리에 실패했습니다: {{error}}",
520
+ "preserve.extract_dir_failed": "디렉토리 {{dir}} 보존에 실패했습니다: {{error}}",
521
+ "preserve.extract_failed": "파일 {{file}} 보존에 실패했습니다: {{error}}",
522
+ "preserve.restore_dir_failed": "디렉토리 {{dir}} 복원에 실패했습니다: {{error}}",
523
+ "preserve.restore_failed": "파일 {{file}} 복원에 실패했습니다: {{error}}",
524
+ "preserve_files.invalid_path": "{{source}}의 유효하지 않은 preserve_files 항목을 무시했습니다: {{path}} ({{reason}})",
525
+ "update.codex_missing": "Codex CLI가 설치되어 있지 않습니다",
526
+ "update.rtk_missing": "RTK가 설치되어 있지 않습니다",
527
+ "update.self_update_skipped": "업데이트를 건너뜁니다: 소스 프로젝트는 스스로 업데이트할 수 없습니다",
528
+ "update.deprecated_file_invalid_path": "경로가 유효하지 않아 지원 중단 파일을 건너뜁니다: {{path}} ({{reason}})",
529
+ "update.deprecated_file_removed": "지원 중단 파일을 삭제했습니다: {{path}} ({{reason}})",
530
+ "update.entry_doc_created": "진입 문서를 생성했습니다: {{path}}",
531
+ "update.entry_doc_force_updated": "진입 문서를 강제로 업데이트했습니다: {{path}}",
532
+ "update.entry_merge_warning": "진입 문서 병합 경고: {{warning}}",
533
+ "update.entry_template_not_found": "진입 문서 템플릿을 찾을 수 없습니다: {{template}}",
534
+ "update.protected_file_force_overwrite": "{{component}}의 보호 파일 {{file}}을(를) 덮어썼습니다: {{hint}}",
535
+ "update.protected_file_skipped": "{{component}}의 보호 파일 {{file}}을(를) 건너뜁니다: {{hint}}",
536
+ "update.settings_local_backfill_failed": "{{path}}의 statusLine refreshInterval 보충에 실패했습니다",
412
537
  "general.done": "완료!",
413
538
  "general.failed": "실패",
414
- "general.skipped": "건너뜀"
539
+ "general.skipped": "건너뜀",
540
+ "install.agent_domain_excluded": "에이전트 {{agent}}을(를) 제외했습니다 (도메인: {{domain}})",
541
+ "install.backed_up": "{{from}}을(를) {{to}}(으)로 백업했습니다",
542
+ "install.hooks_settings_merged": "settings.local.json에 훅 연결을 병합했습니다",
543
+ "install.hooks_settings_skipped": "settings.local.json의 훅 연결을 건너뜁니다: {{reason}}",
544
+ "install.schemas_not_found": "schemas/tool-inputs.json 템플릿을 찾을 수 없습니다: {{path}}",
545
+ "install.schemas_skipped": "schemas/tool-inputs.json 설치를 건너뜁니다: {{reason}}",
546
+ "install.schemas_installed": "schemas/tool-inputs.json을 설치했습니다",
547
+ "install.settings_local_created": "settings.local.json을 생성했습니다",
548
+ "install.settings_local_merged": "settings.local.json에 statusLine을 병합했습니다",
549
+ "install.settings_local_refreshInterval_added": "settings.local.json에 statusLine refreshInterval을 추가했습니다",
550
+ "install.settings_local_skipped": "settings.local.json의 statusLine을 건너뜁니다: {{reason}}",
551
+ "install.settings_local_statusline_migrated": "settings.local.json의 기본 statusLine 명령을 프로젝트 루트 기준으로 변경했습니다",
552
+ "install.skill_scope_excluded": "스킬 {{skill}}을(를) 제외했습니다 (범위: {{scope}})",
553
+ "install.statusline_installed": "statusline.sh를 설치했습니다",
554
+ "install.statusline_not_found": "statusline.sh 템플릿을 찾을 수 없습니다: {{path}}",
555
+ "install.statusline_skipped": "statusline.sh 설치를 건너뜁니다: {{reason}}",
556
+ "install.tests_config_installed": "tests/tsconfig.json을 설치했습니다",
557
+ "install.tests_config_not_found": "tests/tsconfig.json 템플릿을 찾을 수 없습니다: {{path}}",
558
+ "install.tests_config_skipped": "tests/tsconfig.json 설치를 건너뜁니다: {{reason}}",
559
+ "preserve.cleanup": "보존 디렉토리를 정리했습니다: {{dir}}",
560
+ "preserve.copied_json": "JSON 파일을 복사했습니다: {{file}}",
561
+ "preserve.extracted_dir": "디렉토리를 보존했습니다: {{dir}}",
562
+ "preserve.extracted_file": "파일을 보존했습니다: {{file}}",
563
+ "preserve.merged_json": "JSON 파일을 병합했습니다: {{file}}",
564
+ "preserve.restored_dir": "디렉토리를 복원했습니다: {{dir}}",
565
+ "preserve.restored_file": "파일을 복원했습니다: {{file}}",
566
+ "update.deprecated_files_cleaned": "지원 중단 파일 {{count}}개를 삭제했습니다",
567
+ "update.entry_doc_merged": "진입 문서를 병합했습니다: {{path}} (관리 섹션 {{managed}}개, 사용자 섹션 {{custom}}개)",
568
+ "update.file_backed_up": "{{path}}을(를) {{backup}}(으)로 백업했습니다",
569
+ "update.root_files_synced": "루트 수준 파일을 동기화했습니다: {{files}}",
570
+ "update.settings_local_refreshInterval_backfilled": "settings.local.json의 statusLine refreshInterval을 보충했습니다"
415
571
  }
416
572
  };
417
573
  function createLogger(options = {}) {
@@ -893,7 +1049,7 @@ import {
893
1049
  rename,
894
1050
  stat as stat2
895
1051
  } from "node:fs/promises";
896
- import { basename as basename2, join as join5 } from "node:path";
1052
+ import { basename as basename2, dirname as dirname3, join as join5 } from "node:path";
897
1053
 
898
1054
  // src/core/codex-installer.ts
899
1055
  import { execSync } from "node:child_process";
@@ -957,6 +1113,117 @@ function installCodex(deps = defaultDeps) {
957
1113
  // src/core/file-preservation.ts
958
1114
  init_fs();
959
1115
  import { basename, join as join3 } from "node:path";
1116
+
1117
+ // src/core/hook-group-merge.ts
1118
+ var LEGACY_OMCUSTOM_DESCRIPTIONS = [
1119
+ "Record agent outcomes + auto-continue workflow + R007/R008 drift advisory on subagent completion (autonomous-loop re-entry, #1545)",
1120
+ '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)'
1121
+ ];
1122
+ var STANDALONE_SCRIPT_COMMAND = /^(?:bash )?(?:"(?:\$\{CLAUDE_PROJECT_DIR:-\.\}\/|\.\/)?\.claude\/hooks\/((?:scripts\/)?[A-Za-z0-9._-]+\.sh)"|(?:\.\/)?\.claude\/hooks\/((?:scripts\/)?[A-Za-z0-9._-]+\.sh))$/;
1123
+ var SCRIPT_REFERENCE = /\.claude\/hooks\/((?:scripts\/)?[A-Za-z0-9._-]+\.sh)/g;
1124
+ function matcherKey(group) {
1125
+ return typeof group.matcher === "string" ? group.matcher : "";
1126
+ }
1127
+ function isRecord(value) {
1128
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1129
+ }
1130
+ function addHookOwnership(hook, owned) {
1131
+ if (!isRecord(hook) || typeof hook.command !== "string") {
1132
+ return;
1133
+ }
1134
+ owned.commands.add(hook.command);
1135
+ for (const match of hook.command.matchAll(SCRIPT_REFERENCE)) {
1136
+ owned.scripts.add(match[1]);
1137
+ }
1138
+ }
1139
+ function addGroupOwnership(group, owned, descriptions) {
1140
+ if (!isRecord(group)) {
1141
+ return;
1142
+ }
1143
+ const key = matcherKey(group);
1144
+ let forMatcher = owned.get(key);
1145
+ if (forMatcher === undefined) {
1146
+ forMatcher = { commands: new Set, scripts: new Set };
1147
+ owned.set(key, forMatcher);
1148
+ }
1149
+ if (typeof group.description === "string") {
1150
+ descriptions.add(group.description);
1151
+ }
1152
+ if (Array.isArray(group.hooks)) {
1153
+ for (const hook of group.hooks) {
1154
+ addHookOwnership(hook, forMatcher);
1155
+ }
1156
+ }
1157
+ }
1158
+ function collectOwnership(generated) {
1159
+ const descriptions = new Set(LEGACY_OMCUSTOM_DESCRIPTIONS);
1160
+ const events = new Map;
1161
+ for (const [event, groups] of Object.entries(generated)) {
1162
+ const owned = new Map;
1163
+ events.set(event, owned);
1164
+ for (const group of groups) {
1165
+ addGroupOwnership(group, owned, descriptions);
1166
+ }
1167
+ }
1168
+ return { descriptions, events };
1169
+ }
1170
+ function isOwnedCommand(hook, owned) {
1171
+ if (owned === undefined || !isRecord(hook) || hook.type !== "command") {
1172
+ return false;
1173
+ }
1174
+ const command = hook.command;
1175
+ if (typeof command !== "string") {
1176
+ return false;
1177
+ }
1178
+ if (owned.commands.has(command)) {
1179
+ return true;
1180
+ }
1181
+ const match = STANDALONE_SCRIPT_COMMAND.exec(command);
1182
+ const script = match?.[1] ?? match?.[2];
1183
+ return script !== undefined && owned.scripts.has(script);
1184
+ }
1185
+ function userResidual(group, owned, ownership) {
1186
+ if (!isRecord(group) || !Array.isArray(group.hooks)) {
1187
+ return [group];
1188
+ }
1189
+ if (typeof group.description === "string" && ownership.descriptions.has(group.description)) {
1190
+ return [];
1191
+ }
1192
+ const ownedForMatcher = owned?.get(matcherKey(group));
1193
+ const remaining = group.hooks.filter((hook) => !isOwnedCommand(hook, ownedForMatcher));
1194
+ if (remaining.length === group.hooks.length) {
1195
+ return [group];
1196
+ }
1197
+ return remaining.length === 0 ? [] : [{ ...group, hooks: remaining }];
1198
+ }
1199
+ function mergeHookBlocks(existing, generated) {
1200
+ const result = {};
1201
+ for (const [event, groups] of Object.entries(generated)) {
1202
+ result[event] = [...groups];
1203
+ }
1204
+ if (!isRecord(existing)) {
1205
+ return result;
1206
+ }
1207
+ const ownership = collectOwnership(generated);
1208
+ for (const [event, value] of Object.entries(existing)) {
1209
+ if (!Array.isArray(value)) {
1210
+ if (!ownership.events.has(event)) {
1211
+ result[event] = value;
1212
+ }
1213
+ continue;
1214
+ }
1215
+ const owned = ownership.events.get(event);
1216
+ const residual = value.flatMap((group) => userResidual(group, owned, ownership));
1217
+ if (residual.length === 0) {
1218
+ continue;
1219
+ }
1220
+ const current = result[event];
1221
+ result[event] = Array.isArray(current) ? [...current, ...residual] : residual;
1222
+ }
1223
+ return result;
1224
+ }
1225
+
1226
+ // src/core/file-preservation.ts
960
1227
  var DEFAULT_CRITICAL_FILES = ["settings.json", "settings.local.json"];
961
1228
  var DEFAULT_CRITICAL_DIRECTORIES = ["agent-memory", "agent-memory-local"];
962
1229
  var PROTECTED_FRAMEWORK_FILES = ["CLAUDE.md", "AGENTS.md"];
@@ -1066,12 +1333,19 @@ async function restoreCriticalFiles(rootDir, preservation) {
1066
1333
  }
1067
1334
  return result;
1068
1335
  }
1336
+ var HOOKS_MERGE_FILES = new Set(["settings.json", "settings.local.json"]);
1337
+ function isGeneratedHookBlock(value) {
1338
+ return typeof value === "object" && value !== null && !Array.isArray(value) && Object.values(value).every((groups) => Array.isArray(groups));
1339
+ }
1069
1340
  async function mergeJsonFile(preservedPath, targetPath) {
1070
- const preservedData = await readJsonFile(preservedPath);
1341
+ const { data: preservedData, format } = parseJsonText(await readTextFile(preservedPath));
1071
1342
  if (await fileExists(targetPath)) {
1072
- const targetData = await readJsonFile(targetPath);
1343
+ const { data: targetData } = parseJsonText(await readTextFile(targetPath));
1073
1344
  const merged = deepMerge(targetData, preservedData);
1074
- await writeJsonFile(targetPath, merged);
1345
+ if (HOOKS_MERGE_FILES.has(basename(targetPath)) && isGeneratedHookBlock(targetData.hooks)) {
1346
+ merged.hooks = mergeHookBlocks(preservedData.hooks, targetData.hooks);
1347
+ }
1348
+ await writeTextFile(targetPath, stringifyJson(merged, format));
1075
1349
  debug("preserve.merged_json", { file: basename(targetPath) });
1076
1350
  } else {
1077
1351
  await copyFile(preservedPath, targetPath);
@@ -1112,14 +1386,14 @@ function rewriteRelativeHookCommand(command) {
1112
1386
  const interpreter = match[1] ?? "";
1113
1387
  return `${interpreter}"\${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/${match[2]}"`;
1114
1388
  }
1115
- function isRecord(value) {
1389
+ function isRecord2(value) {
1116
1390
  return typeof value === "object" && value !== null && !Array.isArray(value);
1117
1391
  }
1118
1392
  function rewriteNode(node, counter) {
1119
1393
  if (Array.isArray(node)) {
1120
1394
  return node.map((item) => rewriteNode(item, counter));
1121
1395
  }
1122
- if (!isRecord(node)) {
1396
+ if (!isRecord2(node)) {
1123
1397
  return node;
1124
1398
  }
1125
1399
  const out = {};
@@ -1136,16 +1410,32 @@ function rewriteNode(node, counter) {
1136
1410
  }
1137
1411
  return out;
1138
1412
  }
1139
- function migrateHookCommands(settings) {
1140
- if (!isRecord(settings.hooks)) {
1141
- return { settings, rewritten: 0 };
1413
+ var OLD_STATUSLINE_COMMAND = ".claude/statusline.sh";
1414
+ var NEW_STATUSLINE_COMMAND = `bash "\${CLAUDE_PROJECT_DIR:-.}/.claude/statusline.sh"`;
1415
+ function migrateStatusLine(statusLine) {
1416
+ if (!isRecord2(statusLine) || statusLine.command !== OLD_STATUSLINE_COMMAND) {
1417
+ return null;
1142
1418
  }
1419
+ return { ...statusLine, command: NEW_STATUSLINE_COMMAND };
1420
+ }
1421
+ function migrateHookCommands(settings) {
1143
1422
  const counter = { count: 0 };
1144
- const hooks = rewriteNode(settings.hooks, counter);
1423
+ const updates = {};
1424
+ if (isRecord2(settings.hooks)) {
1425
+ const hooks = rewriteNode(settings.hooks, counter);
1426
+ if (counter.count > 0) {
1427
+ updates.hooks = hooks;
1428
+ }
1429
+ }
1430
+ const statusLine = migrateStatusLine(settings.statusLine);
1431
+ if (statusLine !== null) {
1432
+ counter.count++;
1433
+ updates.statusLine = statusLine;
1434
+ }
1145
1435
  if (counter.count === 0) {
1146
1436
  return { settings, rewritten: 0 };
1147
1437
  }
1148
- return { settings: { ...settings, hooks }, rewritten: counter.count };
1438
+ return { settings: { ...settings, ...updates }, rewritten: counter.count };
1149
1439
  }
1150
1440
 
1151
1441
  // src/core/hooks-settings.ts
@@ -1618,20 +1908,44 @@ function convertHooksJson(hooksJson) {
1618
1908
  }
1619
1909
  return { hooks: result, warnings };
1620
1910
  }
1621
- async function mergeHooksIntoSettings(settingsPath, hooksJsonPath) {
1622
- const hooksJson = await readJsonFile(hooksJsonPath);
1623
- const { hooks, warnings } = convertHooksJson(hooksJson);
1624
- let settings = {};
1911
+ var UTF8_BOM2 = "\uFEFF";
1912
+ function isPlainObject(value) {
1913
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1914
+ }
1915
+ async function readExistingSettings(settingsPath) {
1916
+ let raw;
1625
1917
  try {
1626
- const existingRaw = await readTextFile(settingsPath);
1627
- settings = JSON.parse(existingRaw);
1918
+ raw = await readTextFile(settingsPath);
1628
1919
  } catch {
1629
- settings = {};
1920
+ return { ok: true, settings: {}, bom: false };
1921
+ }
1922
+ const bom = raw.startsWith(UTF8_BOM2);
1923
+ if ((bom ? raw.slice(UTF8_BOM2.length) : raw).trim() === "") {
1924
+ return { ok: true, settings: {}, bom };
1630
1925
  }
1631
- settings.hooks = hooks;
1926
+ try {
1927
+ const { data } = parseJsonText(raw);
1928
+ if (isPlainObject(data)) {
1929
+ return { ok: true, settings: data, bom };
1930
+ }
1931
+ return { ok: false, reason: "the top-level JSON value is not a JSON object", bom };
1932
+ } catch (err) {
1933
+ return { ok: false, reason: err instanceof Error ? err.message : String(err), bom };
1934
+ }
1935
+ }
1936
+ async function mergeHooksIntoSettings(settingsPath, hooksJsonPath, options = {}) {
1937
+ const hooksJson = await readJsonFile(hooksJsonPath);
1938
+ const { hooks, warnings } = convertHooksJson(hooksJson);
1939
+ const preserve = options.preserveUserHooks === true;
1940
+ const existing = await readExistingSettings(settingsPath);
1941
+ if (!existing.ok && preserve) {
1942
+ 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\``);
1943
+ return { warnings };
1944
+ }
1945
+ const settings = existing.ok ? existing.settings : {};
1946
+ settings.hooks = preserve ? mergeHookBlocks(settings.hooks, hooks) : hooks;
1632
1947
  await ensureDirectory(dirname2(settingsPath));
1633
- await writeTextFile(settingsPath, `${JSON.stringify(settings, null, 2)}
1634
- `);
1948
+ await writeTextFile(settingsPath, stringifyJson(settings, { bom: existing.bom, trailingNewline: true }));
1635
1949
  return { warnings };
1636
1950
  }
1637
1951
 
@@ -2008,6 +2322,25 @@ async function installStatusline(targetDir, options, _result) {
2008
2322
  await fs.chmod(destPath, 493);
2009
2323
  debug("install.statusline_installed", {});
2010
2324
  }
2325
+ async function installSchemas(targetDir, options, _result) {
2326
+ const layout = getProviderLayout();
2327
+ const relPath = join5(layout.rootDir, "schemas", "tool-inputs.json");
2328
+ const srcPath = resolveTemplatePath(relPath);
2329
+ const destPath = join5(targetDir, relPath);
2330
+ if (!await fileExists(srcPath)) {
2331
+ debug("install.schemas_not_found", { path: srcPath });
2332
+ return;
2333
+ }
2334
+ if (await fileExists(destPath)) {
2335
+ if (!options.force && !options.backup) {
2336
+ debug("install.schemas_skipped", { reason: "exists" });
2337
+ return;
2338
+ }
2339
+ }
2340
+ await ensureDirectory(dirname3(destPath));
2341
+ await copyFile(srcPath, destPath);
2342
+ debug("install.schemas_installed", {});
2343
+ }
2011
2344
  async function installTestsConfig(targetDir, options, _result) {
2012
2345
  const srcPath = resolveTemplatePath(join5("tests", "tsconfig.json"));
2013
2346
  const destPath = join5(targetDir, "tests", "tsconfig.json");
@@ -2024,34 +2357,55 @@ async function installTestsConfig(targetDir, options, _result) {
2024
2357
  await copyFile(srcPath, destPath);
2025
2358
  debug("install.tests_config_installed", {});
2026
2359
  }
2360
+ var STATUSLINE_REFRESH_INTERVAL = 10;
2361
+ function updateExistingStatusLine(existing) {
2362
+ const migrated = migrateHookCommands({ statusLine: existing.statusLine });
2363
+ const commandMigrated = migrated.rewritten > 0;
2364
+ if (commandMigrated) {
2365
+ existing.statusLine = migrated.settings.statusLine;
2366
+ }
2367
+ const sl = existing.statusLine;
2368
+ const refreshIntervalAdded = sl.refreshInterval === undefined;
2369
+ if (refreshIntervalAdded) {
2370
+ sl.refreshInterval = STATUSLINE_REFRESH_INTERVAL;
2371
+ }
2372
+ return { commandMigrated, refreshIntervalAdded };
2373
+ }
2374
+ async function mergeStatusLineIntoExisting(settingsPath, existing, format, defaults) {
2375
+ if (!existing.statusLine) {
2376
+ existing.statusLine = defaults.statusLine;
2377
+ await writeTextFile(settingsPath, stringifyJson(existing, format));
2378
+ debug("install.settings_local_merged", {});
2379
+ return;
2380
+ }
2381
+ const { commandMigrated, refreshIntervalAdded } = updateExistingStatusLine(existing);
2382
+ if (!commandMigrated && !refreshIntervalAdded) {
2383
+ debug("install.settings_local_skipped", { reason: "statusLine exists" });
2384
+ return;
2385
+ }
2386
+ await writeTextFile(settingsPath, stringifyJson(existing, format));
2387
+ if (commandMigrated) {
2388
+ debug("install.settings_local_statusline_migrated", {});
2389
+ }
2390
+ if (refreshIntervalAdded) {
2391
+ debug("install.settings_local_refreshInterval_added", {});
2392
+ }
2393
+ }
2027
2394
  async function installSettingsLocal(targetDir, result) {
2028
2395
  const layout = getProviderLayout();
2029
2396
  const settingsPath = join5(targetDir, layout.rootDir, "settings.local.json");
2030
2397
  const statusLineConfig = {
2031
2398
  statusLine: {
2032
2399
  type: "command",
2033
- command: ".claude/statusline.sh",
2400
+ command: NEW_STATUSLINE_COMMAND,
2034
2401
  padding: 0,
2035
- refreshInterval: 10
2402
+ refreshInterval: STATUSLINE_REFRESH_INTERVAL
2036
2403
  }
2037
2404
  };
2038
2405
  if (await fileExists(settingsPath)) {
2039
2406
  try {
2040
- const existing = await readJsonFile(settingsPath);
2041
- if (!existing.statusLine) {
2042
- existing.statusLine = statusLineConfig.statusLine;
2043
- await writeJsonFile(settingsPath, existing);
2044
- debug("install.settings_local_merged", {});
2045
- } else {
2046
- const sl = existing.statusLine;
2047
- if (sl.refreshInterval === undefined) {
2048
- sl.refreshInterval = statusLineConfig.statusLine.refreshInterval;
2049
- await writeJsonFile(settingsPath, existing);
2050
- debug("install.settings_local_refreshInterval_added", {});
2051
- } else {
2052
- debug("install.settings_local_skipped", { reason: "statusLine exists" });
2053
- }
2054
- }
2407
+ const { data: existing, format } = parseJsonText(await readTextFile(settingsPath));
2408
+ await mergeStatusLineIntoExisting(settingsPath, existing, format, statusLineConfig);
2055
2409
  } catch {
2056
2410
  result.warnings.push("Failed to parse existing settings.local.json, skipping statusLine config");
2057
2411
  }
@@ -2069,7 +2423,9 @@ async function installHooksSettings(targetDir, _options, result) {
2069
2423
  return;
2070
2424
  }
2071
2425
  try {
2072
- const mergeResult = await mergeHooksIntoSettings(settingsPath, hooksJsonPath);
2426
+ const mergeResult = await mergeHooksIntoSettings(settingsPath, hooksJsonPath, {
2427
+ preserveUserHooks: true
2428
+ });
2073
2429
  if (mergeResult && Array.isArray(mergeResult.warnings) && mergeResult.warnings.length > 0) {
2074
2430
  result.warnings.push(...mergeResult.warnings);
2075
2431
  }
@@ -2162,6 +2518,7 @@ async function install(options) {
2162
2518
  await verifyTemplateDirectory();
2163
2519
  await installAllComponents(options.targetDir, options, result);
2164
2520
  await installStatusline(options.targetDir, options, result);
2521
+ await installSchemas(options.targetDir, options, result);
2165
2522
  await installTestsConfig(options.targetDir, options, result);
2166
2523
  await installSettingsLocal(options.targetDir, result);
2167
2524
  await installHooksSettings(options.targetDir, options, result);
@@ -2407,7 +2764,7 @@ var package_default = {
2407
2764
  workspaces: [
2408
2765
  "packages/*"
2409
2766
  ],
2410
- version: "1.1.92",
2767
+ version: "1.1.94",
2411
2768
  description: "Batteries-included agent harness for Claude Code",
2412
2769
  type: "module",
2413
2770
  bin: {
@@ -5270,11 +5627,12 @@ async function migrateHookCommandsInSettingsLocal(targetDir, options) {
5270
5627
  });
5271
5628
  }
5272
5629
  }
5273
- async function runFullUpdatePostProcessing(options, result, config) {
5630
+ async function runFullUpdatePostProcessing(options, result, config, customizations) {
5274
5631
  const isFullUpdate = !options.components || options.components.length === 0;
5275
5632
  if (isFullUpdate) {
5276
- const synced = await syncRootLevelFiles(options.targetDir, options);
5633
+ const { synced, preserved } = await syncRootLevelFiles(options.targetDir, options, customizations);
5277
5634
  result.syncedRootFiles = synced;
5635
+ result.preservedFiles.push(...preserved);
5278
5636
  const removed = await removeDeprecatedFiles(options.targetDir, options);
5279
5637
  result.removedDeprecatedFiles = removed;
5280
5638
  if (!options.dryRun) {
@@ -5391,7 +5749,7 @@ async function update(options) {
5391
5749
  const lockfile = await readLockfile(options.targetDir);
5392
5750
  const components = options.components || getAllUpdateComponents();
5393
5751
  await updateAllComponents(options.targetDir, components, updateCheck, customizations, options, result, config, lockfile);
5394
- await runFullUpdatePostProcessing(options, result, config);
5752
+ await runFullUpdatePostProcessing(options, result, config, customizations);
5395
5753
  await regenerateLockfile(options.targetDir, result);
5396
5754
  checkAndInstallRtkAfterUpdate();
5397
5755
  checkAndInstallCodexAfterUpdate();
@@ -5603,19 +5961,31 @@ async function updateComponent(targetDir, component, customizations, options, co
5603
5961
  });
5604
5962
  return preservedFiles;
5605
5963
  }
5606
- var ROOT_LEVEL_FILES = ["statusline.sh", "install-hooks.sh", "uninstall-hooks.sh"];
5607
- async function syncRootLevelFiles(targetDir, options) {
5964
+ var ROOT_LEVEL_FILES = [
5965
+ "statusline.sh",
5966
+ "install-hooks.sh",
5967
+ "uninstall-hooks.sh",
5968
+ "schemas/tool-inputs.json"
5969
+ ];
5970
+ async function syncRootLevelFiles(targetDir, options, customizations) {
5608
5971
  if (options.dryRun) {
5609
- return ROOT_LEVEL_FILES;
5972
+ return { synced: ROOT_LEVEL_FILES, preserved: [] };
5610
5973
  }
5611
5974
  const fs = await import("node:fs/promises");
5612
5975
  const layout = getProviderLayout();
5613
5976
  const synced = [];
5977
+ const preserved = [];
5978
+ const preserveSet = new Set(customizations && !options.forceOverwriteAll ? customizations.preserveFiles : []);
5614
5979
  for (const fileName of ROOT_LEVEL_FILES) {
5615
5980
  const srcPath = resolveTemplatePath(join7(layout.rootDir, fileName));
5616
5981
  if (!await fileExists(srcPath)) {
5617
5982
  continue;
5618
5983
  }
5984
+ const relPath = `${layout.rootDir}/${fileName}`;
5985
+ if (preserveSet.has(relPath)) {
5986
+ preserved.push(relPath);
5987
+ continue;
5988
+ }
5619
5989
  const destPath = join7(targetDir, layout.rootDir, fileName);
5620
5990
  await ensureDirectory(join7(destPath, ".."));
5621
5991
  await fs.copyFile(srcPath, destPath);
@@ -5627,7 +5997,7 @@ async function syncRootLevelFiles(targetDir, options) {
5627
5997
  if (synced.length > 0) {
5628
5998
  debug("update.root_files_synced", { files: synced.join(", ") });
5629
5999
  }
5630
- return synced;
6000
+ return { synced, preserved };
5631
6001
  }
5632
6002
  async function removeDeprecatedFiles(targetDir, options) {
5633
6003
  const manifestPath = resolveTemplatePath("deprecated-files.json");