@wooojin/forgen 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.
- package/.claude-plugin/plugin.json +20 -0
- package/CHANGELOG.md +353 -0
- package/CONTRIBUTING.md +98 -0
- package/LICENSE +21 -0
- package/README.ja.md +469 -0
- package/README.ko.md +469 -0
- package/README.md +483 -0
- package/README.zh.md +469 -0
- package/agents/analyst.md +98 -0
- package/agents/architect.md +62 -0
- package/agents/code-reviewer.md +120 -0
- package/agents/code-simplifier.md +197 -0
- package/agents/critic.md +70 -0
- package/agents/debugger.md +117 -0
- package/agents/designer.md +131 -0
- package/agents/executor.md +54 -0
- package/agents/explore.md +145 -0
- package/agents/git-master.md +212 -0
- package/agents/performance-reviewer.md +172 -0
- package/agents/planner.md +29 -0
- package/agents/qa-tester.md +158 -0
- package/agents/refactoring-expert.md +168 -0
- package/agents/scientist.md +144 -0
- package/agents/security-reviewer.md +137 -0
- package/agents/test-engineer.md +153 -0
- package/agents/verifier.md +133 -0
- package/agents/writer.md +184 -0
- package/commands/api-design.md +268 -0
- package/commands/architecture-decision.md +314 -0
- package/commands/ci-cd.md +270 -0
- package/commands/code-review.md +233 -0
- package/commands/compound.md +117 -0
- package/commands/database.md +263 -0
- package/commands/debug-detective.md +99 -0
- package/commands/docker.md +274 -0
- package/commands/documentation.md +276 -0
- package/commands/ecomode.md +51 -0
- package/commands/frontend.md +271 -0
- package/commands/git-master.md +90 -0
- package/commands/incident-response.md +292 -0
- package/commands/migrate.md +101 -0
- package/commands/performance.md +288 -0
- package/commands/refactor.md +105 -0
- package/commands/security-review.md +288 -0
- package/commands/tdd.md +183 -0
- package/commands/testing-strategy.md +265 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +295 -0
- package/dist/core/auto-compound-runner.d.ts +12 -0
- package/dist/core/auto-compound-runner.js +460 -0
- package/dist/core/config-hooks.d.ts +10 -0
- package/dist/core/config-hooks.js +112 -0
- package/dist/core/config-injector.d.ts +50 -0
- package/dist/core/config-injector.js +455 -0
- package/dist/core/doctor.d.ts +1 -0
- package/dist/core/doctor.js +163 -0
- package/dist/core/errors.d.ts +81 -0
- package/dist/core/errors.js +133 -0
- package/dist/core/global-config.d.ts +43 -0
- package/dist/core/global-config.js +25 -0
- package/dist/core/harness.d.ts +24 -0
- package/dist/core/harness.js +621 -0
- package/dist/core/init.d.ts +7 -0
- package/dist/core/init.js +37 -0
- package/dist/core/inspect-cli.d.ts +7 -0
- package/dist/core/inspect-cli.js +47 -0
- package/dist/core/legacy-detector.d.ts +33 -0
- package/dist/core/legacy-detector.js +66 -0
- package/dist/core/logger.d.ts +34 -0
- package/dist/core/logger.js +121 -0
- package/dist/core/mcp-config.d.ts +44 -0
- package/dist/core/mcp-config.js +177 -0
- package/dist/core/notepad.d.ts +31 -0
- package/dist/core/notepad.js +88 -0
- package/dist/core/paths.d.ts +85 -0
- package/dist/core/paths.js +101 -0
- package/dist/core/plugin-detector.d.ts +44 -0
- package/dist/core/plugin-detector.js +226 -0
- package/dist/core/runtime-detector.d.ts +8 -0
- package/dist/core/runtime-detector.js +49 -0
- package/dist/core/scope-resolver.d.ts +8 -0
- package/dist/core/scope-resolver.js +45 -0
- package/dist/core/session-logger.d.ts +6 -0
- package/dist/core/session-logger.js +111 -0
- package/dist/core/session-store.d.ts +28 -0
- package/dist/core/session-store.js +218 -0
- package/dist/core/settings-lock.d.ts +18 -0
- package/dist/core/settings-lock.js +125 -0
- package/dist/core/spawn.d.ts +3 -0
- package/dist/core/spawn.js +135 -0
- package/dist/core/types.d.ts +108 -0
- package/dist/core/types.js +1 -0
- package/dist/core/uninstall.d.ts +4 -0
- package/dist/core/uninstall.js +307 -0
- package/dist/core/v1-bootstrap.d.ts +26 -0
- package/dist/core/v1-bootstrap.js +155 -0
- package/dist/engine/compound-cli.d.ts +24 -0
- package/dist/engine/compound-cli.js +250 -0
- package/dist/engine/compound-extractor.d.ts +68 -0
- package/dist/engine/compound-extractor.js +860 -0
- package/dist/engine/compound-lifecycle.d.ts +32 -0
- package/dist/engine/compound-lifecycle.js +305 -0
- package/dist/engine/compound-loop.d.ts +32 -0
- package/dist/engine/compound-loop.js +511 -0
- package/dist/engine/match-eval-log.d.ts +139 -0
- package/dist/engine/match-eval-log.js +270 -0
- package/dist/engine/phrase-blocklist.d.ts +119 -0
- package/dist/engine/phrase-blocklist.js +208 -0
- package/dist/engine/skill-promoter.d.ts +20 -0
- package/dist/engine/skill-promoter.js +115 -0
- package/dist/engine/solution-format.d.ts +160 -0
- package/dist/engine/solution-format.js +432 -0
- package/dist/engine/solution-index.d.ts +13 -0
- package/dist/engine/solution-index.js +252 -0
- package/dist/engine/solution-matcher.d.ts +364 -0
- package/dist/engine/solution-matcher.js +656 -0
- package/dist/engine/solution-writer.d.ts +76 -0
- package/dist/engine/solution-writer.js +157 -0
- package/dist/engine/term-matcher.d.ts +81 -0
- package/dist/engine/term-matcher.js +268 -0
- package/dist/engine/term-normalizer.d.ts +116 -0
- package/dist/engine/term-normalizer.js +171 -0
- package/dist/fgx.d.ts +6 -0
- package/dist/fgx.js +42 -0
- package/dist/forge/cli.d.ts +11 -0
- package/dist/forge/cli.js +100 -0
- package/dist/forge/evidence-processor.d.ts +21 -0
- package/dist/forge/evidence-processor.js +87 -0
- package/dist/forge/mismatch-detector.d.ts +44 -0
- package/dist/forge/mismatch-detector.js +83 -0
- package/dist/forge/onboarding-cli.d.ts +6 -0
- package/dist/forge/onboarding-cli.js +89 -0
- package/dist/forge/onboarding.d.ts +25 -0
- package/dist/forge/onboarding.js +122 -0
- package/dist/hooks/compound-reflection.d.ts +45 -0
- package/dist/hooks/compound-reflection.js +82 -0
- package/dist/hooks/context-guard.d.ts +24 -0
- package/dist/hooks/context-guard.js +156 -0
- package/dist/hooks/dangerous-patterns.json +18 -0
- package/dist/hooks/db-guard.d.ts +17 -0
- package/dist/hooks/db-guard.js +105 -0
- package/dist/hooks/hook-config.d.ts +29 -0
- package/dist/hooks/hook-config.js +92 -0
- package/dist/hooks/hook-registry.d.ts +43 -0
- package/dist/hooks/hook-registry.js +31 -0
- package/dist/hooks/hooks-generator.d.ts +49 -0
- package/dist/hooks/hooks-generator.js +99 -0
- package/dist/hooks/intent-classifier.d.ts +12 -0
- package/dist/hooks/intent-classifier.js +62 -0
- package/dist/hooks/keyword-detector.d.ts +25 -0
- package/dist/hooks/keyword-detector.js +389 -0
- package/dist/hooks/notepad-injector.d.ts +18 -0
- package/dist/hooks/notepad-injector.js +51 -0
- package/dist/hooks/permission-handler.d.ts +14 -0
- package/dist/hooks/permission-handler.js +114 -0
- package/dist/hooks/post-tool-failure.d.ts +11 -0
- package/dist/hooks/post-tool-failure.js +118 -0
- package/dist/hooks/post-tool-handlers.d.ts +17 -0
- package/dist/hooks/post-tool-handlers.js +115 -0
- package/dist/hooks/post-tool-use.d.ts +29 -0
- package/dist/hooks/post-tool-use.js +151 -0
- package/dist/hooks/pre-compact.d.ts +10 -0
- package/dist/hooks/pre-compact.js +165 -0
- package/dist/hooks/pre-tool-use.d.ts +31 -0
- package/dist/hooks/pre-tool-use.js +325 -0
- package/dist/hooks/prompt-injection-filter.d.ts +56 -0
- package/dist/hooks/prompt-injection-filter.js +287 -0
- package/dist/hooks/rate-limiter.d.ts +21 -0
- package/dist/hooks/rate-limiter.js +86 -0
- package/dist/hooks/secret-filter.d.ts +14 -0
- package/dist/hooks/secret-filter.js +65 -0
- package/dist/hooks/session-recovery.d.ts +27 -0
- package/dist/hooks/session-recovery.js +406 -0
- package/dist/hooks/shared/atomic-write.d.ts +41 -0
- package/dist/hooks/shared/atomic-write.js +148 -0
- package/dist/hooks/shared/context-budget.d.ts +37 -0
- package/dist/hooks/shared/context-budget.js +45 -0
- package/dist/hooks/shared/file-lock.d.ts +56 -0
- package/dist/hooks/shared/file-lock.js +253 -0
- package/dist/hooks/shared/hook-response.d.ts +33 -0
- package/dist/hooks/shared/hook-response.js +62 -0
- package/dist/hooks/shared/injection-caps.d.ts +39 -0
- package/dist/hooks/shared/injection-caps.js +52 -0
- package/dist/hooks/shared/plugin-signal.d.ts +23 -0
- package/dist/hooks/shared/plugin-signal.js +104 -0
- package/dist/hooks/shared/read-stdin.d.ts +8 -0
- package/dist/hooks/shared/read-stdin.js +63 -0
- package/dist/hooks/shared/sanitize-id.d.ts +7 -0
- package/dist/hooks/shared/sanitize-id.js +9 -0
- package/dist/hooks/shared/sanitize.d.ts +7 -0
- package/dist/hooks/shared/sanitize.js +22 -0
- package/dist/hooks/skill-injector.d.ts +38 -0
- package/dist/hooks/skill-injector.js +285 -0
- package/dist/hooks/slop-detector.d.ts +18 -0
- package/dist/hooks/slop-detector.js +93 -0
- package/dist/hooks/solution-injector.d.ts +58 -0
- package/dist/hooks/solution-injector.js +436 -0
- package/dist/hooks/subagent-tracker.d.ts +10 -0
- package/dist/hooks/subagent-tracker.js +90 -0
- package/dist/i18n/index.d.ts +43 -0
- package/dist/i18n/index.js +224 -0
- package/dist/lib.d.ts +14 -0
- package/dist/lib.js +14 -0
- package/dist/mcp/server.d.ts +8 -0
- package/dist/mcp/server.js +40 -0
- package/dist/mcp/solution-reader.d.ts +90 -0
- package/dist/mcp/solution-reader.js +273 -0
- package/dist/mcp/tools.d.ts +16 -0
- package/dist/mcp/tools.js +302 -0
- package/dist/preset/facet-catalog.d.ts +17 -0
- package/dist/preset/facet-catalog.js +46 -0
- package/dist/preset/preset-manager.d.ts +31 -0
- package/dist/preset/preset-manager.js +111 -0
- package/dist/renderer/inspect-renderer.d.ts +11 -0
- package/dist/renderer/inspect-renderer.js +123 -0
- package/dist/renderer/rule-renderer.d.ts +18 -0
- package/dist/renderer/rule-renderer.js +159 -0
- package/dist/store/evidence-store.d.ts +23 -0
- package/dist/store/evidence-store.js +58 -0
- package/dist/store/profile-store.d.ts +12 -0
- package/dist/store/profile-store.js +53 -0
- package/dist/store/recommendation-store.d.ts +22 -0
- package/dist/store/recommendation-store.js +64 -0
- package/dist/store/rule-store.d.ts +22 -0
- package/dist/store/rule-store.js +62 -0
- package/dist/store/session-state-store.d.ts +11 -0
- package/dist/store/session-state-store.js +44 -0
- package/dist/store/types.d.ts +159 -0
- package/dist/store/types.js +7 -0
- package/hooks/hook-registry.json +21 -0
- package/hooks/hooks.json +185 -0
- package/package.json +89 -0
- package/plugin.json +20 -0
- package/scripts/postinstall.js +826 -0
- package/skills/api-design/SKILL.md +262 -0
- package/skills/architecture-decision/SKILL.md +309 -0
- package/skills/ci-cd/SKILL.md +264 -0
- package/skills/code-review/SKILL.md +228 -0
- package/skills/compound/SKILL.md +101 -0
- package/skills/database/SKILL.md +257 -0
- package/skills/debug-detective/SKILL.md +95 -0
- package/skills/docker/SKILL.md +268 -0
- package/skills/documentation/SKILL.md +270 -0
- package/skills/ecomode/SKILL.md +46 -0
- package/skills/frontend/SKILL.md +265 -0
- package/skills/git-master/SKILL.md +86 -0
- package/skills/incident-response/SKILL.md +286 -0
- package/skills/migrate/SKILL.md +96 -0
- package/skills/performance/SKILL.md +282 -0
- package/skills/refactor/SKILL.md +100 -0
- package/skills/security-review/SKILL.md +282 -0
- package/skills/tdd/SKILL.md +178 -0
- package/skills/testing-strategy/SKILL.md +260 -0
- package/starter-pack/solutions/starter-api-error-responses.md +37 -0
- package/starter-pack/solutions/starter-async-patterns.md +40 -0
- package/starter-pack/solutions/starter-caching-strategy.md +40 -0
- package/starter-pack/solutions/starter-code-review-checklist.md +39 -0
- package/starter-pack/solutions/starter-debugging-systematic.md +40 -0
- package/starter-pack/solutions/starter-dependency-injection.md +40 -0
- package/starter-pack/solutions/starter-error-handling-patterns.md +38 -0
- package/starter-pack/solutions/starter-git-atomic-commits.md +36 -0
- package/starter-pack/solutions/starter-input-validation.md +40 -0
- package/starter-pack/solutions/starter-n-plus-one-queries.md +37 -0
- package/starter-pack/solutions/starter-refactor-safely.md +38 -0
- package/starter-pack/solutions/starter-secret-management.md +37 -0
- package/starter-pack/solutions/starter-separation-of-concerns.md +36 -0
- package/starter-pack/solutions/starter-tdd-red-green-refactor.md +40 -0
- package/starter-pack/solutions/starter-typescript-strict-types.md +39 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen — Injection Caps
|
|
3
|
+
*
|
|
4
|
+
* 컨텍스트 주입 하드캡 상수. 다른 플러그인과 공존 시 컨텍스트 윈도우를
|
|
5
|
+
* 과도하게 점유하지 않도록 각 훅의 최대 주입량을 제한합니다.
|
|
6
|
+
*
|
|
7
|
+
* 이 값들은 어댑티브 버짓 시스템의 "절대 상한"이며,
|
|
8
|
+
* 실제 주입량은 context-budget.ts에서 동적으로 조절됩니다.
|
|
9
|
+
*/
|
|
10
|
+
export const INJECTION_CAPS = {
|
|
11
|
+
/** notepad-injector: 노트패드 최대 주입 글자 수 (현재: 무제한 → 2000) */
|
|
12
|
+
notepadMax: 2000,
|
|
13
|
+
/** skill-injector: 스킬 파일 하나당 최대 글자 수 (현재: 무제한 → 3000) */
|
|
14
|
+
skillContentMax: 3000,
|
|
15
|
+
/** solution-injector: 솔루션 하나당 최대 글자 수 (기존 값 유지) */
|
|
16
|
+
solutionMax: 1500,
|
|
17
|
+
/** solution-injector: 세션 전체 주입 상한 (기존 값 유지) */
|
|
18
|
+
solutionSessionMax: 8000,
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* .claude/rules/ 자동생성 파일의 사이즈 하드캡.
|
|
22
|
+
* rules 파일은 Claude Code가 세션 시작 시 전부 로드하므로
|
|
23
|
+
* 무제한 성장을 방지해야 합니다.
|
|
24
|
+
*
|
|
25
|
+
* 근거: Claude Code 공식 권장 — "CLAUDE.md 파일당 200줄 이하",
|
|
26
|
+
* "길수록 지시 준수율 저하". 3000자 ≈ 60~80줄.
|
|
27
|
+
*/
|
|
28
|
+
export const RULE_FILE_CAPS = {
|
|
29
|
+
/** .claude/rules/ 파일 1개당 최대 글자 수 */
|
|
30
|
+
perRuleFile: 3000,
|
|
31
|
+
/** .claude/rules/에 forgen가 쓸 수 있는 총량 (글자) */
|
|
32
|
+
totalRuleFiles: 15000,
|
|
33
|
+
/** compound.md에 포함할 rule summary 최대 수 */
|
|
34
|
+
maxRuleSummaries: 10,
|
|
35
|
+
};
|
|
36
|
+
/** truncation 시 끝에 추가되는 표시 */
|
|
37
|
+
export const TRUNCATION_SUFFIX = '\n... (truncated)';
|
|
38
|
+
/** 주어진 content를 maxChars로 잘라서 반환. 초과 시 truncation suffix 추가. */
|
|
39
|
+
export function truncateContent(content, maxChars) {
|
|
40
|
+
if (content.length <= maxChars)
|
|
41
|
+
return content;
|
|
42
|
+
const cutAt = Math.max(0, maxChars - TRUNCATION_SUFFIX.length);
|
|
43
|
+
let sliced = content.slice(0, cutAt);
|
|
44
|
+
// UTF-16 서로게이트 쌍 분리 방지: 마지막 문자가 high surrogate이면 제거
|
|
45
|
+
if (sliced.length > 0) {
|
|
46
|
+
const lastCode = sliced.charCodeAt(sliced.length - 1);
|
|
47
|
+
if (lastCode >= 0xD800 && lastCode <= 0xDBFF) {
|
|
48
|
+
sliced = sliced.slice(0, -1);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return sliced + TRUNCATION_SUFFIX;
|
|
52
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen — Plugin Signal Protocol
|
|
3
|
+
*
|
|
4
|
+
* 플러그인 간 컨텍스트 주입량을 상호 인지하기 위한 파일 기반 시그널.
|
|
5
|
+
* 현재는 forgen만 시그널을 쓰지만, 다른 플러그인도 같은 프로토콜을
|
|
6
|
+
* 채택하면 동적 버짓 조율이 가능해집니다.
|
|
7
|
+
*
|
|
8
|
+
* 시그널 디렉토리: ~/.claude/plugin-signals/
|
|
9
|
+
* 파일 형식: {pluginName}-{sessionId}.json
|
|
10
|
+
* TTL: 30분 (자동 정리)
|
|
11
|
+
*/
|
|
12
|
+
export interface PluginSignal {
|
|
13
|
+
pluginName: string;
|
|
14
|
+
hookEvent: string;
|
|
15
|
+
charsInjected: number;
|
|
16
|
+
timestamp: string;
|
|
17
|
+
}
|
|
18
|
+
/** forgen의 주입 시그널을 기록합니다 */
|
|
19
|
+
export declare function writeSignal(sessionId: string, hookEvent: string, charsInjected: number): void;
|
|
20
|
+
/** stale 시그널 파일을 정리합니다 (읽기와 분리된 side-effect) */
|
|
21
|
+
export declare function cleanupStaleSignals(): void;
|
|
22
|
+
/** 다른 플러그인의 시그널을 읽어서 총 주입량을 반환합니다 (읽기 전용) */
|
|
23
|
+
export declare function readOtherSignals(_sessionId: string): PluginSignal[];
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen — Plugin Signal Protocol
|
|
3
|
+
*
|
|
4
|
+
* 플러그인 간 컨텍스트 주입량을 상호 인지하기 위한 파일 기반 시그널.
|
|
5
|
+
* 현재는 forgen만 시그널을 쓰지만, 다른 플러그인도 같은 프로토콜을
|
|
6
|
+
* 채택하면 동적 버짓 조율이 가능해집니다.
|
|
7
|
+
*
|
|
8
|
+
* 시그널 디렉토리: ~/.claude/plugin-signals/
|
|
9
|
+
* 파일 형식: {pluginName}-{sessionId}.json
|
|
10
|
+
* TTL: 30분 (자동 정리)
|
|
11
|
+
*/
|
|
12
|
+
import * as fs from 'node:fs';
|
|
13
|
+
import * as path from 'node:path';
|
|
14
|
+
import { CLAUDE_DIR } from '../../core/paths.js';
|
|
15
|
+
const SIGNAL_DIR = path.join(CLAUDE_DIR, 'plugin-signals');
|
|
16
|
+
const SIGNAL_TTL_MS = 30 * 60 * 1000; // 30분
|
|
17
|
+
/** forgen의 주입 시그널을 기록합니다 */
|
|
18
|
+
export function writeSignal(sessionId, hookEvent, charsInjected) {
|
|
19
|
+
try {
|
|
20
|
+
fs.mkdirSync(SIGNAL_DIR, { recursive: true });
|
|
21
|
+
// 경로 탈출 방지: sessionId에서 디렉토리 구분자와 위험 문자 제거
|
|
22
|
+
const safeId = sessionId.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
23
|
+
const filePath = path.join(SIGNAL_DIR, `forgen-${safeId}.json`);
|
|
24
|
+
const signal = {
|
|
25
|
+
pluginName: 'forgen',
|
|
26
|
+
hookEvent,
|
|
27
|
+
charsInjected,
|
|
28
|
+
timestamp: new Date().toISOString(),
|
|
29
|
+
};
|
|
30
|
+
fs.writeFileSync(filePath, JSON.stringify(signal));
|
|
31
|
+
// 쓰기 시점에 stale 시그널 정리 (읽기와 분리)
|
|
32
|
+
cleanupStaleSignals();
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// 시그널 기록 실패는 무시
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** stale 시그널 파일을 정리합니다 (읽기와 분리된 side-effect) */
|
|
39
|
+
export function cleanupStaleSignals() {
|
|
40
|
+
try {
|
|
41
|
+
if (!fs.existsSync(SIGNAL_DIR))
|
|
42
|
+
return;
|
|
43
|
+
const now = Date.now();
|
|
44
|
+
for (const file of fs.readdirSync(SIGNAL_DIR)) {
|
|
45
|
+
if (!file.endsWith('.json'))
|
|
46
|
+
continue;
|
|
47
|
+
try {
|
|
48
|
+
const filePath = path.join(SIGNAL_DIR, file);
|
|
49
|
+
// 파일 크기 체크 — 1KB 초과 시그널은 비정상 (DoS 방지)
|
|
50
|
+
const stat = fs.statSync(filePath);
|
|
51
|
+
if (stat.size > 1024) {
|
|
52
|
+
fs.unlinkSync(filePath);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
56
|
+
const age = now - new Date(data.timestamp).getTime();
|
|
57
|
+
if (!Number.isFinite(age) || age > SIGNAL_TTL_MS) {
|
|
58
|
+
fs.unlinkSync(filePath);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// 개별 파일 처리 실패는 무시
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// 디렉토리 접근 실패
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** 다른 플러그인의 시그널을 읽어서 총 주입량을 반환합니다 (읽기 전용) */
|
|
71
|
+
export function readOtherSignals(_sessionId) {
|
|
72
|
+
const signals = [];
|
|
73
|
+
try {
|
|
74
|
+
if (!fs.existsSync(SIGNAL_DIR))
|
|
75
|
+
return signals;
|
|
76
|
+
const now = Date.now();
|
|
77
|
+
for (const file of fs.readdirSync(SIGNAL_DIR)) {
|
|
78
|
+
if (file.startsWith('forgen-'))
|
|
79
|
+
continue;
|
|
80
|
+
if (!file.endsWith('.json'))
|
|
81
|
+
continue;
|
|
82
|
+
try {
|
|
83
|
+
const filePath = path.join(SIGNAL_DIR, file);
|
|
84
|
+
// 파일 크기 체크 — 1KB 초과 시그널은 비정상
|
|
85
|
+
const stat = fs.statSync(filePath);
|
|
86
|
+
if (stat.size > 1024)
|
|
87
|
+
continue;
|
|
88
|
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
89
|
+
const age = now - new Date(data.timestamp).getTime();
|
|
90
|
+
// TTL 초과는 건너뜀 (삭제는 cleanupStaleSignals에서)
|
|
91
|
+
if (!Number.isFinite(age) || age > SIGNAL_TTL_MS)
|
|
92
|
+
continue;
|
|
93
|
+
signals.push(data);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// 개별 시그널 읽기 실패는 무시
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// 디렉토리 접근 실패
|
|
102
|
+
}
|
|
103
|
+
return signals;
|
|
104
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 훅 공유 유틸: timeout-protected stdin 읽기
|
|
3
|
+
*
|
|
4
|
+
* event-based 패턴으로 Linux에서 hang을 방지합니다.
|
|
5
|
+
* (for await of process.stdin은 일부 환경에서 hang 발생)
|
|
6
|
+
*/
|
|
7
|
+
/** stdin에서 JSON 데이터를 읽어 파싱. 실패 시 null 반환. */
|
|
8
|
+
export declare function readStdinJSON<T = Record<string, unknown>>(timeoutMs?: number): Promise<T | null>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 훅 공유 유틸: timeout-protected stdin 읽기
|
|
3
|
+
*
|
|
4
|
+
* event-based 패턴으로 Linux에서 hang을 방지합니다.
|
|
5
|
+
* (for await of process.stdin은 일부 환경에서 hang 발생)
|
|
6
|
+
*/
|
|
7
|
+
const MAX_STDIN_BYTES = 10 * 1024 * 1024; // 10MB — 메모리 고갈 방지
|
|
8
|
+
/** stdin에서 JSON 데이터를 읽어 파싱. 실패 시 null 반환. */
|
|
9
|
+
export async function readStdinJSON(timeoutMs = 2000) {
|
|
10
|
+
const chunks = [];
|
|
11
|
+
let totalSize = 0;
|
|
12
|
+
let settled = false;
|
|
13
|
+
const raw = await new Promise((resolve) => {
|
|
14
|
+
const timeout = setTimeout(() => {
|
|
15
|
+
if (!settled) {
|
|
16
|
+
settled = true;
|
|
17
|
+
process.stdin.removeAllListeners();
|
|
18
|
+
process.stdin.pause();
|
|
19
|
+
resolve(Buffer.concat(chunks).toString('utf-8'));
|
|
20
|
+
}
|
|
21
|
+
}, timeoutMs);
|
|
22
|
+
// 일부 Node.js 환경에서 stdin이 paused 상태로 시작 — 명시적 resume 필요
|
|
23
|
+
if (typeof process.stdin.resume === 'function') {
|
|
24
|
+
process.stdin.resume();
|
|
25
|
+
}
|
|
26
|
+
process.stdin.on('data', (chunk) => {
|
|
27
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
28
|
+
totalSize += buf.length;
|
|
29
|
+
if (totalSize > MAX_STDIN_BYTES) {
|
|
30
|
+
if (!settled) {
|
|
31
|
+
settled = true;
|
|
32
|
+
clearTimeout(timeout);
|
|
33
|
+
process.stdin.removeAllListeners();
|
|
34
|
+
if (typeof process.stdin.pause === 'function')
|
|
35
|
+
process.stdin.pause();
|
|
36
|
+
resolve('');
|
|
37
|
+
}
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
chunks.push(buf);
|
|
41
|
+
});
|
|
42
|
+
process.stdin.on('end', () => {
|
|
43
|
+
if (!settled) {
|
|
44
|
+
settled = true;
|
|
45
|
+
clearTimeout(timeout);
|
|
46
|
+
resolve(Buffer.concat(chunks).toString('utf-8'));
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
process.stdin.on('error', () => {
|
|
50
|
+
if (!settled) {
|
|
51
|
+
settled = true;
|
|
52
|
+
clearTimeout(timeout);
|
|
53
|
+
resolve('');
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
try {
|
|
58
|
+
return JSON.parse(raw);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 훅 공유 유틸: session_id / 파일명에 사용되는 ID 정제
|
|
3
|
+
*
|
|
4
|
+
* path traversal 방지를 위해 영숫자, 하이픈, 언더스코어만 허용합니다.
|
|
5
|
+
*/
|
|
6
|
+
/** ID를 파일명에 안전한 형태로 변환. 위험 문자는 '_'로 치환. */
|
|
7
|
+
export function sanitizeId(id, maxLength = 128) {
|
|
8
|
+
return id.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, maxLength) || 'default';
|
|
9
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 훅 공유 유틸: 키워드/트리거 감지용 프롬프트 정제
|
|
3
|
+
*
|
|
4
|
+
* 코드 블록, URL, XML 태그, 파일 경로를 제거하여
|
|
5
|
+
* 순수 텍스트에서만 키워드를 감지할 수 있게 합니다.
|
|
6
|
+
*/
|
|
7
|
+
export function sanitizeForDetection(prompt) {
|
|
8
|
+
let sanitized = prompt;
|
|
9
|
+
// 코드 블록 제거 (```...```)
|
|
10
|
+
sanitized = sanitized.replace(/```[\s\S]*?```/g, '');
|
|
11
|
+
// 인라인 코드 제거 (`...`)
|
|
12
|
+
sanitized = sanitized.replace(/`[^`]+`/g, '');
|
|
13
|
+
// URL 제거
|
|
14
|
+
sanitized = sanitized.replace(/https?:\/\/\S+/g, '');
|
|
15
|
+
// XML 태그 블록 제거 (열고 닫는 태그 이름 일치)
|
|
16
|
+
sanitized = sanitized.replace(/<(\w[\w-]*)[\s>][\s\S]*?<\/\1>/g, '');
|
|
17
|
+
// 자체 폐쇄 태그 제거
|
|
18
|
+
sanitized = sanitized.replace(/<\w[\w-]*(?:\s[^>]*)?\s*\/>/g, '');
|
|
19
|
+
// 파일 경로 제거 (multi-segment 포함)
|
|
20
|
+
sanitized = sanitized.replace(/(^|[\s"'`(])(?:\.?\/(?:[\w.-]+\/)*[\w.-]+|(?:[\w.-]+\/)+[\w.-]+\.\w+)/gm, '$1');
|
|
21
|
+
return sanitized;
|
|
22
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Forgen — Skill Injector Hook
|
|
4
|
+
*
|
|
5
|
+
* Claude Code UserPromptSubmit 훅으로 등록.
|
|
6
|
+
* 프롬프트와 매칭되는 학습된 스킬을 자동으로 컨텍스트에 주입합니다.
|
|
7
|
+
*
|
|
8
|
+
* 스킬 파일 위치:
|
|
9
|
+
* 1. {project}/.compound/skills/*.md (프로젝트 스킬)
|
|
10
|
+
* 2. ~/.compound/skills/*.md (글로벌 스킬)
|
|
11
|
+
* 3. ~/.compound/me/skills/*.md (개인 학습 스킬)
|
|
12
|
+
*
|
|
13
|
+
* 스킬 포맷:
|
|
14
|
+
* ---
|
|
15
|
+
* name: my-skill
|
|
16
|
+
* description: What this skill does
|
|
17
|
+
* triggers:
|
|
18
|
+
* - "keyword1"
|
|
19
|
+
* - "keyword2"
|
|
20
|
+
* ---
|
|
21
|
+
* <Purpose>...</Purpose>
|
|
22
|
+
* <Steps>...</Steps>
|
|
23
|
+
*/
|
|
24
|
+
export interface SkillMeta {
|
|
25
|
+
name: string;
|
|
26
|
+
description: string;
|
|
27
|
+
triggers: string[];
|
|
28
|
+
filePath: string;
|
|
29
|
+
content: string;
|
|
30
|
+
}
|
|
31
|
+
/** YAML frontmatter 파싱 (간단한 구현) */
|
|
32
|
+
export declare function parseFrontmatter(content: string): {
|
|
33
|
+
meta: Record<string, unknown>;
|
|
34
|
+
body: string;
|
|
35
|
+
};
|
|
36
|
+
/** 프롬프트와 스킬 트리거 매칭 (sanitized 텍스트에서만)
|
|
37
|
+
* keyword-detector가 이미 처리하는 스킬은 제외하여 이중 주입을 방지합니다. */
|
|
38
|
+
export declare function matchSkills(prompt: string, skills: SkillMeta[]): SkillMeta[];
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Forgen — Skill Injector Hook
|
|
4
|
+
*
|
|
5
|
+
* Claude Code UserPromptSubmit 훅으로 등록.
|
|
6
|
+
* 프롬프트와 매칭되는 학습된 스킬을 자동으로 컨텍스트에 주입합니다.
|
|
7
|
+
*
|
|
8
|
+
* 스킬 파일 위치:
|
|
9
|
+
* 1. {project}/.compound/skills/*.md (프로젝트 스킬)
|
|
10
|
+
* 2. ~/.compound/skills/*.md (글로벌 스킬)
|
|
11
|
+
* 3. ~/.compound/me/skills/*.md (개인 학습 스킬)
|
|
12
|
+
*
|
|
13
|
+
* 스킬 포맷:
|
|
14
|
+
* ---
|
|
15
|
+
* name: my-skill
|
|
16
|
+
* description: What this skill does
|
|
17
|
+
* triggers:
|
|
18
|
+
* - "keyword1"
|
|
19
|
+
* - "keyword2"
|
|
20
|
+
* ---
|
|
21
|
+
* <Purpose>...</Purpose>
|
|
22
|
+
* <Steps>...</Steps>
|
|
23
|
+
*/
|
|
24
|
+
import * as fs from 'node:fs';
|
|
25
|
+
import * as path from 'node:path';
|
|
26
|
+
import { fileURLToPath } from 'node:url';
|
|
27
|
+
import { createLogger } from '../core/logger.js';
|
|
28
|
+
const log = createLogger('skill-injector');
|
|
29
|
+
import { readStdinJSON } from './shared/read-stdin.js';
|
|
30
|
+
import { sanitizeForDetection } from './shared/sanitize.js';
|
|
31
|
+
import { sanitizeId } from './shared/sanitize-id.js';
|
|
32
|
+
import { escapeAllXmlTags } from './prompt-injection-filter.js';
|
|
33
|
+
function escapeXmlAttr(s) {
|
|
34
|
+
return s.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
35
|
+
}
|
|
36
|
+
import { atomicWriteJSON } from './shared/atomic-write.js';
|
|
37
|
+
import { FORGEN_HOME, ME_DIR, STATE_DIR } from '../core/paths.js';
|
|
38
|
+
import { KEYWORD_PATTERNS } from './keyword-detector.js';
|
|
39
|
+
import { isHookEnabled } from './hook-config.js';
|
|
40
|
+
import { approve, approveWithContext, failOpen } from './shared/hook-response.js';
|
|
41
|
+
/** keyword-detector가 처리하는 키워드 이름 집합 (skill + inject 모두 포함, 이중 주입 방지) */
|
|
42
|
+
const KEYWORD_DETECTOR_SKILL_NAMES = new Set(KEYWORD_PATTERNS
|
|
43
|
+
.filter(p => p.type === 'skill' || p.type === 'inject')
|
|
44
|
+
.map(p => p.skill ?? p.keyword));
|
|
45
|
+
const MAX_SKILLS_PER_SESSION = 5;
|
|
46
|
+
/** 파일 기반 세션 캐시 (훅은 매번 새 프로세스로 실행되므로 in-memory 불가) */
|
|
47
|
+
function getSessionCachePath(sessionId) {
|
|
48
|
+
return path.join(STATE_DIR, `skill-cache-${sanitizeId(sessionId)}.json`);
|
|
49
|
+
}
|
|
50
|
+
function loadSessionCache(sessionId) {
|
|
51
|
+
const cachePath = getSessionCachePath(sessionId);
|
|
52
|
+
try {
|
|
53
|
+
if (fs.existsSync(cachePath)) {
|
|
54
|
+
const data = JSON.parse(fs.readFileSync(cachePath, 'utf-8'));
|
|
55
|
+
// 24시간 초과 시 만료
|
|
56
|
+
if (data.updatedAt && Date.now() - new Date(data.updatedAt).getTime() > 24 * 60 * 60 * 1000) {
|
|
57
|
+
fs.unlinkSync(cachePath);
|
|
58
|
+
return new Set();
|
|
59
|
+
}
|
|
60
|
+
return new Set(data.injected ?? []);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
log.debug('세션 캐시 파일 읽기/파싱 실패', e);
|
|
65
|
+
}
|
|
66
|
+
return new Set();
|
|
67
|
+
}
|
|
68
|
+
function saveSessionCache(sessionId, injected) {
|
|
69
|
+
atomicWriteJSON(getSessionCachePath(sessionId), {
|
|
70
|
+
injected: [...injected],
|
|
71
|
+
updatedAt: new Date().toISOString(),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/** YAML frontmatter 파싱 (간단한 구현) */
|
|
75
|
+
export function parseFrontmatter(content) {
|
|
76
|
+
const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
77
|
+
if (!match)
|
|
78
|
+
return { meta: {}, body: content };
|
|
79
|
+
const meta = {};
|
|
80
|
+
const yamlLines = match[1].split('\n');
|
|
81
|
+
let currentKey = '';
|
|
82
|
+
let inArray = false;
|
|
83
|
+
const arrayValues = [];
|
|
84
|
+
for (const line of yamlLines) {
|
|
85
|
+
const trimmed = line.trim();
|
|
86
|
+
if (inArray) {
|
|
87
|
+
if (trimmed.startsWith('- ')) {
|
|
88
|
+
arrayValues.push(trimmed.slice(2).replace(/^["']|["']$/g, ''));
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
meta[currentKey] = [...arrayValues];
|
|
93
|
+
arrayValues.length = 0;
|
|
94
|
+
inArray = false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const kvMatch = trimmed.match(/^(\w[\w-]*)\s*:\s*(.*)$/);
|
|
98
|
+
if (kvMatch) {
|
|
99
|
+
currentKey = kvMatch[1];
|
|
100
|
+
const val = kvMatch[2].trim();
|
|
101
|
+
if (val === '' || val === '[]') {
|
|
102
|
+
// 다음 줄이 배열일 수 있음
|
|
103
|
+
inArray = true;
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
meta[currentKey] = val.replace(/^["']|["']$/g, '');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (inArray && arrayValues.length > 0) {
|
|
111
|
+
meta[currentKey] = arrayValues;
|
|
112
|
+
}
|
|
113
|
+
return { meta, body: match[2] };
|
|
114
|
+
}
|
|
115
|
+
/** 디렉토리에서 스킬 파일 스캔 */
|
|
116
|
+
function scanSkills(dir) {
|
|
117
|
+
if (!fs.existsSync(dir))
|
|
118
|
+
return [];
|
|
119
|
+
try {
|
|
120
|
+
return fs.readdirSync(dir)
|
|
121
|
+
.filter(f => f.endsWith('.md'))
|
|
122
|
+
.filter(f => {
|
|
123
|
+
const filePath = path.join(dir, f);
|
|
124
|
+
try {
|
|
125
|
+
return !fs.lstatSync(filePath).isSymbolicLink();
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
.map(f => {
|
|
132
|
+
const filePath = path.join(dir, f);
|
|
133
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
134
|
+
const { meta, body } = parseFrontmatter(raw);
|
|
135
|
+
return {
|
|
136
|
+
name: meta.name ?? f.replace('.md', ''),
|
|
137
|
+
description: meta.description ?? '',
|
|
138
|
+
triggers: meta.triggers ?? [],
|
|
139
|
+
filePath,
|
|
140
|
+
content: body.trim(),
|
|
141
|
+
};
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
catch (e) {
|
|
145
|
+
log.debug(`스킬 디렉토리 스캔 실패: ${dir}`, e);
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/** 모든 스킬 소스에서 스킬 수집 */
|
|
150
|
+
function collectSkills() {
|
|
151
|
+
const skills = [];
|
|
152
|
+
const seen = new Map(); // name → source dir
|
|
153
|
+
// 패키지 내장 스킬 경로 (dist/../skills/)
|
|
154
|
+
const pkgSkillsDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'commands');
|
|
155
|
+
// v1: 팀 팩 스킬 제거. 프로젝트 > 개인 > 글로벌 > 패키지 내장
|
|
156
|
+
const dirs = [
|
|
157
|
+
path.join(process.cwd(), '.compound', 'skills'),
|
|
158
|
+
path.join(ME_DIR, 'skills'),
|
|
159
|
+
path.join(FORGEN_HOME, 'skills'),
|
|
160
|
+
pkgSkillsDir,
|
|
161
|
+
];
|
|
162
|
+
const overrides = [];
|
|
163
|
+
for (const dir of dirs) {
|
|
164
|
+
for (const skill of scanSkills(dir)) {
|
|
165
|
+
if (!seen.has(skill.name)) {
|
|
166
|
+
seen.set(skill.name, dir);
|
|
167
|
+
skills.push(skill);
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
// 팩 스킬이 무시된 경우 기록
|
|
171
|
+
const winnerDir = seen.get(skill.name) ?? '';
|
|
172
|
+
if (dir.includes('/packs/') || winnerDir.includes('/packs/')) {
|
|
173
|
+
overrides.push({ name: skill.name, winner: winnerDir, loser: dir });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (overrides.length > 0) {
|
|
179
|
+
for (const o of overrides) {
|
|
180
|
+
log.debug(`⚠ 스킬 '${o.name}' 오버라이드: ${path.basename(path.dirname(o.winner))} 우선, ${path.basename(path.dirname(o.loser))} 무시됨`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return skills;
|
|
184
|
+
}
|
|
185
|
+
/** 프롬프트와 스킬 트리거 매칭 (sanitized 텍스트에서만)
|
|
186
|
+
* keyword-detector가 이미 처리하는 스킬은 제외하여 이중 주입을 방지합니다. */
|
|
187
|
+
export function matchSkills(prompt, skills) {
|
|
188
|
+
const sanitized = sanitizeForDetection(prompt);
|
|
189
|
+
const lower = sanitized.toLowerCase();
|
|
190
|
+
return skills.filter(skill => {
|
|
191
|
+
if (skill.triggers.length === 0)
|
|
192
|
+
return false;
|
|
193
|
+
// keyword-detector가 처리하는 스킬은 skill-injector에서 주입하지 않음
|
|
194
|
+
if (KEYWORD_DETECTOR_SKILL_NAMES.has(skill.name))
|
|
195
|
+
return false;
|
|
196
|
+
return skill.triggers.some(trigger => lower.includes(trigger.toLowerCase()));
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
/** 오래된 skill-cache 파일 가비지 컬렉션 (1시간마다 실행) */
|
|
200
|
+
function cleanStaleSkillCaches() {
|
|
201
|
+
if (!fs.existsSync(STATE_DIR))
|
|
202
|
+
return;
|
|
203
|
+
// W-D3: 매 프롬프트 대신 1시간 간격으로 실행
|
|
204
|
+
const markerPath = path.join(STATE_DIR, '.last-cache-gc');
|
|
205
|
+
try {
|
|
206
|
+
if (fs.existsSync(markerPath)) {
|
|
207
|
+
const lastGc = fs.statSync(markerPath).mtimeMs;
|
|
208
|
+
if (Date.now() - lastGc < 60 * 60 * 1000)
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
catch { /* marker read failure — proceed with GC */ }
|
|
213
|
+
try {
|
|
214
|
+
const now = Date.now();
|
|
215
|
+
for (const f of fs.readdirSync(STATE_DIR)) {
|
|
216
|
+
if (!f.startsWith('skill-cache-'))
|
|
217
|
+
continue;
|
|
218
|
+
const p = path.join(STATE_DIR, f);
|
|
219
|
+
const stat = fs.statSync(p);
|
|
220
|
+
if (now - stat.mtimeMs > 24 * 60 * 60 * 1000) {
|
|
221
|
+
fs.unlinkSync(p);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
fs.writeFileSync(markerPath, '');
|
|
225
|
+
}
|
|
226
|
+
catch (e) {
|
|
227
|
+
log.debug('오래된 캐시 파일 삭제 실패', e);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// ── 메인 ──
|
|
231
|
+
async function main() {
|
|
232
|
+
const input = await readStdinJSON();
|
|
233
|
+
if (!isHookEnabled('skill-injector')) {
|
|
234
|
+
console.log(approve());
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (!input?.prompt) {
|
|
238
|
+
console.log(approve());
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const sessionId = input.session_id ?? 'default';
|
|
242
|
+
// 오래된 캐시 파일 정리 (가비지 컬렉션)
|
|
243
|
+
cleanStaleSkillCaches();
|
|
244
|
+
// 파일 기반 세션 캐시 로드
|
|
245
|
+
const injected = loadSessionCache(sessionId);
|
|
246
|
+
// 이미 최대치 주입했으면 통과
|
|
247
|
+
if (injected.size >= MAX_SKILLS_PER_SESSION) {
|
|
248
|
+
console.log(approve());
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
// 스킬 수집 및 매칭
|
|
252
|
+
const allSkills = collectSkills();
|
|
253
|
+
const matched = matchSkills(input.prompt, allSkills)
|
|
254
|
+
.filter(s => !injected.has(s.name)); // 이미 주입된 것 제외
|
|
255
|
+
if (matched.length === 0) {
|
|
256
|
+
console.log(approve());
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
// 최대 제한 적용
|
|
260
|
+
const toInject = matched.slice(0, MAX_SKILLS_PER_SESSION - injected.size);
|
|
261
|
+
// 파일 기반 캐시 업데이트
|
|
262
|
+
for (const skill of toInject) {
|
|
263
|
+
injected.add(skill.name);
|
|
264
|
+
}
|
|
265
|
+
saveSessionCache(sessionId, injected);
|
|
266
|
+
// Adaptive budget: 다른 플러그인 감지 시 스킬 주입량 축소
|
|
267
|
+
let skillCap = 3000; // INJECTION_CAPS.skillContentMax 기본값
|
|
268
|
+
try {
|
|
269
|
+
const { calculateBudget } = await import('./shared/context-budget.js');
|
|
270
|
+
skillCap = calculateBudget().skillContentMax;
|
|
271
|
+
}
|
|
272
|
+
catch { /* budget 로드 실패 시 기본값 사용 */ }
|
|
273
|
+
// 스킬 컨텍스트 주입 (adaptive cap 적용)
|
|
274
|
+
const injections = toInject.map(skill => {
|
|
275
|
+
const capped = skill.content.length > skillCap
|
|
276
|
+
? `${skill.content.slice(0, skillCap)}\n... (capped)`
|
|
277
|
+
: skill.content;
|
|
278
|
+
return `<compound-learned-skill name="${escapeXmlAttr(skill.name)}" description="${escapeXmlAttr(skill.description)}">\n${escapeAllXmlTags(capped)}\n</compound-learned-skill>`;
|
|
279
|
+
}).join('\n\n');
|
|
280
|
+
console.log(approveWithContext(injections, 'UserPromptSubmit'));
|
|
281
|
+
}
|
|
282
|
+
main().catch((e) => {
|
|
283
|
+
process.stderr.write(`[ch-hook] ${e instanceof Error ? e.message : String(e)}\n`);
|
|
284
|
+
console.log(failOpen());
|
|
285
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Forgen — Slop Detector Hook (PostToolUse)
|
|
4
|
+
*
|
|
5
|
+
* Write/Edit 도구 실행 후 결과물에서 AI 슬롭 패턴을 감지합니다.
|
|
6
|
+
* - TODO 주석 잔류, eslint-disable, @ts-expect-error, as any 등
|
|
7
|
+
* - Empty catch blocks, unnecessary comments, console.log debug code
|
|
8
|
+
*/
|
|
9
|
+
export declare const SLOP_PATTERNS: Array<{
|
|
10
|
+
pattern: RegExp;
|
|
11
|
+
message: string;
|
|
12
|
+
severity: 'warn' | 'info';
|
|
13
|
+
}>;
|
|
14
|
+
/** 텍스트에서 슬롭 패턴을 감지하여 메시지 목록 반환 (순수 함수) */
|
|
15
|
+
export declare function detectSlop(text: string): Array<{
|
|
16
|
+
message: string;
|
|
17
|
+
severity: 'warn' | 'info';
|
|
18
|
+
}>;
|