@handsupmin/gc-tree 1.1.1 → 1.1.2

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.
@@ -65,14 +65,15 @@ export async function removeManagedMarkdownBlock({ filePath, marker, }) {
65
65
  await writeFile(filePath, next, 'utf8');
66
66
  return 'removed';
67
67
  }
68
- function buildHookEntry(event) {
68
+ function buildHookEntry(event, target) {
69
69
  return {
70
70
  type: 'command',
71
71
  command: `gctree __hook --event ${event}`,
72
72
  metadata: {
73
73
  owner: 'gctree',
74
+ managed_by: '@handsupmin/gc-tree',
74
75
  },
75
- ...(event === 'UserPromptSubmit' ? { timeout: 10 } : {}),
76
+ ...(target === 'claude-code' || event === 'UserPromptSubmit' ? { timeout: 10 } : {}),
76
77
  };
77
78
  }
78
79
  function ensureObject(value) {
@@ -95,21 +96,29 @@ function mergeHookJson(raw, target) {
95
96
  };
96
97
  const upsertGroup = (event, matcher) => {
97
98
  const groups = ensureEventArray(event);
98
- let group = groups.find((candidate) => {
99
- const hooksArr = Array.isArray(candidate?.hooks) ? candidate.hooks : [];
100
- return hooksArr.some((entry) => isGcTreeHook(ensureObject(entry), event));
99
+ hooks[event] = groups
100
+ .map((candidate) => {
101
+ const group = ensureObject(candidate);
102
+ const hooksArr = Array.isArray(group.hooks) ? group.hooks : [];
103
+ const nextHooks = hooksArr.filter((entry) => !isGcTreeHook(ensureObject(entry), event));
104
+ return nextHooks.length > 0 ? { ...group, hooks: nextHooks } : null;
105
+ })
106
+ .filter(Boolean);
107
+ let group = hooks[event].find((candidate) => {
108
+ if (matcher === undefined)
109
+ return candidate.matcher === undefined;
110
+ return String(candidate.matcher ?? '') === matcher;
101
111
  });
102
112
  if (!group) {
103
- group = matcher ? { matcher, hooks: [] } : { hooks: [] };
104
- groups.push(group);
113
+ group = matcher !== undefined ? { matcher, hooks: [] } : { hooks: [] };
114
+ hooks[event].push(group);
105
115
  }
106
116
  if (!Array.isArray(group.hooks))
107
117
  group.hooks = [];
108
- group.hooks = group.hooks.filter((entry) => !isGcTreeHook(ensureObject(entry), event));
109
- group.hooks.push(buildHookEntry(event));
118
+ group.hooks.push(buildHookEntry(event, target));
110
119
  };
111
- upsertGroup(sessionEvent, target === 'codex' ? 'startup|resume' : '*');
112
- upsertGroup(promptEvent, target === 'codex' ? undefined : '*');
120
+ upsertGroup(sessionEvent, target === 'codex' ? 'startup|resume' : '');
121
+ upsertGroup(promptEvent, target === 'codex' ? undefined : '');
113
122
  return `${JSON.stringify({ ...parsed, hooks }, null, 2)}\n`;
114
123
  }
115
124
  function unmergeHookJson(raw, events) {
@@ -166,7 +175,7 @@ export function gctreeManagedMarkdownTargets(targetDir) {
166
175
  export function gctreeHookJsonTargets(targetDir) {
167
176
  return {
168
177
  codex: join(targetDir, '.codex', 'hooks.json'),
169
- claude: join(targetDir, '.claude', 'hooks', 'hooks.json'),
178
+ claude: join(targetDir, '.claude', 'settings.json'),
170
179
  };
171
180
  }
172
181
  export function gctreeGlobalRoot(host) {
@@ -177,5 +186,11 @@ export function gctreeGlobalRoot(host) {
177
186
  }
178
187
  export function gctreeGlobalHookJsonTarget(host) {
179
188
  const root = gctreeGlobalRoot(host);
180
- return host === 'codex' ? join(root, 'hooks.json') : join(root, 'hooks', 'hooks.json');
189
+ return host === 'codex' ? join(root, 'hooks.json') : join(root, 'settings.json');
190
+ }
191
+ export function gctreeLegacyClaudeHooksJsonTarget(targetDir) {
192
+ return join(targetDir, 'hooks', 'hooks.json');
193
+ }
194
+ export function gctreeLegacyLocalClaudeHooksJsonTarget(targetDir) {
195
+ return join(targetDir, '.claude', 'hooks', 'hooks.json');
181
196
  }
@@ -1,6 +1,6 @@
1
1
  import { access, mkdir, writeFile } from 'node:fs/promises';
2
2
  import { dirname, join } from 'node:path';
3
- import { gctreeGlobalHookJsonTarget, gctreeGlobalRoot, gctreeHookJsonTargets, gctreeManagedMarkdownTargets, mergeGcTreeHooksJson, unmergeGcTreeHooksJson, upsertManagedMarkdownBlock, } from './integration-files.js';
3
+ import { gctreeGlobalHookJsonTarget, gctreeGlobalRoot, gctreeHookJsonTargets, gctreeLegacyClaudeHooksJsonTarget, gctreeLegacyLocalClaudeHooksJsonTarget, gctreeManagedMarkdownTargets, mergeGcTreeHooksJson, unmergeGcTreeHooksJson, upsertManagedMarkdownBlock, } from './integration-files.js';
4
4
  import { onboardingCompletionLines, onboardingProtocolLines } from './onboarding-protocol.js';
5
5
  function renderCodexAgentsSnippet() {
6
6
  return [
@@ -181,11 +181,15 @@ function renderClaudeHooksJson() {
181
181
  hooks: {
182
182
  SessionStart: [
183
183
  {
184
- matcher: '*',
184
+ matcher: '',
185
185
  hooks: [
186
186
  {
187
187
  type: 'command',
188
188
  command: 'gctree __hook --event SessionStart',
189
+ metadata: {
190
+ owner: 'gctree',
191
+ managed_by: '@handsupmin/gc-tree',
192
+ },
189
193
  timeout: 10,
190
194
  },
191
195
  ],
@@ -193,11 +197,15 @@ function renderClaudeHooksJson() {
193
197
  ],
194
198
  UserPromptSubmit: [
195
199
  {
196
- matcher: '*',
200
+ matcher: '',
197
201
  hooks: [
198
202
  {
199
203
  type: 'command',
200
204
  command: 'gctree __hook --event UserPromptSubmit',
205
+ metadata: {
206
+ owner: 'gctree',
207
+ managed_by: '@handsupmin/gc-tree',
208
+ },
201
209
  timeout: 10,
202
210
  },
203
211
  ],
@@ -272,7 +280,7 @@ export function scaffoldFiles(host, scope = 'local') {
272
280
  }
273
281
  if (scope === 'global') {
274
282
  return [
275
- { path: 'hooks/hooks.json', content: renderClaudeHooksJson() },
283
+ { path: 'settings.json', content: renderClaudeHooksJson() },
276
284
  { path: 'hooks/gctree-session-start.md', content: renderClaudeSessionStartHook() },
277
285
  { path: 'commands/gc-resolve-context.md', content: renderClaudeResolveCommand() },
278
286
  { path: 'commands/gc-onboard.md', content: renderClaudeOnboardCommand() },
@@ -281,7 +289,7 @@ export function scaffoldFiles(host, scope = 'local') {
281
289
  }
282
290
  return [
283
291
  { path: 'CLAUDE.md', content: renderClaudeSnippet() },
284
- { path: '.claude/hooks/hooks.json', content: renderClaudeHooksJson() },
292
+ { path: '.claude/settings.json', content: renderClaudeHooksJson() },
285
293
  { path: '.claude/hooks/gctree-session-start.md', content: renderClaudeSessionStartHook() },
286
294
  { path: '.claude/commands/gc-resolve-context.md', content: renderClaudeResolveCommand() },
287
295
  { path: '.claude/commands/gc-onboard.md', content: renderClaudeOnboardCommand() },
@@ -309,15 +317,21 @@ export async function scaffoldHostIntegration({ host, targetDir, force = false,
309
317
  written.push(managedMarkdownPath);
310
318
  hookPath = isCodex ? hookTargets.codex : hookTargets.claude;
311
319
  if (!isCodex) {
312
- const oldHooksPath = join(resolvedTargetDir, '.claude', 'settings.json');
313
- await unmergeGcTreeHooksJson(oldHooksPath);
320
+ const legacyHooksPath = gctreeLegacyLocalClaudeHooksJsonTarget(resolvedTargetDir);
321
+ const legacyStatus = await unmergeGcTreeHooksJson(legacyHooksPath);
322
+ if (legacyStatus !== 'missing') {
323
+ written.push(legacyHooksPath);
324
+ }
314
325
  }
315
326
  }
316
327
  else {
317
328
  hookPath = gctreeGlobalHookJsonTarget(host);
318
329
  if (!isCodex) {
319
- const oldHooksPath = join(resolvedTargetDir, 'settings.json');
320
- await unmergeGcTreeHooksJson(oldHooksPath);
330
+ const legacyHooksPath = gctreeLegacyClaudeHooksJsonTarget(resolvedTargetDir);
331
+ const legacyStatus = await unmergeGcTreeHooksJson(legacyHooksPath);
332
+ if (legacyStatus !== 'missing') {
333
+ written.push(legacyHooksPath);
334
+ }
321
335
  }
322
336
  }
323
337
  if (scope === 'local') {
@@ -1,6 +1,6 @@
1
1
  import { readdir, rm } from 'node:fs/promises';
2
2
  import { dirname, join } from 'node:path';
3
- import { gctreeGlobalHookJsonTarget, gctreeGlobalRoot, gctreeHookJsonTargets, gctreeManagedMarkdownTargets, pathExists, removeManagedMarkdownBlock, unmergeGcTreeHooksJson, } from './integration-files.js';
3
+ import { gctreeGlobalHookJsonTarget, gctreeGlobalRoot, gctreeHookJsonTargets, gctreeLegacyClaudeHooksJsonTarget, gctreeLegacyLocalClaudeHooksJsonTarget, gctreeManagedMarkdownTargets, pathExists, removeManagedMarkdownBlock, unmergeGcTreeHooksJson, } from './integration-files.js';
4
4
  import { scaffoldFiles } from './scaffold.js';
5
5
  async function pathHasEntries(path) {
6
6
  const entries = await readdir(path).catch(() => []);
@@ -26,6 +26,14 @@ async function uninstallHostScaffold({ host, scope, targetDir, removed, }) {
26
26
  const hookPath = scope === 'local'
27
27
  ? (isCodex ? gctreeHookJsonTargets(targetDir).codex : gctreeHookJsonTargets(targetDir).claude)
28
28
  : gctreeGlobalHookJsonTarget(host);
29
+ const hookPaths = isCodex
30
+ ? [hookPath]
31
+ : [
32
+ hookPath,
33
+ scope === 'local'
34
+ ? gctreeLegacyLocalClaudeHooksJsonTarget(targetDir)
35
+ : gctreeLegacyClaudeHooksJsonTarget(targetDir),
36
+ ];
29
37
  if (managedMarkdownPath) {
30
38
  const markdownStatus = await removeManagedMarkdownBlock({
31
39
  filePath: managedMarkdownPath,
@@ -36,14 +44,16 @@ async function uninstallHostScaffold({ host, scope, targetDir, removed, }) {
36
44
  await pruneEmptyParents(managedMarkdownPath, targetDir);
37
45
  }
38
46
  }
39
- const hookStatus = await unmergeGcTreeHooksJson(hookPath);
40
- if (hookStatus !== 'missing') {
41
- removed.push(hookPath);
42
- await pruneEmptyParents(hookPath, targetDir);
47
+ for (const candidate of [...new Set(hookPaths)]) {
48
+ const hookStatus = await unmergeGcTreeHooksJson(candidate);
49
+ if (hookStatus !== 'missing') {
50
+ removed.push(candidate);
51
+ await pruneEmptyParents(candidate, targetDir);
52
+ }
43
53
  }
44
54
  const extraTargets = scaffoldFiles(host, scope)
45
55
  .map((file) => join(targetDir, file.path))
46
- .filter((path) => path !== hookPath && path !== managedMarkdownPath);
56
+ .filter((path) => !hookPaths.includes(path) && path !== managedMarkdownPath);
47
57
  for (const target of [...new Set(extraTargets)]) {
48
58
  if (!(await pathExists(target)))
49
59
  continue;
package/docs/usage.es.md CHANGED
@@ -181,7 +181,7 @@ AGENTS.md ← fragmento de gctree añadido a la
181
181
  **Archivos globales de Claude Code (`gctree init`):**
182
182
 
183
183
  ```
184
- ~/.claude/hooks/hooks.json ← hooks de auto-resolve para SessionStart/UserPromptSubmit
184
+ ~/.claude/settings.json bloque de hooks de auto-resolve para SessionStart/UserPromptSubmit
185
185
  ~/.claude/hooks/gctree-session-start.md ← nota fallback de inicio de sesión
186
186
  ~/.claude/commands/gc-resolve-context.md ← comando slash de resolve
187
187
  ~/.claude/commands/gc-onboard.md ← comando slash de onboard
@@ -190,9 +190,10 @@ AGENTS.md ← fragmento de gctree añadido a la
190
190
 
191
191
  **Archivos de override local para `gctree scaffold --host claude-code`:**
192
192
 
193
+ Los hooks de Claude Code permanecen en el `~/.claude/settings.json` global; el scaffold local no escribe un archivo de hooks duplicado.
194
+
193
195
  ```
194
196
  CLAUDE.md ← fragmento de gctree añadido
195
- .claude/hooks/hooks.json ← hooks de auto-resolve para SessionStart/UserPromptSubmit
196
197
  .claude/hooks/gctree-session-start.md ← nota fallback de inicio de sesión
197
198
  .claude/commands/gc-resolve-context.md ← comando slash de resolve
198
199
  .claude/commands/gc-onboard.md ← comando slash de onboard
package/docs/usage.ja.md CHANGED
@@ -181,7 +181,7 @@ AGENTS.md ← gctree のスニペットがエ
181
181
  **Claude Code のグローバルファイル(`gctree init`):**
182
182
 
183
183
  ```
184
- ~/.claude/hooks/hooks.json ← SessionStart / UserPromptSubmit の自動 resolve フック
184
+ ~/.claude/settings.json ← SessionStart / UserPromptSubmit の自動 resolve フックブロック
185
185
  ~/.claude/hooks/gctree-session-start.md ← セッション開始のフォールバックメモ
186
186
  ~/.claude/commands/gc-resolve-context.md ← resolve スラッシュコマンド
187
187
  ~/.claude/commands/gc-onboard.md ← onboard スラッシュコマンド
@@ -190,9 +190,10 @@ AGENTS.md ← gctree のスニペットがエ
190
190
 
191
191
  **`gctree scaffold --host claude-code` のローカル override ファイル:**
192
192
 
193
+ Claude Code のフックはグローバルの `~/.claude/settings.json` に残ります。ローカル scaffold は重複するフックファイルを書きません。
194
+
193
195
  ```
194
196
  CLAUDE.md ← gctree のスニペットが追記される
195
- .claude/hooks/hooks.json ← SessionStart / UserPromptSubmit の自動 resolve フック
196
197
  .claude/hooks/gctree-session-start.md ← セッション開始のフォールバックメモ
197
198
  .claude/commands/gc-resolve-context.md ← resolve スラッシュコマンド
198
199
  .claude/commands/gc-onboard.md ← onboard スラッシュコマンド
package/docs/usage.ko.md CHANGED
@@ -181,7 +181,7 @@ AGENTS.md ← 에이전트 지시사항에 gctr
181
181
  **Claude Code 전역 파일 (`gctree init`):**
182
182
 
183
183
  ```
184
- ~/.claude/hooks/hooks.json ← SessionStart/UserPromptSubmit 자동 resolve 훅
184
+ ~/.claude/settings.json ← SessionStart/UserPromptSubmit 자동 resolve 훅 블록
185
185
  ~/.claude/hooks/gctree-session-start.md ← 세션 시작 fallback 메모
186
186
  ~/.claude/commands/gc-resolve-context.md ← resolve 슬래시 명령
187
187
  ~/.claude/commands/gc-onboard.md ← onboard 슬래시 명령
@@ -190,9 +190,10 @@ AGENTS.md ← 에이전트 지시사항에 gctr
190
190
 
191
191
  **`gctree scaffold --host claude-code` 로컬 override 파일:**
192
192
 
193
+ Claude Code 훅은 전역 `~/.claude/settings.json`에 유지됩니다. 로컬 scaffold는 중복 훅 파일을 쓰지 않습니다.
194
+
193
195
  ```
194
196
  CLAUDE.md ← gctree 스니펫 추가
195
- .claude/hooks/hooks.json ← SessionStart/UserPromptSubmit 자동 resolve 훅
196
197
  .claude/hooks/gctree-session-start.md ← 세션 시작 fallback 메모
197
198
  .claude/commands/gc-resolve-context.md ← resolve 슬래시 명령
198
199
  .claude/commands/gc-onboard.md ← onboard 슬래시 명령
package/docs/usage.md CHANGED
@@ -181,7 +181,7 @@ AGENTS.md ← gctree snippet appended to agent
181
181
  **Global files for Claude Code (`gctree init`):**
182
182
 
183
183
  ```
184
- ~/.claude/hooks/hooks.json ← SessionStart/UserPromptSubmit auto-resolve hooks
184
+ ~/.claude/settings.json ← SessionStart/UserPromptSubmit auto-resolve hooks block
185
185
  ~/.claude/hooks/gctree-session-start.md ← session-start fallback note
186
186
  ~/.claude/commands/gc-resolve-context.md ← resolve slash command
187
187
  ~/.claude/commands/gc-onboard.md ← onboard slash command
@@ -190,9 +190,10 @@ AGENTS.md ← gctree snippet appended to agent
190
190
 
191
191
  **Local override files for `gctree scaffold --host claude-code`:**
192
192
 
193
+ Claude Code hooks remain in the global `~/.claude/settings.json`; local scaffolding does not write a duplicate hook file.
194
+
193
195
  ```
194
196
  CLAUDE.md ← gctree snippet appended
195
- .claude/hooks/hooks.json ← SessionStart/UserPromptSubmit auto-resolve hooks
196
197
  .claude/hooks/gctree-session-start.md ← session-start fallback note
197
198
  .claude/commands/gc-resolve-context.md ← resolve slash command
198
199
  .claude/commands/gc-onboard.md ← onboard slash command
package/docs/usage.zh.md CHANGED
@@ -181,7 +181,7 @@ AGENTS.md ← gctree 代码片段追加到 agen
181
181
  **Claude Code 全局文件(`gctree init`):**
182
182
 
183
183
  ```
184
- ~/.claude/hooks/hooks.json ← SessionStart / UserPromptSubmit 自动 resolve hook
184
+ ~/.claude/settings.json ← SessionStart / UserPromptSubmit 自动 resolve hook
185
185
  ~/.claude/hooks/gctree-session-start.md ← 会话启动 fallback 说明
186
186
  ~/.claude/commands/gc-resolve-context.md ← resolve 斜杠命令
187
187
  ~/.claude/commands/gc-onboard.md ← onboard 斜杠命令
@@ -190,9 +190,10 @@ AGENTS.md ← gctree 代码片段追加到 agen
190
190
 
191
191
  **`gctree scaffold --host claude-code` 的本地 override 文件:**
192
192
 
193
+ Claude Code hook 保留在全局 `~/.claude/settings.json` 中;本地 scaffold 不写入重复的 hook 文件。
194
+
193
195
  ```
194
196
  CLAUDE.md ← gctree 代码片段追加
195
- .claude/hooks/hooks.json ← SessionStart / UserPromptSubmit 自动 resolve hook
196
197
  .claude/hooks/gctree-session-start.md ← 会话启动 fallback 说明
197
198
  .claude/commands/gc-resolve-context.md ← resolve 斜杠命令
198
199
  .claude/commands/gc-onboard.md ← onboard 斜杠命令
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@handsupmin/gc-tree",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "description": "Global Context Tree, a lightweight branch-aware global context orchestrator for AI coding tools",
5
5
  "type": "module",
6
6
  "private": false,