@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,143 @@
1
+ /**
2
+ * @file scripts/hook-stop.mjs
3
+ * @description Stop Hook 脚本
4
+ * 在 Claude Code 会话终止时执行资源清理(顺序严格固定):
5
+ * 1. 触发 Skill Learning 分析(onSessionEnd,此时 Daemon 仍在运行)
6
+ * 2. 释放 Session Lease(引用计数 -1,决定是否为最后一个会话)
7
+ * 3. 如果是最后一个会话,关闭 Daemon(stopDaemon)
8
+ * 4. 清理状态文件(clearState)
9
+ *
10
+ * 顺序约束:onSessionEnd 必须在 stopDaemon 之前执行,确保 Daemon 仍在运行时完成分析
11
+ *
12
+ * 参考: src/plugin/src/hooks/handlers.ts - stopHandler
13
+ * @author Atlas.oi
14
+ * @date 2026-03-05
15
+ */
16
+
17
+ import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync } from "node:fs";
18
+ import { join, dirname } from "node:path";
19
+ import { homedir } from "node:os";
20
+
21
+ // ============================================
22
+ // 常量
23
+ // ============================================
24
+
25
+ const GHOSTCODE_HOME = process.env.GHOSTCODE_HOME || join(homedir(), ".ghostcode");
26
+
27
+ // Hook 状态文件(与 hook-pre-tool-use.mjs 共享)
28
+ const STATE_FILE = join(GHOSTCODE_HOME, "state", "hook-state.json");
29
+
30
+ // Plugin 根目录
31
+ const PLUGIN_ROOT = process.env.CLAUDE_PLUGIN_ROOT || join(dirname(new URL(import.meta.url).pathname), "..");
32
+
33
+ // ============================================
34
+ // 状态文件读取
35
+ // ============================================
36
+
37
+ /**
38
+ * 读取 Hook 状态文件
39
+ */
40
+ function readState() {
41
+ try {
42
+ if (existsSync(STATE_FILE)) {
43
+ return JSON.parse(readFileSync(STATE_FILE, "utf-8"));
44
+ }
45
+ } catch {
46
+ // 解析失败返回默认状态
47
+ }
48
+ return { daemonStarted: false, socketPath: null, leaseId: null };
49
+ }
50
+
51
+ /**
52
+ * 清理状态文件(会话结束,重置所有状态)
53
+ */
54
+ function clearState() {
55
+ try {
56
+ if (existsSync(STATE_FILE)) {
57
+ unlinkSync(STATE_FILE);
58
+ }
59
+ } catch {
60
+ // 清理失败不影响主流程
61
+ }
62
+ }
63
+
64
+ // ============================================
65
+ // 主逻辑
66
+ // ============================================
67
+
68
+ async function main() {
69
+ const state = readState();
70
+ let shouldShutdown = false;
71
+
72
+ // ============================================
73
+ // 第一步:触发 Skill Learning 分析(onSessionEnd)
74
+ // 必须在 releaseLease 和 stopDaemon 之前调用
75
+ // 确保此时 Daemon 仍在运行,Skill Learning 可以访问 Daemon 状态
76
+ // onSessionEnd 失败不阻断后续 Stop 流程(隔离错误边界)
77
+ // ============================================
78
+ try {
79
+ const { onSessionEnd } = await import(join(PLUGIN_ROOT, "dist", "learner", "index.js"));
80
+ await onSessionEnd();
81
+ } catch (err) {
82
+ // Skill Learning 失败不阻断 Stop 流程
83
+ console.error("[GhostCode] Skill Learning 分析失败,继续执行 Stop 流程:", err);
84
+ }
85
+
86
+ // ============================================
87
+ // 第二步:释放 Session Lease(releaseLease)
88
+ // 基于引用计数决定是否需要关闭 Daemon
89
+ // ============================================
90
+ try {
91
+ const { SessionLeaseManager } = await import(join(PLUGIN_ROOT, "dist", "session-lease.js"));
92
+ const sessionsPath = join(GHOSTCODE_HOME, "daemon", "sessions.json");
93
+ const leaseManager = new SessionLeaseManager(sessionsPath);
94
+
95
+ if (state.leaseId) {
96
+ // 正常路径:本会话持有 lease,释放后由 isLast 决定
97
+ try {
98
+ const result = leaseManager.releaseLease(state.leaseId);
99
+ shouldShutdown = result.isLast;
100
+ } catch {
101
+ console.error("[GhostCode] Lease 释放失败,保守保留 Daemon 运行");
102
+ }
103
+ } else {
104
+ // 异常路径:acquire 曾失败,本会话从未持有 lease
105
+ // 显式读取 refcount,只有确认无其他会话时才关闭
106
+ try {
107
+ const refcount = leaseManager.getRefcount();
108
+ shouldShutdown = refcount === 0;
109
+ } catch {
110
+ console.error("[GhostCode] 无法读取 refcount,保守保留 Daemon 运行");
111
+ }
112
+ }
113
+ } catch (err) {
114
+ console.error("[GhostCode] Session Lease 模块加载失败:", err);
115
+ }
116
+
117
+ // ============================================
118
+ // 第三步:关闭 Daemon(仅最后一个会话时)
119
+ // onSessionEnd 已完成,此时安全关闭 Daemon
120
+ // ============================================
121
+ if (shouldShutdown) {
122
+ try {
123
+ const { stopDaemon } = await import(join(PLUGIN_ROOT, "dist", "daemon.js"));
124
+ await stopDaemon();
125
+ } catch (err) {
126
+ console.error("[GhostCode] Daemon 关闭失败:", err);
127
+ }
128
+ }
129
+
130
+ // ============================================
131
+ // 第四步:清理状态文件(clearState)
132
+ // 会话已结束,重置所有 Hook 状态
133
+ // ============================================
134
+ clearState();
135
+ }
136
+
137
+ // 执行主逻辑
138
+ main().catch((err) => {
139
+ console.error("[GhostCode] hook-stop 异常:", err);
140
+ // 确保即使异常也清理状态文件
141
+ clearState();
142
+ process.exit(0);
143
+ });
@@ -0,0 +1,231 @@
1
+ /**
2
+ * @file scripts/hook-subagent-start.mjs
3
+ * @description SubagentStart Hook 脚本
4
+ * 在子 Agent 启动时执行:
5
+ * 1. 从 stdin 读取子 Agent 事件信息(agent ID、session ID 等)
6
+ * 2. 将子 Agent 记录写入状态文件,供其他 Hook 感知当前活跃的子 Agent
7
+ * 3. 向 Daemon 注册 Actor(IPC actor_start),使 Dashboard 实时感知
8
+ *
9
+ * 状态文件结构(~/.ghostcode/state/subagents.json):
10
+ * {
11
+ * "agents": {
12
+ * "<agentId>": {
13
+ * "startedAt": "<ISO时间>",
14
+ * "sessionId": "<会话ID>"
15
+ * }
16
+ * }
17
+ * }
18
+ *
19
+ * 注意:失败时 exit 0,不阻断子 Agent 启动
20
+ * @author Atlas.oi
21
+ * @date 2026-03-06
22
+ */
23
+
24
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from "node:fs";
25
+ import { join } from "node:path";
26
+ import { homedir } from "node:os";
27
+ import { readStdin } from "./lib/stdin.mjs";
28
+
29
+ // ============================================
30
+ // 常量配置
31
+ // ============================================
32
+
33
+ // GhostCode 主目录,支持环境变量覆盖(主要用于测试隔离)
34
+ const GHOSTCODE_HOME = process.env.GHOSTCODE_HOME || join(homedir(), ".ghostcode");
35
+
36
+ // 子 Agent 状态文件路径
37
+ const SUBAGENTS_FILE = join(GHOSTCODE_HOME, "state", "subagents.json");
38
+
39
+ // ============================================
40
+ // 状态文件读写工具函数
41
+ // ============================================
42
+
43
+ /**
44
+ * 读取子 Agent 状态文件
45
+ *
46
+ * @returns {{ agents: Record<string, { startedAt: string; sessionId: string }> }} 子 Agent 状态
47
+ */
48
+ function readSubagentsState() {
49
+ try {
50
+ if (existsSync(SUBAGENTS_FILE)) {
51
+ return JSON.parse(readFileSync(SUBAGENTS_FILE, "utf-8"));
52
+ }
53
+ } catch {
54
+ // 解析失败返回空状态
55
+ }
56
+ return { agents: {} };
57
+ }
58
+
59
+ /**
60
+ * 写入子 Agent 状态文件
61
+ *
62
+ * @param {{ agents: Record<string, unknown> }} state - 要写入的状态对象
63
+ */
64
+ function writeSubagentsState(state) {
65
+ try {
66
+ const stateDir = join(GHOSTCODE_HOME, "state");
67
+ mkdirSync(stateDir, { recursive: true });
68
+ writeFileSync(SUBAGENTS_FILE, JSON.stringify(state, null, 2), "utf-8");
69
+ } catch {
70
+ // 写入失败静默处理
71
+ }
72
+ }
73
+
74
+ /**
75
+ * 从 ~/.ghostcode/groups/ 目录解析当前活跃的 group_id
76
+ *
77
+ * 简单策略:取第一个 g- 开头的目录名(当前单 group 场景)
78
+ * 后续多 group 场景可通过 hook-state.json 中的 activeGroupId 扩展
79
+ *
80
+ * @returns {string|null} group_id 或 null
81
+ */
82
+ function resolveGroupId() {
83
+ try {
84
+ const groupsDir = join(GHOSTCODE_HOME, "groups");
85
+ if (!existsSync(groupsDir)) return null;
86
+ const dirs = readdirSync(groupsDir, { withFileTypes: true })
87
+ .filter((d) => d.isDirectory() && d.name.startsWith("g-"))
88
+ .map((d) => d.name);
89
+ return dirs.length > 0 ? dirs[0] : null;
90
+ } catch {
91
+ return null;
92
+ }
93
+ }
94
+
95
+ // ============================================
96
+ // 显示名称生成
97
+ // ============================================
98
+
99
+ /**
100
+ * 根据 agent_type 生成人类可读的显示名称
101
+ *
102
+ * 转换规则:
103
+ * - "feature-dev:code-reviewer" -> "Code Reviewer"(取冒号后部分)
104
+ * - "general-purpose" -> "General Purpose"(直接转换)
105
+ * - kebab-case 和 snake_case 转换为 Title Case
106
+ *
107
+ * @param {string|null|undefined} agentType - Claude Code 传入的 agent_type 标识
108
+ * @returns {string|null} 人类可读名称,无法生成时返回 null
109
+ */
110
+ /**
111
+ * 将单词首字母大写
112
+ *
113
+ * @param {string} word - 待转换的单词
114
+ * @returns {string} 首字母大写的单词
115
+ */
116
+ function capitalizeWord(word) {
117
+ return word.charAt(0).toUpperCase() + word.slice(1);
118
+ }
119
+
120
+ function generateDisplayName(agentType) {
121
+ if (!agentType || agentType.trim() === '') return null;
122
+ // W3 修复:取冒号后部分,若为空则回退到冒号前部分
123
+ // 例如 "feature-dev:" → pop() 返回空字符串 → 回退到 "feature-dev"
124
+ let part = agentType;
125
+ if (agentType.includes(':')) {
126
+ const segments = agentType.split(':');
127
+ const last = segments.pop();
128
+ part = (last && last.trim() !== '') ? last : segments[0] || agentType;
129
+ }
130
+ // kebab-case / snake_case 转 Title Case,过滤空元素防止多余空格
131
+ const result = part.split(/[-_]/).filter(Boolean).map(capitalizeWord).join(' ');
132
+ return result || null;
133
+ }
134
+
135
+ // ============================================
136
+ // 主逻辑
137
+ // ============================================
138
+
139
+ /**
140
+ * SubagentStart Hook 主函数
141
+ *
142
+ * 业务逻辑说明:
143
+ * 1. 从 stdin 读取 Claude Code 传入的事件 JSON
144
+ * 2. 提取子 Agent ID、会话 ID、agent_type 并生成 display_name
145
+ * 3. 将子 Agent 记录追加到 subagents.json 状态文件
146
+ * 4. 向 Daemon 注册 Actor(IPC actor_start),携带 display_name 和 agent_type
147
+ */
148
+ async function main() {
149
+ // ============================================
150
+ // 第一步:从 stdin 读取事件数据
151
+ // Claude Code 通过 stdin 传入 SubagentStart 事件的 JSON 数据
152
+ // ============================================
153
+ const raw = await readStdin();
154
+ let event = {};
155
+ try {
156
+ event = JSON.parse(raw);
157
+ } catch {
158
+ // JSON 解析失败,使用空事件继续(可能无 stdin 输入)
159
+ }
160
+
161
+ // ============================================
162
+ // 第二步:提取子 Agent 信息
163
+ // 兼容嵌套格式:event.event.agentId 或 event.agentId
164
+ // ============================================
165
+ const inner = event?.event ?? event;
166
+ const agentId = inner?.agent_id || inner?.agentId || `agent-${Date.now()}`;
167
+ const sessionId = inner?.session_id || inner?.sessionId || "";
168
+ // W5 修复:无 agent_id 时记录错误并提前退出,不写入无效数据
169
+ if (!inner?.agent_id && !inner?.agentId) {
170
+ console.error("[GhostCode] SubagentStart: stdin 中未找到 agent_id 字段,跳过注册");
171
+ process.exit(0);
172
+ }
173
+
174
+ // 提取 agent_type 并生成人类可读的 display_name
175
+ const agentType = inner?.agent_type || inner?.agentType || null;
176
+ const displayName = generateDisplayName(agentType);
177
+
178
+ // ============================================
179
+ // 第三步:记录子 Agent 到状态文件
180
+ // 追加到已有的 agents 记录,不覆盖其他活跃 Agent
181
+ // ============================================
182
+ const state = readSubagentsState();
183
+ state.agents[agentId] = {
184
+ startedAt: new Date().toISOString(),
185
+ sessionId,
186
+ agentType,
187
+ displayName,
188
+ };
189
+ writeSubagentsState(state);
190
+
191
+ console.log(`[GhostCode] SubagentStart: 已记录子 Agent ${agentId}`);
192
+
193
+ // ============================================
194
+ // 第四步:向 Daemon 注册 Actor(Daemon 在运行时)
195
+ // 通过 IPC 调用 actor_start,让 Daemon 感知新 Agent 的存在
196
+ // Daemon 会将事件写入 Ledger,Dashboard 通过 SSE 实时显示
197
+ //
198
+ // 注意:Hook 失败 exit 0 是 Claude Code Plugin 规范要求,
199
+ // 不是降级策略——Hook 失败不应阻断 Claude Code 本身运行
200
+ // ============================================
201
+ try {
202
+ const { callDaemon } = await import("./lib/daemon-client.mjs");
203
+
204
+ // 从 groups 目录获取当前活跃的 group_id
205
+ const groupId = resolveGroupId();
206
+ if (groupId) {
207
+ // 仅在有值时附加可选字段,避免发送 null(与 Rust 侧惯例一致)
208
+ const ipcArgs = { group_id: groupId, actor_id: agentId, by: "system" };
209
+ if (displayName) ipcArgs.display_name = displayName;
210
+ if (agentType) ipcArgs.agent_type = agentType;
211
+ const resp = await callDaemon("actor_start", ipcArgs);
212
+ if (resp?.ok) {
213
+ console.log(`[GhostCode] SubagentStart: Actor ${agentId} 已向 Daemon 注册`);
214
+ } else {
215
+ console.error(`[GhostCode] SubagentStart: Daemon actor_start 失败:`, resp?.error?.message || "未知错误");
216
+ }
217
+ }
218
+ } catch (err) {
219
+ // Daemon IPC 失败不阻断子 Agent 启动(Hook exit 0 规范)
220
+ console.error("[GhostCode] SubagentStart: Daemon IPC 异常:", err.message);
221
+ }
222
+ }
223
+
224
+ // ============================================
225
+ // 入口:执行主逻辑
226
+ // exit 0 策略:记录失败不阻断子 Agent 启动
227
+ // ============================================
228
+ main().catch((err) => {
229
+ console.error("[GhostCode] hook-subagent-start 异常:", err);
230
+ process.exit(0);
231
+ });
@@ -0,0 +1,168 @@
1
+ /**
2
+ * @file scripts/hook-subagent-stop.mjs
3
+ * @description SubagentStop Hook 脚本
4
+ * 在子 Agent 停止时执行:
5
+ * 1. 从 stdin 读取子 Agent 事件信息(agent ID 等)
6
+ * 2. 从状态文件中移除该子 Agent 的记录
7
+ * 3. 向 Daemon 注销 Actor(IPC actor_stop),使 Dashboard 实时感知
8
+ *
9
+ * 注意:失败时 exit 0,不阻断子 Agent 正常停止
10
+ * @author Atlas.oi
11
+ * @date 2026-03-06
12
+ */
13
+
14
+ import { existsSync, readFileSync, writeFileSync, readdirSync } from "node:fs";
15
+ import { join } from "node:path";
16
+ import { homedir } from "node:os";
17
+ import { readStdin } from "./lib/stdin.mjs";
18
+
19
+ // ============================================
20
+ // 常量配置
21
+ // ============================================
22
+
23
+ // GhostCode 主目录,支持环境变量覆盖(主要用于测试隔离)
24
+ const GHOSTCODE_HOME = process.env.GHOSTCODE_HOME || join(homedir(), ".ghostcode");
25
+
26
+ // 子 Agent 状态文件路径(与 hook-subagent-start.mjs 共享)
27
+ const SUBAGENTS_FILE = join(GHOSTCODE_HOME, "state", "subagents.json");
28
+
29
+ // ============================================
30
+ // 状态文件读写工具函数
31
+ // ============================================
32
+
33
+ /**
34
+ * 读取子 Agent 状态文件
35
+ *
36
+ * @returns {{ agents: Record<string, unknown> }} 子 Agent 状态
37
+ */
38
+ function readSubagentsState() {
39
+ try {
40
+ if (existsSync(SUBAGENTS_FILE)) {
41
+ return JSON.parse(readFileSync(SUBAGENTS_FILE, "utf-8"));
42
+ }
43
+ } catch {
44
+ // 解析失败返回空状态
45
+ }
46
+ return { agents: {} };
47
+ }
48
+
49
+ /**
50
+ * 写入子 Agent 状态文件
51
+ *
52
+ * @param {{ agents: Record<string, unknown> }} state - 要写入的状态对象
53
+ */
54
+ function writeSubagentsState(state) {
55
+ try {
56
+ writeFileSync(SUBAGENTS_FILE, JSON.stringify(state, null, 2), "utf-8");
57
+ } catch {
58
+ // 写入失败静默处理
59
+ }
60
+ }
61
+
62
+ /**
63
+ * 从 ~/.ghostcode/groups/ 目录解析当前活跃的 group_id
64
+ *
65
+ * 简单策略:取第一个 g- 开头的目录名(当前单 group 场景)
66
+ *
67
+ * @returns {string|null} group_id 或 null
68
+ */
69
+ function resolveGroupId() {
70
+ try {
71
+ const groupsDir = join(GHOSTCODE_HOME, "groups");
72
+ if (!existsSync(groupsDir)) return null;
73
+ const dirs = readdirSync(groupsDir, { withFileTypes: true })
74
+ .filter((d) => d.isDirectory() && d.name.startsWith("g-"))
75
+ .map((d) => d.name);
76
+ return dirs.length > 0 ? dirs[0] : null;
77
+ } catch {
78
+ return null;
79
+ }
80
+ }
81
+
82
+ // ============================================
83
+ // 主逻辑
84
+ // ============================================
85
+
86
+ /**
87
+ * SubagentStop Hook 主函数
88
+ *
89
+ * 业务逻辑说明:
90
+ * 1. 从 stdin 读取 Claude Code 传入的事件 JSON
91
+ * 2. 提取子 Agent ID
92
+ * 3. 从 subagents.json 状态文件中删除该 Agent 记录
93
+ * 4. 向 Daemon 注销 Actor(IPC actor_stop)
94
+ */
95
+ async function main() {
96
+ // ============================================
97
+ // 第一步:从 stdin 读取事件数据
98
+ // Claude Code 通过 stdin 传入 SubagentStop 事件的 JSON 数据
99
+ // ============================================
100
+ const raw = await readStdin();
101
+ let event = {};
102
+ try {
103
+ event = JSON.parse(raw);
104
+ } catch {
105
+ // JSON 解析失败,使用空事件继续
106
+ }
107
+
108
+ // ============================================
109
+ // 第二步:提取子 Agent ID
110
+ // 兼容嵌套格式:event.event.agentId 或 event.agentId
111
+ // ============================================
112
+ const inner = event?.event ?? event;
113
+ const agentId = inner?.agent_id || inner?.agentId || "";
114
+
115
+ // ============================================
116
+ // 第三步:从状态文件移除子 Agent 记录
117
+ // 不影响其他仍在运行的子 Agent
118
+ // ============================================
119
+ const state = readSubagentsState();
120
+ if (agentId && state.agents[agentId]) {
121
+ delete state.agents[agentId];
122
+ writeSubagentsState(state);
123
+ console.log(`[GhostCode] SubagentStop: 已移除子 Agent ${agentId} 的记录`);
124
+ } else {
125
+ // agentId 不存在于状态文件(可能从未记录),静默处理
126
+ console.log(`[GhostCode] SubagentStop: 子 Agent ${agentId || "(未知)"} 已停止`);
127
+ }
128
+
129
+ // ============================================
130
+ // 第四步:向 Daemon 注销 Actor(Daemon 在运行时)
131
+ // 通过 IPC 调用 actor_stop,让 Daemon 感知 Agent 已停止
132
+ // Daemon 会将事件写入 Ledger,Dashboard 通过 SSE 实时更新
133
+ //
134
+ // 注意:Hook 失败 exit 0 是 Claude Code Plugin 规范要求,
135
+ // 不是降级策略——Hook 失败不应阻断 Claude Code 本身运行
136
+ // ============================================
137
+ if (agentId) {
138
+ try {
139
+ const { callDaemon } = await import("./lib/daemon-client.mjs");
140
+
141
+ const groupId = resolveGroupId();
142
+ if (groupId) {
143
+ const resp = await callDaemon("actor_stop", {
144
+ group_id: groupId,
145
+ actor_id: agentId,
146
+ by: "system",
147
+ });
148
+ if (resp?.ok) {
149
+ console.log(`[GhostCode] SubagentStop: Actor ${agentId} 已向 Daemon 注销`);
150
+ } else {
151
+ console.error(`[GhostCode] SubagentStop: Daemon actor_stop 失败:`, resp?.error?.message || "未知错误");
152
+ }
153
+ }
154
+ } catch (err) {
155
+ // Daemon IPC 失败不阻断子 Agent 停止(Hook exit 0 规范)
156
+ console.error("[GhostCode] SubagentStop: Daemon IPC 异常:", err.message);
157
+ }
158
+ }
159
+ }
160
+
161
+ // ============================================
162
+ // 入口:执行主逻辑
163
+ // exit 0 策略:清理失败不阻断子 Agent 正常停止
164
+ // ============================================
165
+ main().catch((err) => {
166
+ console.error("[GhostCode] hook-subagent-stop 异常:", err);
167
+ process.exit(0);
168
+ });
@@ -0,0 +1,134 @@
1
+ /**
2
+ * @file scripts/hook-user-prompt-submit.mjs
3
+ * @description UserPromptSubmit Hook 脚本
4
+ * 在用户提交 prompt 时检测 Magic Keywords 并注入上下文。
5
+ *
6
+ * 输入:从 stdin 读取事件 JSON
7
+ * 输出:到 stdout 输出响应 JSON(含 additionalContext)
8
+ *
9
+ * 参考: src/plugin/src/hooks/handlers.ts - userPromptSubmitHandler
10
+ * @author Atlas.oi
11
+ * @date 2026-03-05
12
+ */
13
+
14
+ import { join, dirname } from "node:path";
15
+ import { readStdin } from "./lib/stdin.mjs";
16
+
17
+ // ============================================
18
+ // 常量
19
+ // ============================================
20
+
21
+ // Plugin 根目录
22
+ const PLUGIN_ROOT = process.env.CLAUDE_PLUGIN_ROOT || join(dirname(new URL(import.meta.url).pathname), "..");
23
+
24
+ // 工作区根目录(Claude Code 保证 process.cwd() 等于项目根目录)
25
+ const WORKSPACE_ROOT = process.cwd();
26
+
27
+ // Magic Keyword 激活后注入给 Claude 的上下文说明
28
+ const KEYWORD_CONTEXT_MAP = {
29
+ ralph: "[GhostCode] Ralph 验证模式已激活 - 代码变更将经过 7 项自动验证",
30
+ autopilot: "[GhostCode] Autopilot 模式已激活 - 全自动执行模式",
31
+ team: "[GhostCode] Team 模式已激活 - 多 Agent 协作模式",
32
+ ultrawork: "[GhostCode] UltraWork 模式已激活 - 极致工作模式",
33
+ };
34
+
35
+ // ============================================
36
+ // 主逻辑
37
+ // ============================================
38
+
39
+ async function main() {
40
+ // ============================================
41
+ // 第一步:从 stdin 读取事件并提取 prompt
42
+ // ============================================
43
+ let prompt = "";
44
+ try {
45
+ const raw = await readStdin();
46
+ if (raw) {
47
+ const event = JSON.parse(raw);
48
+ // 防御性提取:支持多种事件格式
49
+ prompt = event?.event?.prompt || event?.prompt || "";
50
+ }
51
+ } catch {
52
+ // 解析失败,无 prompt 可处理
53
+ }
54
+
55
+ if (!prompt) {
56
+ // 无 prompt,输出空 JSON 不干扰
57
+ console.log(JSON.stringify({}));
58
+ return;
59
+ }
60
+
61
+ // ============================================
62
+ // 第二步:追加到 Skill Learning 缓冲区
63
+ // ============================================
64
+ try {
65
+ const { appendSessionContent } = await import(join(PLUGIN_ROOT, "dist", "learner", "manager.js"));
66
+ appendSessionContent(prompt);
67
+ } catch {
68
+ // Skill Learning 模块加载失败不影响主流程
69
+ }
70
+
71
+ // ============================================
72
+ // 第三步:检测 Magic Keywords
73
+ // ============================================
74
+ let topMatch = null;
75
+ try {
76
+ const { detectMagicKeywords, resolveKeywordPriority } = await import(join(PLUGIN_ROOT, "dist", "keywords", "index.js"));
77
+ const matches = detectMagicKeywords(prompt);
78
+ topMatch = resolveKeywordPriority(matches);
79
+ } catch (err) {
80
+ console.error("[GhostCode] Keywords 模块加载失败:", err);
81
+ }
82
+
83
+ if (topMatch === null) {
84
+ // 无关键词命中,输出空 JSON
85
+ console.log(JSON.stringify({}));
86
+ return;
87
+ }
88
+
89
+ // ============================================
90
+ // 第四步:处理 cancel 特殊逻辑
91
+ // ============================================
92
+ if (topMatch.type === "cancel") {
93
+ try {
94
+ const { writeKeywordState } = await import(join(PLUGIN_ROOT, "dist", "keywords", "state.js"));
95
+ await writeKeywordState(WORKSPACE_ROOT, {
96
+ active: null,
97
+ activatedAt: null,
98
+ prompt: null,
99
+ });
100
+ } catch {
101
+ // 状态写入失败不阻断流程
102
+ }
103
+ console.log(JSON.stringify({ additionalContext: "[GhostCode] 模式已取消" }));
104
+ return;
105
+ }
106
+
107
+ // ============================================
108
+ // 第五步:激活关键词模式
109
+ // ============================================
110
+ try {
111
+ const { writeKeywordState } = await import(join(PLUGIN_ROOT, "dist", "keywords", "state.js"));
112
+ await writeKeywordState(WORKSPACE_ROOT, {
113
+ active: topMatch.type,
114
+ activatedAt: new Date().toISOString(),
115
+ prompt,
116
+ });
117
+ } catch {
118
+ // 状态写入失败不阻断流程
119
+ }
120
+
121
+ // 构建上下文注入信息
122
+ const contextMessage = KEYWORD_CONTEXT_MAP[topMatch.type] ?? `[GhostCode] ${topMatch.type} 模式已激活`;
123
+
124
+ // 输出到 stdout
125
+ console.log(JSON.stringify({ additionalContext: contextMessage }));
126
+ }
127
+
128
+ // 执行主逻辑
129
+ main().catch((err) => {
130
+ console.error("[GhostCode] hook-user-prompt-submit 异常:", err);
131
+ // 即使异常也输出空 JSON,不阻断流程
132
+ console.log(JSON.stringify({}));
133
+ process.exit(0);
134
+ });