@bbyx0011/ghostcode 0.1.0

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.
Files changed (42) hide show
  1. package/bin/ghostcode-mcp +11 -0
  2. package/bin/ghostcode-mcp-darwin-arm64 +0 -0
  3. package/bin/ghostcode-mcp-darwin-x64 +0 -0
  4. package/bin/ghostcode-mcp-linux-x64 +0 -0
  5. package/bin/ghostcode-wrapper +11 -0
  6. package/bin/ghostcode-wrapper-darwin-arm64 +0 -0
  7. package/bin/ghostcode-wrapper-darwin-x64 +0 -0
  8. package/bin/ghostcode-wrapper-linux-x64 +0 -0
  9. package/bin/ghostcoded-darwin-arm64 +0 -0
  10. package/bin/ghostcoded-darwin-x64 +0 -0
  11. package/bin/ghostcoded-linux-x64 +0 -0
  12. package/dist/cli.d.ts +21 -0
  13. package/dist/cli.js +97 -0
  14. package/dist/daemon.d.ts +72 -0
  15. package/dist/index.d.ts +31 -0
  16. package/dist/index.js +35 -0
  17. package/dist/install.d.ts +61 -0
  18. package/dist/ipc.d.ts +121 -0
  19. package/dist/postinstall.d.ts +42 -0
  20. package/dist/session-lease.d.ts +110 -0
  21. package/dist/web.d.ts +51 -0
  22. package/hooks/hooks.json +101 -0
  23. package/package.json +29 -0
  24. package/prompts/codex-analyzer.md +64 -0
  25. package/prompts/codex-reviewer.md +73 -0
  26. package/prompts/gemini-analyzer.md +82 -0
  27. package/prompts/gemini-reviewer.md +91 -0
  28. package/scripts/hook-pre-compact.mjs +128 -0
  29. package/scripts/hook-pre-tool-use.mjs +225 -0
  30. package/scripts/hook-session-end.mjs +108 -0
  31. package/scripts/hook-session-start.mjs +243 -0
  32. package/scripts/hook-stop.mjs +143 -0
  33. package/scripts/hook-subagent-start.mjs +231 -0
  34. package/scripts/hook-subagent-stop.mjs +168 -0
  35. package/scripts/hook-user-prompt-submit.mjs +134 -0
  36. package/scripts/lib/daemon-client.mjs +165 -0
  37. package/scripts/lib/stdin.mjs +88 -0
  38. package/scripts/run.mjs +98 -0
  39. package/skills/execute/SKILL.md +481 -0
  40. package/skills/plan/SKILL.md +318 -0
  41. package/skills/research/SKILL.md +267 -0
  42. package/skills/review/SKILL.md +238 -0
@@ -0,0 +1,225 @@
1
+ /**
2
+ * @file scripts/hook-pre-tool-use.mjs
3
+ * @description PreToolUse Hook 脚本
4
+ * 在每次工具调用前确保 GhostCode Daemon 已启动并获取 Session Lease。
5
+ *
6
+ * 与原 handlers.ts 的关键区别:
7
+ * - 每次调用是独立进程,使用文件状态替代内存变量
8
+ * - 不启动心跳(进程执行完即退出,心跳无意义)
9
+ * - 通过状态文件实现跨调用的幂等性
10
+ *
11
+ * 参考: src/plugin/src/hooks/handlers.ts - preToolUseHandler
12
+ * @author Atlas.oi
13
+ * @date 2026-03-05
14
+ */
15
+
16
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
17
+ import { join, dirname } from "node:path";
18
+ import { homedir } from "node:os";
19
+
20
+ // ============================================
21
+ // 常量:路径配置
22
+ // ============================================
23
+
24
+ // GhostCode 主目录,优先使用环境变量覆盖
25
+ const GHOSTCODE_HOME = process.env.GHOSTCODE_HOME || join(homedir(), ".ghostcode");
26
+
27
+ // Hook 状态文件:记录 Daemon 启动状态和 Lease 信息
28
+ // 所有 Hook 脚本共享此文件,实现跨进程状态传递
29
+ const STATE_FILE = join(GHOSTCODE_HOME, "state", "hook-state.json");
30
+
31
+ // Plugin 根目录(由 run.mjs 注入或从脚本路径推导)
32
+ const PLUGIN_ROOT = process.env.CLAUDE_PLUGIN_ROOT || join(dirname(new URL(import.meta.url).pathname), "..");
33
+
34
+ // ============================================
35
+ // 状态文件读写工具函数
36
+ // ============================================
37
+
38
+ /**
39
+ * 读取 Hook 状态文件
40
+ *
41
+ * 业务逻辑:
42
+ * 1. 检查文件是否存在
43
+ * 2. 解析 JSON 内容
44
+ * 3. 文件不存在或解析失败时返回默认空状态
45
+ *
46
+ * @returns {{ daemonStarted: boolean, socketPath: string|null, leaseId: string|null }}
47
+ */
48
+ function readState() {
49
+ try {
50
+ if (existsSync(STATE_FILE)) {
51
+ return JSON.parse(readFileSync(STATE_FILE, "utf-8"));
52
+ }
53
+ } catch {
54
+ // 状态文件解析失败时,返回默认状态重新初始化
55
+ }
56
+ return { daemonStarted: false, socketPath: null, leaseId: null, webStarted: false };
57
+ }
58
+
59
+ /**
60
+ * 写入 Hook 状态文件
61
+ *
62
+ * 业务逻辑:
63
+ * 1. 确保父目录存在(首次运行时创建)
64
+ * 2. 将状态序列化为 JSON 写入文件
65
+ *
66
+ * @param {{ daemonStarted: boolean, socketPath: string|null, leaseId: string|null }} state - 要持久化的状态
67
+ */
68
+ function writeState(state) {
69
+ // 确保状态目录存在,首次运行时自动创建
70
+ const dir = dirname(STATE_FILE);
71
+ mkdirSync(dir, { recursive: true });
72
+ writeFileSync(STATE_FILE, JSON.stringify(state, null, 2), "utf-8");
73
+ }
74
+
75
+ /**
76
+ * 检测指定 PID 的进程是否存活
77
+ *
78
+ * 使用 kill -0 信号探测进程,不发送实际信号
79
+ *
80
+ * @param {number} pid - 目标进程 PID
81
+ * @returns {boolean} 进程存活返回 true,否则返回 false
82
+ */
83
+ function isProcessAlive(pid) {
84
+ try {
85
+ process.kill(pid, 0);
86
+ return true;
87
+ } catch {
88
+ // 进程不存在或无权限访问,均视为不存活
89
+ return false;
90
+ }
91
+ }
92
+
93
+ // ============================================
94
+ // 主逻辑
95
+ // ============================================
96
+
97
+ /**
98
+ * PreToolUse Hook 主函数
99
+ *
100
+ * 业务逻辑说明:
101
+ * 1. 读取当前状态文件,获取 Daemon 启动状态
102
+ * 2. 如果 Daemon 已启动,验证进程是否仍在运行(幂等保护)
103
+ * 3. 如果 Daemon 不存在或进程已退出,重新启动 Daemon
104
+ * 4. 获取 Session Lease(如果尚未持有)
105
+ * 5. 将最新状态写回状态文件
106
+ */
107
+ async function main() {
108
+ // ============================================
109
+ // 第一步:读取当前状态
110
+ // ============================================
111
+ const state = readState();
112
+
113
+ // ============================================
114
+ // 第二步:检查 Daemon 是否已启动(幂等保护)
115
+ // 如果状态文件记录 Daemon 已启动,通过 addr.json 验证进程是否仍然存活
116
+ // ============================================
117
+ if (state.daemonStarted && state.socketPath) {
118
+ // 读取 addr.json 获取 Daemon 进程信息
119
+ const addrPath = join(GHOSTCODE_HOME, "daemon", "ghostcoded.addr.json");
120
+ let daemonAlive = false;
121
+ try {
122
+ if (existsSync(addrPath)) {
123
+ const addr = JSON.parse(readFileSync(addrPath, "utf-8"));
124
+ if (addr.pid && isProcessAlive(addr.pid)) {
125
+ daemonAlive = true;
126
+ }
127
+ }
128
+ } catch {
129
+ // addr.json 读取失败,说明 Daemon 状态异常,需要重新启动
130
+ }
131
+
132
+ if (daemonAlive) {
133
+ // Daemon 仍在运行,但仍需确保 Web Dashboard 也在运行
134
+ // Bug 修复:之前直接 return 导致 ensureWeb 永远不会被调用
135
+ if (!state.webStarted) {
136
+ try {
137
+ const { ensureWeb } = await import(join(PLUGIN_ROOT, "dist", "web.js"));
138
+ await ensureWeb();
139
+ state.webStarted = true;
140
+ writeState(state);
141
+ } catch (err) {
142
+ // Dashboard 启动失败不阻断工具调用
143
+ console.error("[GhostCode] Dashboard 自动启动失败:", err);
144
+ }
145
+ }
146
+ return;
147
+ }
148
+
149
+ // Daemon 进程不再存活,重置状态准备重新启动
150
+ state.daemonStarted = false;
151
+ state.socketPath = null;
152
+ state.leaseId = null;
153
+ state.webStarted = false;
154
+ }
155
+
156
+ // ============================================
157
+ // 第三步:启动 Daemon
158
+ // 动态导入编译产物,支持 CLAUDE_PLUGIN_ROOT 环境变量覆盖
159
+ // ============================================
160
+ try {
161
+ const { ensureDaemon } = await import(join(PLUGIN_ROOT, "dist", "daemon.js"));
162
+ const addr = await ensureDaemon();
163
+
164
+ // Daemon 启动成功,更新状态
165
+ state.daemonStarted = true;
166
+ state.socketPath = addr.path;
167
+
168
+ // ============================================
169
+ // 第四步:获取 Session Lease(如果尚未持有)
170
+ // Lease 标识当前 Claude Code 会话,用于多 Agent 协作管理
171
+ // ============================================
172
+ if (!state.leaseId) {
173
+ try {
174
+ const { SessionLeaseManager } = await import(join(PLUGIN_ROOT, "dist", "session-lease.js"));
175
+ // sessions.json 记录所有活跃会话的 Lease 信息
176
+ const sessionsPath = join(GHOSTCODE_HOME, "daemon", "sessions.json");
177
+ const leaseManager = new SessionLeaseManager(sessionsPath);
178
+ const lease = leaseManager.acquireLease();
179
+ state.leaseId = lease.leaseId;
180
+ } catch (err) {
181
+ // Lease 获取失败不阻断流程,仅记录错误
182
+ // 没有 Lease 仍可正常使用,只是部分协作功能受限
183
+ console.error("[GhostCode] Session lease 获取失败:", err);
184
+ }
185
+ }
186
+
187
+ // ============================================
188
+ // 第五步:启动 Dashboard Web 服务(单实例保证)
189
+ // ensureWeb() 内部检查 ghostcode-web 是否已运行,
190
+ // 已运行则跳过,未运行则自动启动并等待健康检查通过
191
+ // ============================================
192
+ try {
193
+ const { ensureWeb } = await import(join(PLUGIN_ROOT, "dist", "web.js"));
194
+ await ensureWeb();
195
+ state.webStarted = true;
196
+ } catch (err) {
197
+ // Dashboard 启动失败不阻断工具调用
198
+ // 用户仍可通过 /gc-web 命令手动启动
199
+ console.error("[GhostCode] Dashboard 自动启动失败:", err);
200
+ }
201
+
202
+ // ============================================
203
+ // 第六步:将最新状态写回文件
204
+ // 下次 Hook 调用时可从此文件读取状态,实现跨进程幂等性
205
+ // ============================================
206
+ writeState(state);
207
+ } catch (err) {
208
+ // Daemon 启动失败,记录错误但不阻断工具调用
209
+ // 遵循 exit 0 策略:GhostCode 的问题不应影响用户正常使用 Claude Code
210
+ console.error("[GhostCode] Daemon 启动失败,工具调用将继续但无协作功能:", err);
211
+
212
+ // 重置状态文件,下次 Hook 调用时会重试启动
213
+ writeState({ daemonStarted: false, socketPath: null, leaseId: null, webStarted: false });
214
+ }
215
+ }
216
+
217
+ // ============================================
218
+ // 入口:执行主逻辑
219
+ // 任何未捕获的异常都 exit 0,绝不阻断 Claude Code 的工具调用
220
+ // ============================================
221
+ main().catch((err) => {
222
+ console.error("[GhostCode] hook-pre-tool-use 异常:", err);
223
+ // exit 0 策略:即使 Hook 完全失败,也不影响工具调用继续执行
224
+ process.exit(0);
225
+ });
@@ -0,0 +1,108 @@
1
+ /**
2
+ * @file scripts/hook-session-end.mjs
3
+ * @description SessionEnd Hook 脚本
4
+ * 在 Claude Code 会话正常结束时执行清理操作:
5
+ * 1. 触发最终 Skill Learning 汇总(调用 learner 模块的 onSessionEnd)
6
+ * 2. 清理 ~/.ghostcode/state/ 下的状态文件(重置会话状态)
7
+ *
8
+ * 与 Stop Hook 的分工:
9
+ * - Stop: 在 Claude Code 输出停止时触发,负责释放 Lease + 关闭 Daemon
10
+ * - SessionEnd: 在整个会话结束时触发,负责最终汇总 + 状态清理
11
+ *
12
+ * 注意:失败时 exit 0,不阻断 Claude Code 会话正常关闭
13
+ * @author Atlas.oi
14
+ * @date 2026-03-05
15
+ */
16
+
17
+ import { existsSync, readdirSync, unlinkSync, mkdirSync } from "node:fs";
18
+ import { join, dirname } from "node:path";
19
+ import { homedir } from "node:os";
20
+
21
+ // ============================================
22
+ // 常量配置
23
+ // ============================================
24
+
25
+ // GhostCode 主目录,支持环境变量覆盖(主要用于测试隔离)
26
+ const GHOSTCODE_HOME = process.env.GHOSTCODE_HOME || join(homedir(), ".ghostcode");
27
+
28
+ // 状态目录路径
29
+ const STATE_DIR = join(GHOSTCODE_HOME, "state");
30
+
31
+ // Plugin 根目录(用于动态 import learner 模块)
32
+ const PLUGIN_ROOT = process.env.CLAUDE_PLUGIN_ROOT || join(dirname(new URL(import.meta.url).pathname), "..");
33
+
34
+ // ============================================
35
+ // 状态清理工具函数
36
+ // ============================================
37
+
38
+ /**
39
+ * 清理 state 目录下的所有状态文件
40
+ *
41
+ * 业务逻辑说明:
42
+ * 1. 检查 state 目录是否存在
43
+ * 2. 遍历目录下所有 .json 文件并删除
44
+ * 3. 失败时静默处理,不阻断主流程
45
+ */
46
+ function clearStateFiles() {
47
+ try {
48
+ if (!existsSync(STATE_DIR)) {
49
+ return;
50
+ }
51
+ const files = readdirSync(STATE_DIR);
52
+ for (const file of files) {
53
+ if (file.endsWith(".json")) {
54
+ try {
55
+ unlinkSync(join(STATE_DIR, file));
56
+ } catch {
57
+ // 单个文件删除失败不影响其他文件
58
+ }
59
+ }
60
+ }
61
+ } catch {
62
+ // state 目录读取失败,静默处理
63
+ }
64
+ }
65
+
66
+ // ============================================
67
+ // 主逻辑
68
+ // ============================================
69
+
70
+ /**
71
+ * SessionEnd Hook 主函数
72
+ *
73
+ * 业务逻辑说明:
74
+ * 1. 触发 Skill Learning 最终汇总(onSessionEnd)
75
+ * - 调用成功:输出汇总日志
76
+ * - 调用失败:静默处理,继续执行清理
77
+ * 2. 清理 state 目录下的所有状态文件
78
+ */
79
+ async function main() {
80
+ // ============================================
81
+ // 第一步:触发 Skill Learning 最终汇总
82
+ // 确保会话结束时所有学习内容被持久化
83
+ // ============================================
84
+ try {
85
+ const { onSessionEnd } = await import(join(PLUGIN_ROOT, "dist", "learner", "index.js"));
86
+ await onSessionEnd();
87
+ console.log("[GhostCode] SessionEnd: Skill Learning 汇总完成");
88
+ } catch {
89
+ // learner 模块可能未构建(开发环境),静默处理
90
+ }
91
+
92
+ // ============================================
93
+ // 第二步:清理状态文件
94
+ // 会话已完全结束,重置所有 Hook 状态
95
+ // ============================================
96
+ clearStateFiles();
97
+ console.log("[GhostCode] SessionEnd: 状态文件清理完成");
98
+ }
99
+
100
+ // ============================================
101
+ // 入口:执行主逻辑
102
+ // exit 0 策略:SessionEnd 失败不应阻断 Claude Code 会话关闭
103
+ // ============================================
104
+ main().catch((err) => {
105
+ console.error("[GhostCode] hook-session-end 异常:", err);
106
+ clearStateFiles();
107
+ process.exit(0);
108
+ });
@@ -0,0 +1,243 @@
1
+ /**
2
+ * @file scripts/hook-session-start.mjs
3
+ * @description SessionStart Hook 脚本
4
+ * 在 Claude Code 会话启动时执行初始化操作:
5
+ * 1. 确保状态目录存在(首次运行时自动创建)
6
+ * 2. 幂等性:如果状态文件已存在(其他 Hook 已初始化),跳过状态文件写入
7
+ * 3. 自动安装 wrapper + prompts(从 Plugin 包 symlink 到 ~/.ghostcode/)
8
+ * 4. 输出 GhostCode Plugin 启动信息(版本 + skill 数量 + Daemon 状态)
9
+ *
10
+ * 与 PreToolUse 的分工:
11
+ * - SessionStart: 输出欢迎信息,创建目录,标记会话开始
12
+ * - PreToolUse: 启动 Daemon,获取 Session Lease
13
+ *
14
+ * 幂等保证:
15
+ * - 每次 SessionStart 都输出初始化消息(用于用户感知)
16
+ * - 状态文件已存在时跳过写入,避免覆盖 PreToolUse 写入的 Daemon 状态
17
+ * @author Atlas.oi
18
+ * @date 2026-03-05
19
+ */
20
+
21
+ import { existsSync, mkdirSync, writeFileSync, symlinkSync, readlinkSync, unlinkSync, readdirSync } from "node:fs";
22
+ import { join, dirname } from "node:path";
23
+ import { homedir } from "node:os";
24
+ import { fileURLToPath } from "node:url";
25
+
26
+ // ============================================
27
+ // 常量配置
28
+ // ============================================
29
+
30
+ // GhostCode 主目录,支持环境变量覆盖(主要用于测试隔离)
31
+ const GHOSTCODE_HOME = process.env.GHOSTCODE_HOME || join(homedir(), ".ghostcode");
32
+
33
+ // Plugin 版本号(与 package.json 保持一致)
34
+ const PLUGIN_VERSION = "0.1.0";
35
+
36
+ // Hook 状态文件路径(与 hook-pre-tool-use.mjs 和 hook-stop.mjs 共享同一路径)
37
+ const STATE_FILE = join(GHOSTCODE_HOME, "state", "hook-state.json");
38
+
39
+ // ============================================
40
+ // Skill 列表:用于计算 skill 数量
41
+ // 与 hooks.json 中原 echo 命令的 skill 列表保持一致
42
+ // ============================================
43
+ const SKILLS = [
44
+ "/gc:team-research",
45
+ "/gc:team-plan",
46
+ "/gc:team-exec",
47
+ "/gc:team-review",
48
+ "/gc:spec-research",
49
+ "/gc:spec-plan",
50
+ "/gc:spec-impl",
51
+ ];
52
+
53
+ // ============================================
54
+ // 主逻辑
55
+ // ============================================
56
+
57
+ /**
58
+ * SessionStart Hook 主函数
59
+ *
60
+ * 业务逻辑说明:
61
+ * 1. 确保 GhostCode 状态目录存在(首次安装时创建)
62
+ * 2. 如果状态文件不存在,创建初始空状态(daemonStarted: false)
63
+ * - 如果状态文件已存在,说明 PreToolUse 已写入 Daemon 状态,跳过写入(幂等保护)
64
+ * 3. 输出初始化消息:版本号 + skill 数量 + Daemon 状态
65
+ */
66
+ function main() {
67
+ // ============================================
68
+ // 第一步:确保状态目录存在
69
+ // 首次运行时自动创建 ~/.ghostcode/state/ 目录
70
+ // ============================================
71
+ const stateDir = join(GHOSTCODE_HOME, "state");
72
+ mkdirSync(stateDir, { recursive: true });
73
+
74
+ // ============================================
75
+ // 第二步:幂等性检查 + 初始状态文件创建
76
+ // 仅在状态文件不存在时写入初始状态
77
+ // 避免覆盖 PreToolUse 已写入的 Daemon 启动状态
78
+ // ============================================
79
+ if (!existsSync(STATE_FILE)) {
80
+ // 状态文件不存在,创建初始空状态
81
+ // daemonStarted: false 表示 Daemon 尚未启动(由 PreToolUse 负责启动)
82
+ const initialState = {
83
+ daemonStarted: false,
84
+ socketPath: null,
85
+ leaseId: null,
86
+ };
87
+ writeFileSync(STATE_FILE, JSON.stringify(initialState, null, 2), "utf-8");
88
+ }
89
+
90
+ // ============================================
91
+ // 第三步:自动安装 wrapper + prompts
92
+ // 从 Plugin 包 symlink 到 ~/.ghostcode/,Plugin 更新时自动生效
93
+ // ============================================
94
+ setupWrapperAndPrompts();
95
+
96
+ // ============================================
97
+ // 第四步:输出初始化消息
98
+ // 每次 SessionStart 都输出,让用户感知 GhostCode 已加载
99
+ // 格式:[GhostCode] Plugin vX.Y.Z | N skills loaded | Daemon: pending
100
+ // ============================================
101
+ const skillCount = SKILLS.length;
102
+ // Daemon 状态固定显示 pending:实际启动由 PreToolUse 负责,此时尚未启动
103
+ const daemonStatus = "pending";
104
+ console.log(`[GhostCode] Plugin v${PLUGIN_VERSION} | ${skillCount} skills loaded | Daemon: ${daemonStatus}`);
105
+ }
106
+
107
+ // ============================================
108
+ // setupWrapperAndPrompts 函数
109
+ // 从 Plugin 包自动 symlink wrapper 和 prompts 到 ~/.ghostcode/
110
+ //
111
+ // 业务逻辑:
112
+ // 1. 通过 CLAUDE_PLUGIN_ROOT 或脚本路径推算 Plugin 包根目录
113
+ // 2. 将 bin/ghostcode-wrapper 启动器 symlink 到 ~/.ghostcode/bin/
114
+ // 3. 将 prompts/*.md 角色提示词 symlink 到 ~/.ghostcode/prompts/
115
+ // 4. 幂等设计:已存在且指向正确目标的 symlink 不重复创建
116
+ // 5. 失败时仅 console.error,不阻断会话建立
117
+ // ============================================
118
+
119
+ /**
120
+ * 创建或更新 symlink(幂等)
121
+ *
122
+ * 业务逻辑说明:
123
+ * 1. 如果 link 已存在且指向正确目标,跳过
124
+ * 2. 如果 link 已存在但指向旧目标,删除后重建
125
+ * 3. 如果 link 不存在,创建新 symlink
126
+ *
127
+ * @param {string} target - symlink 指向的实际文件路径
128
+ * @param {string} linkPath - symlink 文件路径
129
+ * @returns {boolean} 是否成功
130
+ */
131
+ function ensureSymlink(target, linkPath) {
132
+ try {
133
+ if (existsSync(linkPath)) {
134
+ // 检查现有 symlink 是否指向正确目标
135
+ try {
136
+ const currentTarget = readlinkSync(linkPath);
137
+ if (currentTarget === target) {
138
+ // 已存在且指向正确目标,跳过(幂等)
139
+ return true;
140
+ }
141
+ } catch {
142
+ // readlinkSync 失败说明不是 symlink(可能是普通文件),删除后重建
143
+ }
144
+ // 删除旧的 symlink 或文件
145
+ unlinkSync(linkPath);
146
+ }
147
+ // 创建新 symlink
148
+ symlinkSync(target, linkPath);
149
+ return true;
150
+ } catch (err) {
151
+ console.error(`[GhostCode] symlink 创建失败: ${linkPath} -> ${target}:`, err.message);
152
+ return false;
153
+ }
154
+ }
155
+
156
+ /**
157
+ * 自动安装 wrapper 和 prompts
158
+ *
159
+ * 两种环境的查找策略:
160
+ * - 分发环境:Plugin 包中有 bin/ghostcode-wrapper 启动器 → 直接 symlink
161
+ * - 开发环境:Plugin 包中无预编译二进制 → 查找 cargo 编译产物 target/release/ghostcode-wrapper
162
+ * prompts 从 src/plugin/prompts/ 直接 symlink
163
+ *
164
+ * @returns {void}
165
+ */
166
+ function setupWrapperAndPrompts() {
167
+ try {
168
+ // ============================================
169
+ // 推算 Plugin 包根目录
170
+ // 优先使用 CLAUDE_PLUGIN_ROOT 环境变量(Claude Code 注入)
171
+ // 回退方案:通过当前脚本路径向上推导(scripts/ -> 根目录)
172
+ // ============================================
173
+ let pluginRoot = process.env.CLAUDE_PLUGIN_ROOT;
174
+ if (!pluginRoot) {
175
+ // 回退:当前脚本在 scripts/ 目录下,向上一级即为 Plugin 根目录
176
+ const currentFile = fileURLToPath(import.meta.url);
177
+ pluginRoot = join(dirname(currentFile), "..");
178
+ }
179
+
180
+ // ============================================
181
+ // 确保目标目录存在
182
+ // ============================================
183
+ const binDir = join(GHOSTCODE_HOME, "bin");
184
+ const promptsDir = join(GHOSTCODE_HOME, "prompts");
185
+ mkdirSync(binDir, { recursive: true });
186
+ mkdirSync(promptsDir, { recursive: true });
187
+
188
+ // ============================================
189
+ // 查找 wrapper 二进制
190
+ // 策略 1:分发环境 — Plugin 包中的平台检测启动器
191
+ // 策略 2:开发环境 — cargo build 产物 target/release/ghostcode-wrapper
192
+ // ============================================
193
+ let wrapperSrc = join(pluginRoot, "bin", "ghostcode-wrapper");
194
+ if (!existsSync(wrapperSrc)) {
195
+ // 开发环境:从 src/plugin 向上推算项目根目录(../../),查找 cargo 编译产物
196
+ const projectRoot = join(pluginRoot, "..", "..");
197
+ const cargoBinary = join(projectRoot, "target", "release", "ghostcode-wrapper");
198
+ if (existsSync(cargoBinary)) {
199
+ wrapperSrc = cargoBinary;
200
+ } else {
201
+ // 两种路径都找不到 wrapper 二进制,跳过
202
+ // 开发环境需先执行 cargo build --release -p ghostcode-wrapper
203
+ return;
204
+ }
205
+ }
206
+
207
+ // symlink wrapper
208
+ // ~/.ghostcode/bin/ghostcode-wrapper -> wrapper 实际路径
209
+ const wrapperLink = join(binDir, "ghostcode-wrapper");
210
+ ensureSymlink(wrapperSrc, wrapperLink);
211
+
212
+ // ============================================
213
+ // 查找并 symlink prompts
214
+ // 策略 1:分发环境 — Plugin 包中的 prompts/ 目录
215
+ // 策略 2:开发环境 — src/plugin/prompts/ 目录(pluginRoot 即为 src/plugin)
216
+ // 两种环境下 prompts 都在 ${pluginRoot}/prompts/,逻辑统一
217
+ // ============================================
218
+ const promptsSrcDir = join(pluginRoot, "prompts");
219
+ if (existsSync(promptsSrcDir)) {
220
+ const promptFiles = readdirSync(promptsSrcDir).filter(f => f.endsWith(".md"));
221
+ for (const promptFile of promptFiles) {
222
+ const promptSrc = join(promptsSrcDir, promptFile);
223
+ const promptLink = join(promptsDir, promptFile);
224
+ ensureSymlink(promptSrc, promptLink);
225
+ }
226
+ }
227
+ } catch (err) {
228
+ // 安装失败不阻断会话建立,仅输出错误信息
229
+ console.error("[GhostCode] wrapper/prompts 自动安装失败:", err.message);
230
+ }
231
+ }
232
+
233
+ // ============================================
234
+ // 入口:执行主逻辑
235
+ // exit 0 策略:SessionStart 失败不应阻断 Claude Code 会话建立
236
+ // ============================================
237
+ try {
238
+ main();
239
+ } catch (err) {
240
+ console.error("[GhostCode] hook-session-start 初始化失败:", err);
241
+ // exit 0:初始化失败不阻断 Claude Code 正常使用
242
+ process.exit(0);
243
+ }