oh-my-customcode 1.1.93 → 1.1.95

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