@colin4k1024/tsp 2.4.9 → 2.5.1

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 (37) hide show
  1. package/README.md +71 -1
  2. package/hooks/README.md +1 -1
  3. package/hooks/hooks.json +5 -4
  4. package/hooks/strategic-compact/README.md +7 -5
  5. package/hooks/strategic-compact/suggest-compact.js +9 -174
  6. package/package.json +1 -1
  7. package/scripts/__pycache__/__init__.cpython-311.pyc +0 -0
  8. package/scripts/__pycache__/build_platform_artifacts.cpython-311.pyc +0 -0
  9. package/scripts/__pycache__/install_platform.cpython-311.pyc +0 -0
  10. package/scripts/__pycache__/langfuse_trace.cpython-311.pyc +0 -0
  11. package/scripts/__pycache__/query_audit_logs.cpython-311.pyc +0 -0
  12. package/scripts/__pycache__/scan_leaked_keys.cpython-311.pyc +0 -0
  13. package/scripts/__pycache__/team_skills_platform.cpython-311.pyc +0 -0
  14. package/scripts/__pycache__/team_skills_platform.cpython-313.pyc +0 -0
  15. package/scripts/__pycache__/validate_library.cpython-311.pyc +0 -0
  16. package/scripts/__pycache__/validate_workflow_state.cpython-311.pyc +0 -0
  17. package/scripts/evolution/__pycache__/__init__.cpython-311.pyc +0 -0
  18. package/scripts/evolution/__pycache__/store.cpython-311.pyc +0 -0
  19. package/scripts/harness-audit.js +7 -4
  20. package/scripts/hooks/__pycache__/__init__.cpython-311.pyc +0 -0
  21. package/scripts/hooks/__pycache__/mcp_health_check.cpython-311.pyc +0 -0
  22. package/scripts/hooks/__pycache__/observe.cpython-311.pyc +0 -0
  23. package/scripts/hooks/__pycache__/session_end.cpython-311.pyc +0 -0
  24. package/scripts/hooks/__pycache__/session_start.cpython-311.pyc +0 -0
  25. package/scripts/hooks/suggest-compact.js +331 -63
  26. package/scripts/install-platform.js +68 -85
  27. package/scripts/lib/__pycache__/audit_logger.cpython-311.pyc +0 -0
  28. package/scripts/lib/__pycache__/audit_query.cpython-311.pyc +0 -0
  29. package/scripts/lib/__pycache__/hook_contract.cpython-311.pyc +0 -0
  30. package/scripts/lib/__pycache__/memory_store.cpython-311.pyc +0 -0
  31. package/scripts/lib/__pycache__/utils.cpython-311.pyc +0 -0
  32. package/scripts/lib/opencode/convert-agents.js +273 -0
  33. package/scripts/lib/opencode/convert-hooks.js +286 -0
  34. package/scripts/lib/opencode/generate-agents-md.js +361 -0
  35. package/scripts/test-opencode-install.js +151 -0
  36. package/skills/goframe-v2/examples/practices/quick-demo/manifest/config/config.yaml +14 -14
  37. package/skills/repo-scan/SKILL.md +63 -63
@@ -2,79 +2,347 @@
2
2
  /**
3
3
  * Strategic Compact Suggester
4
4
  *
5
- * Cross-platform (Windows, macOS, Linux)
6
- *
7
- * Runs on PreToolUse or periodically to suggest manual compaction at logical intervals
8
- *
9
- * Why manual over auto-compact:
10
- * - Auto-compact happens at arbitrary points, often mid-task
11
- * - Strategic compacting preserves context through logical phases
12
- * - Compact after exploration, before execution
13
- * - Compact after completing a milestone, before starting next
5
+ * Suggests `/compact` from real context pressure instead of tool-call count.
6
+ * Supports direct hook execution and run-with-flags.js require() mode.
14
7
  */
15
8
 
9
+ 'use strict';
10
+
16
11
  const fs = require('fs');
17
12
  const path = require('path');
18
- const {
19
- getTempDir,
20
- writeFile,
21
- log
22
- } = require('../lib/utils');
23
-
24
- async function main() {
25
- // Track tool call count (increment in a temp file)
26
- // Use a session-specific counter file based on session ID from environment
27
- // or parent PID as fallback
28
- const sessionId = (process.env.CLAUDE_SESSION_ID || 'default').replace(/[^a-zA-Z0-9_-]/g, '') || 'default';
29
- const counterFile = path.join(getTempDir(), `claude-tool-count-${sessionId}`);
30
- const rawThreshold = parseInt(process.env.COMPACT_THRESHOLD || '50', 10);
31
- const threshold = Number.isFinite(rawThreshold) && rawThreshold > 0 && rawThreshold <= 10000
32
- ? rawThreshold
33
- : 50;
34
-
35
- let count = 1;
36
-
37
- // Read existing count or start at 1
38
- // Use fd-based read+write to reduce (but not eliminate) race window
39
- // between concurrent hook invocations
13
+ const os = require('os');
14
+ const crypto = require('crypto');
15
+
16
+ const AUTO_COMPACT_BUFFER_PCT = 16.5;
17
+ const DEFAULT_CONTEXT_LIMIT = 200000;
18
+ const DEFAULT_DEBOUNCE_CALLS = 8;
19
+ const STALE_BRIDGE_SECONDS = 60;
20
+
21
+ const URGENCY = [
22
+ [95, 'critical'],
23
+ [85, 'high'],
24
+ [70, 'medium'],
25
+ [0, 'low'],
26
+ ];
27
+
28
+ function toNumber(value) {
29
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
30
+ if (typeof value === 'string' && value.trim()) {
31
+ const parsed = Number(value);
32
+ if (Number.isFinite(parsed)) return parsed;
33
+ }
34
+ return null;
35
+ }
36
+
37
+ function clampPct(value) {
38
+ return Math.max(0, Math.min(100, Math.round(value)));
39
+ }
40
+
41
+ function normalizeRemainingToUsed(remainingPct) {
42
+ const usableRemaining = Math.max(
43
+ 0,
44
+ ((remainingPct - AUTO_COMPACT_BUFFER_PCT) / (100 - AUTO_COMPACT_BUFFER_PCT)) * 100
45
+ );
46
+ return clampPct(100 - usableRemaining);
47
+ }
48
+
49
+ function getUrgency(usagePct) {
50
+ for (const [threshold, label] of URGENCY) {
51
+ if (usagePct >= threshold) return label;
52
+ }
53
+ return 'low';
54
+ }
55
+
56
+ function getHookEventName(data) {
57
+ const event = data.hook_event_name || data.hookEventName || data.event;
58
+ return typeof event === 'string' && event.trim() ? event.trim() : 'PreToolUse';
59
+ }
60
+
61
+ function sessionKey(data) {
62
+ const raw = data.session_id || process.env.CLAUDE_SESSION_ID;
63
+ if (typeof raw === 'string' && raw.trim()) {
64
+ return raw.replace(/[^a-zA-Z0-9_-]/g, '').slice(-64) || 'default';
65
+ }
66
+
67
+ const cwd = data.cwd || process.cwd();
68
+ return crypto.createHash('sha256').update(String(cwd)).digest('hex').slice(0, 16);
69
+ }
70
+
71
+ function readBridgeMetrics(sessionId) {
72
+ if (!sessionId) return null;
73
+
74
+ const bridgePath = path.join(os.tmpdir(), `harness-ctx-${sessionId}.json`);
75
+ if (!fs.existsSync(bridgePath)) return null;
76
+
40
77
  try {
41
- const fd = fs.openSync(counterFile, 'a+');
42
- try {
43
- const buf = Buffer.alloc(64);
44
- const bytesRead = fs.readSync(fd, buf, 0, 64, 0);
45
- if (bytesRead > 0) {
46
- const parsed = parseInt(buf.toString('utf8', 0, bytesRead).trim(), 10);
47
- // Clamp to reasonable range corrupted files could contain huge values
48
- // that pass Number.isFinite() (e.g., parseInt('9'.repeat(30)) => 1e+29)
49
- count = (Number.isFinite(parsed) && parsed > 0 && parsed <= 1000000)
50
- ? parsed + 1
51
- : 1;
52
- }
53
- // Truncate and write new value
54
- fs.ftruncateSync(fd, 0);
55
- fs.writeSync(fd, String(count), 0);
56
- } finally {
57
- fs.closeSync(fd);
58
- }
59
- } catch {
60
- // Fallback: just use writeFile if fd operations fail
61
- writeFile(counterFile, String(count));
78
+ const bridge = JSON.parse(fs.readFileSync(bridgePath, 'utf8'));
79
+ const now = Math.floor(Date.now() / 1000);
80
+ if (bridge.timestamp && now - bridge.timestamp > STALE_BRIDGE_SECONDS) return null;
81
+
82
+ const usagePct = toNumber(bridge.used_pct);
83
+ const remainingPct = toNumber(bridge.remaining_percentage);
84
+ if (usagePct == null && remainingPct == null) return null;
85
+
86
+ return {
87
+ usagePct: usagePct != null ? clampPct(usagePct) : normalizeRemainingToUsed(remainingPct),
88
+ remainingPct,
89
+ source: 'bridge',
90
+ };
91
+ } catch (_) {
92
+ return null;
93
+ }
94
+ }
95
+
96
+ function resolveContextMetrics(data) {
97
+ const contextLimit = toNumber(process.env.CLAUDE_CONTEXT_LIMIT) || DEFAULT_CONTEXT_LIMIT;
98
+ const cw = data.context_window && typeof data.context_window === 'object' ? data.context_window : {};
99
+
100
+ const stdinUsed = toNumber(cw.used_percentage);
101
+ if (stdinUsed != null) {
102
+ const usagePct = clampPct(stdinUsed);
103
+ return {
104
+ usagePct,
105
+ remainingPct: toNumber(cw.remaining_percentage),
106
+ contextLimit,
107
+ contextSize: Math.round((usagePct / 100) * contextLimit),
108
+ source: 'stdin.used_percentage',
109
+ };
110
+ }
111
+
112
+ const stdinRemaining = toNumber(cw.remaining_percentage);
113
+ if (stdinRemaining != null) {
114
+ const usagePct = normalizeRemainingToUsed(stdinRemaining);
115
+ return {
116
+ usagePct,
117
+ remainingPct: clampPct(stdinRemaining),
118
+ contextLimit,
119
+ contextSize: Math.round((usagePct / 100) * contextLimit),
120
+ source: 'stdin.remaining_percentage',
121
+ };
122
+ }
123
+
124
+ const envSize = toNumber(process.env.CLAUDE_CONTEXT_SIZE);
125
+ if (envSize != null && envSize > 0) {
126
+ const usagePct = clampPct((envSize / contextLimit) * 100);
127
+ return {
128
+ usagePct,
129
+ remainingPct: null,
130
+ contextLimit,
131
+ contextSize: envSize,
132
+ source: 'env',
133
+ };
134
+ }
135
+
136
+ const bridge = readBridgeMetrics(sessionKey(data));
137
+ if (bridge) {
138
+ return {
139
+ ...bridge,
140
+ contextLimit,
141
+ contextSize: Math.round((bridge.usagePct / 100) * contextLimit),
142
+ };
62
143
  }
63
144
 
64
- // Suggest compact after threshold tool calls
65
- if (count === threshold) {
66
- log(`[StrategicCompact] ${threshold} tool calls reached - consider /compact if transitioning phases`);
145
+ return null;
146
+ }
147
+
148
+ function buildSuggestions(usagePct, contextSize) {
149
+ const suggestions = [];
150
+ let savings = 0;
151
+
152
+ if (usagePct >= 85) {
153
+ const saved = Math.round(contextSize * 0.15);
154
+ suggestions.push({
155
+ action: 'summarize',
156
+ target: 'early conversation history',
157
+ reason: 'Preserve decisions and pending work, compress exploration traces',
158
+ estimated_tokens_saved: saved,
159
+ });
160
+ savings += saved;
161
+ }
162
+
163
+ if (usagePct >= 70) {
164
+ const saved = Math.round(contextSize * 0.10);
165
+ suggestions.push({
166
+ action: 'discard',
167
+ target: 'large tool outputs and search traces',
168
+ reason: 'Keep file paths and conclusions, drop bulky intermediate output',
169
+ estimated_tokens_saved: saved,
170
+ });
171
+ savings += saved;
172
+ }
173
+
174
+ if (usagePct >= 70) {
175
+ const saved = Math.round(contextSize * 0.08);
176
+ suggestions.push({
177
+ action: 'reorganize',
178
+ target: 'role and specialist outputs',
179
+ reason: 'Move decisions, handoff state, and validation results into the compact summary',
180
+ estimated_tokens_saved: saved,
181
+ });
182
+ savings += saved;
183
+ }
184
+
185
+ return { suggestions, savings };
186
+ }
187
+
188
+ function buildReorganizationPlan(urgency) {
189
+ return {
190
+ phase_1: {
191
+ action: 'capture_decisions',
192
+ description: 'Keep decisions, constraints, current branch, changed files, and validation results.',
193
+ },
194
+ phase_2: {
195
+ action: 'capture_next_steps',
196
+ description: 'Keep active task, pending todos, blockers, and the next command to run.',
197
+ },
198
+ phase_3: {
199
+ action: 'compress_history',
200
+ description: 'Summarize early exploration and long tool outputs to conclusions plus file paths.',
201
+ },
202
+ phase_4: {
203
+ action: urgency === 'critical' ? 'compact_now' : 'compact_at_logical_break',
204
+ description: urgency === 'critical'
205
+ ? 'Stop broad work and ask the user to run /compact now.'
206
+ : 'Finish the current small step, then run /compact before more exploration.',
207
+ },
208
+ };
209
+ }
210
+
211
+ function shouldEmit(sessionId, urgency, usagePct) {
212
+ if (process.env.STRATEGIC_COMPACT_DISABLE_DEBOUNCE === '1') return true;
213
+ if (!sessionId) return true;
214
+
215
+ const debounceCalls =
216
+ toNumber(process.env.STRATEGIC_COMPACT_DEBOUNCE_CALLS) || DEFAULT_DEBOUNCE_CALLS;
217
+ const statePath = path.join(os.tmpdir(), `harness-strategic-compact-${sessionId}.json`);
218
+ const nextState = {
219
+ lastUrgency: urgency,
220
+ lastUsagePct: usagePct,
221
+ callsSinceEmit: 0,
222
+ updatedAt: new Date().toISOString(),
223
+ };
224
+
225
+ try {
226
+ let previous = null;
227
+ if (fs.existsSync(statePath)) {
228
+ previous = JSON.parse(fs.readFileSync(statePath, 'utf8'));
229
+ }
230
+
231
+ const callsSinceEmit = (previous?.callsSinceEmit || 0) + 1;
232
+ const severityOrder = { low: 0, medium: 1, high: 2, critical: 3 };
233
+ const escalated = severityOrder[urgency] > severityOrder[previous?.lastUrgency || 'low'];
234
+ const usageJumped = usagePct - (previous?.lastUsagePct || 0) >= 8;
235
+ const repeatDue = callsSinceEmit >= debounceCalls;
236
+
237
+ if (!previous || escalated || usageJumped || repeatDue) {
238
+ fs.writeFileSync(statePath, JSON.stringify(nextState));
239
+ return true;
240
+ }
241
+
242
+ fs.writeFileSync(statePath, JSON.stringify({
243
+ ...nextState,
244
+ callsSinceEmit,
245
+ lastUrgency: previous.lastUrgency || urgency,
246
+ lastUsagePct: previous.lastUsagePct || usagePct,
247
+ }));
248
+ return false;
249
+ } catch (_) {
250
+ return true;
67
251
  }
252
+ }
253
+
254
+ function buildContextMessage({ usagePct, remainingPct, urgency, savings, suggestions, source }) {
255
+ const remainingPart = remainingPct == null ? '' : ` | remaining: ${clampPct(remainingPct)}%`;
256
+ const actionByUrgency = {
257
+ medium: 'Finish the current small step, then run `/compact` before more broad reading or implementation.',
258
+ high: 'Stop new exploration, preserve decisions/todos/validation results, and ask the user to run `/compact`.',
259
+ critical: 'Context is nearly exhausted. Do not start new tool chains; ask the user to run `/compact` now.',
260
+ };
261
+
262
+ return [
263
+ '## Strategic Compact Triggered',
264
+ `- Context usage: ${usagePct}%${remainingPart} | urgency: ${urgency} | source: ${source}`,
265
+ `- Action: ${actionByUrgency[urgency] || actionByUrgency.medium}`,
266
+ `- Estimated savings: ~${savings} tokens`,
267
+ ...suggestions.map(item => `- ${item.action}: ${item.target} - ${item.reason}`),
268
+ ].join('\n');
269
+ }
68
270
 
69
- // Suggest at regular intervals after threshold (every 25 calls from threshold)
70
- if (count > threshold && (count - threshold) % 25 === 0) {
71
- log(`[StrategicCompact] ${count} tool calls - good checkpoint for /compact if context is stale`);
271
+ function buildHookOutput(rawInput) {
272
+ let data = {};
273
+ try {
274
+ data = JSON.parse(rawInput || '{}');
275
+ } catch (_) {
276
+ return null;
72
277
  }
73
278
 
74
- process.exit(0);
279
+ const metrics = resolveContextMetrics(data);
280
+ if (!metrics) return null;
281
+
282
+ const urgency = getUrgency(metrics.usagePct);
283
+ if (urgency === 'low') return null;
284
+
285
+ const sessionId = sessionKey(data);
286
+ if (!shouldEmit(sessionId, urgency, metrics.usagePct)) return null;
287
+
288
+ const { suggestions, savings } = buildSuggestions(metrics.usagePct, metrics.contextSize);
289
+ const reorganizationPlan = buildReorganizationPlan(urgency);
290
+ const hookEventName = getHookEventName(data);
291
+ const additionalContext = buildContextMessage({
292
+ usagePct: metrics.usagePct,
293
+ remainingPct: metrics.remainingPct,
294
+ urgency,
295
+ savings,
296
+ suggestions,
297
+ source: metrics.source,
298
+ });
299
+
300
+ return {
301
+ hookSpecificOutput: {
302
+ hookEventName,
303
+ additionalContext,
304
+ },
305
+ compactSuggestion: {
306
+ should_compact: true,
307
+ urgency,
308
+ context_usage_ratio: metrics.usagePct,
309
+ context_source: metrics.source,
310
+ estimated_token_savings: savings,
311
+ suggestions,
312
+ reorganization_plan: reorganizationPlan,
313
+ },
314
+ };
315
+ }
316
+
317
+ function run(rawInput) {
318
+ const output = buildHookOutput(rawInput);
319
+ if (!output) return { exitCode: 0 };
320
+ return { stdout: JSON.stringify(output), exitCode: 0 };
321
+ }
322
+
323
+ function main() {
324
+ let input = '';
325
+ const stdinTimeout = setTimeout(() => process.exit(0), 8000);
326
+ process.stdin.setEncoding('utf8');
327
+ process.stdin.on('data', chunk => { input += chunk; });
328
+ process.stdin.on('end', () => {
329
+ clearTimeout(stdinTimeout);
330
+ try {
331
+ const output = buildHookOutput(input);
332
+ if (output) process.stdout.write(JSON.stringify(output));
333
+ } catch (_) {
334
+ process.exit(0);
335
+ }
336
+ });
337
+ }
338
+
339
+ if (require.main === module) {
340
+ main();
75
341
  }
76
342
 
77
- main().catch(err => {
78
- console.error('[StrategicCompact] Error:', err.message);
79
- process.exit(0);
80
- });
343
+ module.exports = {
344
+ run,
345
+ buildHookOutput,
346
+ resolveContextMetrics,
347
+ normalizeRemainingToUsed,
348
+ };
@@ -718,86 +718,9 @@ function convertRulesToMdc(rulesDir, targetDir) {
718
718
  }
719
719
  }
720
720
 
721
- const OPENCODE_AGENTS_MD_MARKER = "<!-- team-skills-platform -->";
722
-
723
- function buildOpenCodeAgentsMd(root) {
724
- const agentsDir = path.join(root, "agents", "roles");
725
- const roleNames = fs.existsSync(agentsDir)
726
- ? fs
727
- .readdirSync(agentsDir)
728
- .filter((name) => name.endsWith(".md"))
729
- .map((name) => path.parse(name).name)
730
- .sort()
731
- : [];
732
- const roleDisplay = {
733
- "tech-lead": "Tech Lead(技术负责人)",
734
- "product-manager": "Product Manager(产品经理)",
735
- "project-manager": "Project Manager(项目管理)",
736
- architect: "Architect(架构师)",
737
- "frontend-engineer": "Frontend Engineer(前端开发)",
738
- "backend-engineer": "Backend Engineer(后端开发)",
739
- "qa-engineer": "QA Engineer(测试工程师)",
740
- "devops-engineer": "DevOps Engineer(运维工程师)",
741
- };
742
- const lines = [
743
- OPENCODE_AGENTS_MD_MARKER,
744
- "# Team Skills Platform — OpenCode Agent Index",
745
- "",
746
- "本文件由安装脚本自动生成。在 OpenCode 中与任何角色交互时,可直接引用下列角色和命令。",
747
- "",
748
- "## 可用角色",
749
- "",
750
- ];
751
- for (const role of roleNames) {
752
- lines.push(`- **${roleDisplay[role] || role}**: \`plugins/team-skills-platform/agents/roles/${role}.md\``);
753
- }
754
- lines.push(
755
- "",
756
- "## 核心团队命令",
757
- "",
758
- "| 命令 | 用途 |",
759
- "|------|------|",
760
- "| `/team-help` | 根据当前阶段、artifacts 与阻塞项推荐下一步主链命令 |",
761
- "| `/team-intake` | 接收需求并锁定目标、范围、约束 |",
762
- "| `/team-plan` | 拆解任务、角色分工、依赖与里程碑 |",
763
- "| `/team-execute` | 驱动研发角色在边界内实施 |",
764
- "| `/team-review` | 做方案、质量、测试和放行评审 |",
765
- "| `/team-release` | 做发布准备、上线检查与回滚保障 |",
766
- "| `/team-closeout` | 在观察窗口结束后做最终收口与 backlog 回写 |",
767
- "| `/handoff` | 在角色间做结构化交接 |",
768
- "",
769
- "## 插件根路径",
770
- "",
771
- "`~/.config/opencode/plugins/team-skills-platform/`",
772
- "",
773
- `<!-- end ${PLUGIN_NAME} -->`,
774
- );
775
- return `${lines.join("\n")}\n`;
776
- }
777
-
778
- function mergeOpenCodeAgentsMd(targetPath, newContent) {
779
- const markerEnd = `<!-- end ${PLUGIN_NAME} -->`;
780
- if (!fs.existsSync(targetPath)) {
781
- fs.mkdirSync(path.dirname(targetPath), { recursive: true });
782
- fs.writeFileSync(targetPath, newContent, "utf8");
783
- return;
784
- }
785
- const existing = fs.readFileSync(targetPath, "utf8");
786
- if (existing.includes(OPENCODE_AGENTS_MD_MARKER)) {
787
- const startIdx = existing.indexOf(OPENCODE_AGENTS_MD_MARKER);
788
- let endIdx = existing.indexOf(markerEnd, startIdx);
789
- if (endIdx !== -1) {
790
- endIdx += markerEnd.length;
791
- if (existing[endIdx] === "\n") {
792
- endIdx += 1;
793
- }
794
- fs.writeFileSync(targetPath, `${existing.slice(0, startIdx)}${newContent}`, "utf8");
795
- return;
796
- }
797
- }
798
- const separator = existing.endsWith("\n") ? "\n" : "\n\n";
799
- fs.writeFileSync(targetPath, `${existing}${separator}${newContent}`, "utf8");
800
- }
721
+ const { generateAgentsMd, mergeAgentsMd } = require("./lib/opencode/generate-agents-md");
722
+ const { convertRoleAgents, convertSpecialistAgents } = require("./lib/opencode/convert-agents");
723
+ const { convertHooksPlugin } = require("./lib/opencode/convert-hooks");
801
724
 
802
725
  function installClaude(root, claudeHome) {
803
726
  const pluginDir = path.join(claudeHome, "plugins", PLUGIN_NAME);
@@ -883,6 +806,51 @@ function installCursor(root, cursorHome) {
883
806
  console.log(`Converted rules to MDC in ${mdcTarget}`);
884
807
  }
885
808
 
809
+ function generateOpenCodeConfig(root, opencodeHome) {
810
+ const configPath = path.join(opencodeHome, "opencode.json");
811
+
812
+ // 如果配置文件已存在,保留用户自定义配置
813
+ let existingConfig = {};
814
+ if (fs.existsSync(configPath)) {
815
+ try {
816
+ existingConfig = JSON.parse(fs.readFileSync(configPath, "utf8"));
817
+ } catch (error) {
818
+ console.warn(`Warning: Could not parse existing opencode.json: ${error.message}`);
819
+ }
820
+ }
821
+
822
+ // 读取插件配置
823
+ const pluginConfigPath = path.join(root, ".opencode-plugin", "config.json");
824
+ let pluginConfig = {};
825
+ if (fs.existsSync(pluginConfigPath)) {
826
+ try {
827
+ pluginConfig = JSON.parse(fs.readFileSync(pluginConfigPath, "utf8"));
828
+ } catch (error) {
829
+ console.warn(`Warning: Could not read plugin config: ${error.message}`);
830
+ }
831
+ }
832
+
833
+ // 合并配置
834
+ const config = {
835
+ $schema: "https://opencode.ai/config.json",
836
+ ...existingConfig,
837
+ // 确保这些字段始终存在
838
+ instructions: existingConfig.instructions || ["./AGENTS.md"],
839
+ plugin: existingConfig.plugin || [PLUGIN_NAME],
840
+ permission: existingConfig.permission || {
841
+ edit: "allow",
842
+ bash: "allow",
843
+ },
844
+ // 添加插件特定配置
845
+ ...pluginConfig,
846
+ };
847
+
848
+ // 写入配置文件
849
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
850
+ fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
851
+ console.log(`Generated OpenCode config at ${configPath}`);
852
+ }
853
+
886
854
  function installOpenCode(root, opencodeHome) {
887
855
  const pluginDir = path.join(opencodeHome, "plugins", PLUGIN_NAME);
888
856
  fs.mkdirSync(path.dirname(pluginDir), { recursive: true });
@@ -916,25 +884,40 @@ function installOpenCode(root, opencodeHome) {
916
884
  }
917
885
  }
918
886
 
887
+ // 转换 agents 为 OpenCode 格式(添加 YAML front matter)
888
+ convertRoleAgents(path.join(root, "agents", "roles"), path.join(pluginDir, "agents", "roles"));
889
+ convertSpecialistAgents(path.join(root, "agents", "specialists"), path.join(pluginDir, "agents", "specialists"));
890
+
891
+ // 复制转换后的 agents 到 opencodeHome/agents/ 目录
919
892
  const agentsTarget = path.join(opencodeHome, "agents");
920
893
  fs.mkdirSync(agentsTarget, { recursive: true });
921
- const agentsSrc = path.join(root, "agents");
922
- if (fs.existsSync(agentsSrc)) {
894
+ const pluginAgentsDir = path.join(pluginDir, "agents");
895
+ if (fs.existsSync(pluginAgentsDir)) {
923
896
  for (const agentSubdir of ["roles", "specialists"]) {
924
- const agentSrcDir = path.join(agentsSrc, agentSubdir);
897
+ const agentSrcDir = path.join(pluginAgentsDir, agentSubdir);
925
898
  if (!fs.existsSync(agentSrcDir)) {
926
899
  continue;
927
900
  }
928
901
  for (const name of fs.readdirSync(agentSrcDir)) {
929
902
  const src = path.join(agentSrcDir, name);
930
903
  if (fs.statSync(src).isFile()) {
931
- fs.copyFileSync(src, path.join(agentsTarget, name));
904
+ const targetName = agentSubdir === "specialists" ? `specialist-${name}` : name;
905
+ fs.copyFileSync(src, path.join(agentsTarget, targetName));
932
906
  }
933
907
  }
934
908
  }
935
909
  }
936
910
 
937
- mergeOpenCodeAgentsMd(path.join(opencodeHome, "AGENTS.md"), buildOpenCodeAgentsMd(root));
911
+ // 使用新的 AGENTS.md 生成脚本
912
+ const agentsMdContent = generateAgentsMd(root);
913
+ mergeAgentsMd(path.join(opencodeHome, "AGENTS.md"), agentsMdContent);
914
+
915
+ // 生成 opencode.json 配置文件
916
+ generateOpenCodeConfig(root, opencodeHome);
917
+
918
+ // 转换 hooks 为 OpenCode 插件格式
919
+ convertHooksPlugin(root, opencodeHome);
920
+
938
921
  console.log(`Installed OpenCode plugin to ${pluginDir}`);
939
922
  console.log(`Updated AGENTS.md at ${path.join(opencodeHome, "AGENTS.md")}`);
940
923
  console.log(`Copied commands to ${commandTarget}`);