@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,111 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import * as crypto from 'node:crypto';
|
|
4
|
+
import { SESSIONS_DIR } from './paths.js';
|
|
5
|
+
import { createLogger } from './logger.js';
|
|
6
|
+
const logger = createLogger('session-logger');
|
|
7
|
+
/** 현재 세션 로그 파일 경로 (종료 시 업데이트에 사용) */
|
|
8
|
+
let currentSessionPath = null;
|
|
9
|
+
/** 세션 시작 시각 (duration 계산에 사용) */
|
|
10
|
+
let sessionStartMs = null;
|
|
11
|
+
/** exit/signal 리스너 등록 여부 (중복 등록 방지) */
|
|
12
|
+
let isBound = false;
|
|
13
|
+
/** UUID v4 생성 (node:crypto 활용) */
|
|
14
|
+
function generateUUID() {
|
|
15
|
+
return crypto.randomUUID();
|
|
16
|
+
}
|
|
17
|
+
/** YYYY-MM-DD 형식의 날짜 문자열 반환 */
|
|
18
|
+
function todayStr() {
|
|
19
|
+
return new Date().toISOString().slice(0, 10);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* 세션 로그를 ~/.forgen/sessions/{date}_{sessionId}.json 에 기록
|
|
23
|
+
* harness.ts의 prepareHarness() 완료 후 호출
|
|
24
|
+
*/
|
|
25
|
+
export function startSessionLog(context) {
|
|
26
|
+
try {
|
|
27
|
+
// sessions 디렉토리가 없으면 생성
|
|
28
|
+
fs.mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
29
|
+
const sessionId = generateUUID();
|
|
30
|
+
const startTime = new Date().toISOString();
|
|
31
|
+
sessionStartMs = Date.now();
|
|
32
|
+
// 모드 판별: 환경변수 또는 argv 기반
|
|
33
|
+
const mode = process.env.FORGEN_MODE ?? process.env.COMPOUND_MODE ?? 'default';
|
|
34
|
+
const log = {
|
|
35
|
+
sessionId,
|
|
36
|
+
startTime,
|
|
37
|
+
cwd: context.cwd,
|
|
38
|
+
philosophy: context.philosophy.name,
|
|
39
|
+
scope: context.scope.summary,
|
|
40
|
+
mode,
|
|
41
|
+
};
|
|
42
|
+
const filename = `${todayStr()}_${sessionId}.json`;
|
|
43
|
+
currentSessionPath = path.join(SESSIONS_DIR, filename);
|
|
44
|
+
fs.writeFileSync(currentSessionPath, JSON.stringify(log, null, 2));
|
|
45
|
+
// 오래된 세션 로그 정리 (90일+)
|
|
46
|
+
cleanOldSessions();
|
|
47
|
+
// 프로세스 종료 시 자동으로 endTime/duration 업데이트 (한 번만 등록)
|
|
48
|
+
if (!isBound) {
|
|
49
|
+
isBound = true;
|
|
50
|
+
process.on('exit', finalizeSessionLog);
|
|
51
|
+
process.on('SIGINT', () => {
|
|
52
|
+
finalizeSessionLog();
|
|
53
|
+
process.exit(0);
|
|
54
|
+
});
|
|
55
|
+
process.on('SIGTERM', () => {
|
|
56
|
+
finalizeSessionLog();
|
|
57
|
+
process.exit(0);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch (e) {
|
|
62
|
+
logger.debug('세션 로그 시작 실패', e);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const RETENTION_DAYS = 90;
|
|
66
|
+
/** 90일 이상 된 세션 로그 파일 삭제 */
|
|
67
|
+
function cleanOldSessions() {
|
|
68
|
+
try {
|
|
69
|
+
if (!fs.existsSync(SESSIONS_DIR))
|
|
70
|
+
return;
|
|
71
|
+
const cutoff = Date.now() - RETENTION_DAYS * 24 * 60 * 60 * 1000;
|
|
72
|
+
const files = fs.readdirSync(SESSIONS_DIR).filter(f => f.endsWith('.json'));
|
|
73
|
+
let deleted = 0;
|
|
74
|
+
for (const file of files) {
|
|
75
|
+
// 파일명에서 날짜 추출: YYYY-MM-DD_UUID.json
|
|
76
|
+
const dateStr = file.slice(0, 10);
|
|
77
|
+
const fileDate = new Date(dateStr).getTime();
|
|
78
|
+
if (!Number.isNaN(fileDate) && fileDate < cutoff) {
|
|
79
|
+
fs.unlinkSync(path.join(SESSIONS_DIR, file));
|
|
80
|
+
deleted++;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (deleted > 0) {
|
|
84
|
+
logger.debug(`${deleted}개 오래된 세션 로그 정리 (${RETENTION_DAYS}일+)`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch (e) {
|
|
88
|
+
logger.debug('세션 로그 정리 실패', e);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** 프로세스 종료 시 세션 로그에 endTime과 duration 추가 */
|
|
92
|
+
function finalizeSessionLog() {
|
|
93
|
+
if (!currentSessionPath || !sessionStartMs)
|
|
94
|
+
return;
|
|
95
|
+
if (!fs.existsSync(currentSessionPath))
|
|
96
|
+
return;
|
|
97
|
+
try {
|
|
98
|
+
const raw = fs.readFileSync(currentSessionPath, 'utf-8');
|
|
99
|
+
const log = JSON.parse(raw);
|
|
100
|
+
const now = Date.now();
|
|
101
|
+
log.endTime = new Date(now).toISOString();
|
|
102
|
+
log.durationMs = now - sessionStartMs;
|
|
103
|
+
fs.writeFileSync(currentSessionPath, JSON.stringify(log, null, 2));
|
|
104
|
+
}
|
|
105
|
+
catch (e) {
|
|
106
|
+
logger.debug('세션 로그 종료 실패', e);
|
|
107
|
+
}
|
|
108
|
+
// 중복 호출 방지
|
|
109
|
+
currentSessionPath = null;
|
|
110
|
+
sessionStartMs = null;
|
|
111
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen — Session Store (Node.js built-in SQLite)
|
|
3
|
+
*
|
|
4
|
+
* 세션 대화를 SQLite에 저장하여 과거 세션을 전문 검색할 수 있게 함.
|
|
5
|
+
* MCP session-search 도구가 이 데이터를 조회.
|
|
6
|
+
* 외부 의존성 없음 — Node.js 22+ 내장 node:sqlite 사용.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Transcript JSONL을 SQLite에 인덱싱.
|
|
10
|
+
*/
|
|
11
|
+
export declare function indexSession(cwd: string, transcriptPath: string, sessionId: string): Promise<void>;
|
|
12
|
+
/**
|
|
13
|
+
* 과거 세션 검색.
|
|
14
|
+
* FTS5 MATCH 우선 사용 (전문 검색, 순위 정렬).
|
|
15
|
+
* FTS5 미지원 시 LIKE 기반 폴백.
|
|
16
|
+
*/
|
|
17
|
+
export declare function searchSessions(query: string, limit?: number): Array<{
|
|
18
|
+
sessionId: string;
|
|
19
|
+
role: string;
|
|
20
|
+
content: string;
|
|
21
|
+
timestamp: string;
|
|
22
|
+
cwd: string;
|
|
23
|
+
tokens: string[];
|
|
24
|
+
}>;
|
|
25
|
+
/**
|
|
26
|
+
* 매칭 토큰 위치를 기준으로 컨텍스트 윈도우를 추출.
|
|
27
|
+
*/
|
|
28
|
+
export declare function extractContextWindow(content: string, tokens: string[], windowSize?: number): string;
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen — Session Store (Node.js built-in SQLite)
|
|
3
|
+
*
|
|
4
|
+
* 세션 대화를 SQLite에 저장하여 과거 세션을 전문 검색할 수 있게 함.
|
|
5
|
+
* MCP session-search 도구가 이 데이터를 조회.
|
|
6
|
+
* 외부 의존성 없음 — Node.js 22+ 내장 node:sqlite 사용.
|
|
7
|
+
*/
|
|
8
|
+
import { createRequire } from 'node:module';
|
|
9
|
+
import * as fs from 'node:fs';
|
|
10
|
+
import * as path from 'node:path';
|
|
11
|
+
import { createLogger } from './logger.js';
|
|
12
|
+
import { FORGEN_HOME } from './paths.js';
|
|
13
|
+
const require = createRequire(import.meta.url);
|
|
14
|
+
// Suppress ExperimentalWarning for node:sqlite (Node.js 22+)
|
|
15
|
+
{
|
|
16
|
+
const origWarningListeners = process.listeners('warning');
|
|
17
|
+
process.removeAllListeners('warning');
|
|
18
|
+
process.on('warning', (warning) => {
|
|
19
|
+
if (warning.name === 'ExperimentalWarning')
|
|
20
|
+
return;
|
|
21
|
+
for (const listener of origWarningListeners) {
|
|
22
|
+
listener(warning);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
const log = createLogger('session-store');
|
|
27
|
+
const DB_PATH = path.join(FORGEN_HOME, 'sessions.db');
|
|
28
|
+
let fts5Available = false;
|
|
29
|
+
function openDb() {
|
|
30
|
+
try {
|
|
31
|
+
// Node.js 22+ experimental node:sqlite
|
|
32
|
+
const { DatabaseSync } = require('node:sqlite');
|
|
33
|
+
const db = new DatabaseSync(DB_PATH);
|
|
34
|
+
db.exec(`
|
|
35
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
36
|
+
id TEXT PRIMARY KEY,
|
|
37
|
+
cwd TEXT NOT NULL,
|
|
38
|
+
started_at TEXT NOT NULL,
|
|
39
|
+
message_count INTEGER DEFAULT 0
|
|
40
|
+
);
|
|
41
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
42
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
43
|
+
session_id TEXT NOT NULL,
|
|
44
|
+
role TEXT NOT NULL,
|
|
45
|
+
content TEXT NOT NULL,
|
|
46
|
+
timestamp TEXT NOT NULL
|
|
47
|
+
);
|
|
48
|
+
`);
|
|
49
|
+
// FTS5 가상 테이블 생성 (미지원 시 LIKE 폴백)
|
|
50
|
+
try {
|
|
51
|
+
db.exec(`
|
|
52
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
|
|
53
|
+
content,
|
|
54
|
+
content=messages,
|
|
55
|
+
content_rowid=id,
|
|
56
|
+
tokenize='unicode61 remove_diacritics 2'
|
|
57
|
+
);
|
|
58
|
+
`);
|
|
59
|
+
fts5Available = true;
|
|
60
|
+
}
|
|
61
|
+
catch (e) {
|
|
62
|
+
log.debug('FTS5 미지원 — LIKE 폴백 사용', e);
|
|
63
|
+
fts5Available = false;
|
|
64
|
+
}
|
|
65
|
+
return db;
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
log.debug('SQLite 초기화 실패 (Node.js 22+ 필요)', e);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Transcript JSONL을 SQLite에 인덱싱.
|
|
74
|
+
*/
|
|
75
|
+
export async function indexSession(cwd, transcriptPath, sessionId) {
|
|
76
|
+
const db = openDb();
|
|
77
|
+
if (!db)
|
|
78
|
+
return;
|
|
79
|
+
try {
|
|
80
|
+
const existing = db.prepare('SELECT id FROM sessions WHERE id = ?').get(sessionId);
|
|
81
|
+
if (existing)
|
|
82
|
+
return;
|
|
83
|
+
const content = fs.readFileSync(transcriptPath, 'utf-8');
|
|
84
|
+
const lines = content.split('\n').filter(Boolean);
|
|
85
|
+
db.prepare('INSERT INTO sessions (id, cwd, started_at, message_count) VALUES (?, ?, ?, 0)').run(sessionId, cwd, new Date().toISOString());
|
|
86
|
+
let messageCount = 0;
|
|
87
|
+
const insertMsg = db.prepare('INSERT INTO messages (session_id, role, content, timestamp) VALUES (?, ?, ?, ?)');
|
|
88
|
+
for (const line of lines) {
|
|
89
|
+
try {
|
|
90
|
+
const entry = JSON.parse(line);
|
|
91
|
+
let role = '';
|
|
92
|
+
let text = '';
|
|
93
|
+
if ((entry.type === 'user' || entry.type === 'queue-operation') && typeof entry.content === 'string') {
|
|
94
|
+
role = 'user';
|
|
95
|
+
text = entry.content;
|
|
96
|
+
}
|
|
97
|
+
else if (entry.type === 'assistant') {
|
|
98
|
+
role = 'assistant';
|
|
99
|
+
text = typeof entry.content === 'string'
|
|
100
|
+
? entry.content
|
|
101
|
+
: Array.isArray(entry.content)
|
|
102
|
+
? entry.content.filter((c) => c.type === 'text').map((c) => c.text).join('\n')
|
|
103
|
+
: '';
|
|
104
|
+
}
|
|
105
|
+
if (role && text) {
|
|
106
|
+
const truncated = text.slice(0, 10000);
|
|
107
|
+
const result = insertMsg.run(sessionId, role, truncated, entry.timestamp ?? '');
|
|
108
|
+
// FTS5 인덱스 동기화
|
|
109
|
+
if (fts5Available) {
|
|
110
|
+
try {
|
|
111
|
+
db.prepare('INSERT INTO messages_fts(rowid, content) VALUES (?, ?)').run(result.lastInsertRowid, truncated);
|
|
112
|
+
}
|
|
113
|
+
catch { /* FTS sync failure — search may miss this message */ }
|
|
114
|
+
}
|
|
115
|
+
messageCount++;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch { /* skip malformed lines */ }
|
|
119
|
+
}
|
|
120
|
+
db.prepare('UPDATE sessions SET message_count = ? WHERE id = ?').run(messageCount, sessionId);
|
|
121
|
+
log.debug(`세션 인덱싱 완료: ${sessionId} (${messageCount} messages)`);
|
|
122
|
+
}
|
|
123
|
+
catch (e) {
|
|
124
|
+
log.debug('세션 인덱싱 실패', e);
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
try {
|
|
128
|
+
db.close();
|
|
129
|
+
}
|
|
130
|
+
catch { /* ignore */ }
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* 과거 세션 검색.
|
|
135
|
+
* FTS5 MATCH 우선 사용 (전문 검색, 순위 정렬).
|
|
136
|
+
* FTS5 미지원 시 LIKE 기반 폴백.
|
|
137
|
+
*/
|
|
138
|
+
export function searchSessions(query, limit = 10) {
|
|
139
|
+
const db = openDb();
|
|
140
|
+
if (!db)
|
|
141
|
+
return [];
|
|
142
|
+
const tokens = query
|
|
143
|
+
.split(/\s+/)
|
|
144
|
+
.map(t => t.toLowerCase())
|
|
145
|
+
.filter(t => t.length >= 2);
|
|
146
|
+
if (tokens.length === 0)
|
|
147
|
+
return [];
|
|
148
|
+
try {
|
|
149
|
+
let results;
|
|
150
|
+
if (fts5Available) {
|
|
151
|
+
// FTS5 MATCH — 전문 검색 + BM25 순위 정렬
|
|
152
|
+
const ftsQuery = tokens.map(t => `"${t.replace(/"/g, '""')}"`).join(' AND ');
|
|
153
|
+
results = db.prepare(`
|
|
154
|
+
SELECT m.session_id, m.role, m.content, m.timestamp, s.cwd
|
|
155
|
+
FROM messages_fts fts
|
|
156
|
+
JOIN messages m ON fts.rowid = m.id
|
|
157
|
+
JOIN sessions s ON m.session_id = s.id
|
|
158
|
+
WHERE messages_fts MATCH ?
|
|
159
|
+
ORDER BY fts.rank
|
|
160
|
+
LIMIT ?
|
|
161
|
+
`).all(ftsQuery, limit);
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
// LIKE 폴백
|
|
165
|
+
const conditions = tokens.map(() => "LOWER(m.content) LIKE ? ESCAPE '\\'").join(' AND ');
|
|
166
|
+
const escapedTokens = tokens.map(t => t.replace(/%/g, '\\%').replace(/_/g, '\\_'));
|
|
167
|
+
const params = escapedTokens.map(t => `%${t}%`);
|
|
168
|
+
params.push(limit);
|
|
169
|
+
results = db.prepare(`
|
|
170
|
+
SELECT m.session_id, m.role, m.content, m.timestamp, s.cwd
|
|
171
|
+
FROM messages m
|
|
172
|
+
JOIN sessions s ON m.session_id = s.id
|
|
173
|
+
WHERE ${conditions}
|
|
174
|
+
ORDER BY m.id DESC
|
|
175
|
+
LIMIT ?
|
|
176
|
+
`).all(...params);
|
|
177
|
+
}
|
|
178
|
+
return results.map((r) => ({
|
|
179
|
+
sessionId: r.session_id,
|
|
180
|
+
role: r.role,
|
|
181
|
+
content: r.content,
|
|
182
|
+
timestamp: r.timestamp,
|
|
183
|
+
cwd: r.cwd,
|
|
184
|
+
tokens,
|
|
185
|
+
}));
|
|
186
|
+
}
|
|
187
|
+
catch (e) {
|
|
188
|
+
log.debug('세션 검색 실패', e);
|
|
189
|
+
return [];
|
|
190
|
+
}
|
|
191
|
+
finally {
|
|
192
|
+
try {
|
|
193
|
+
db.close();
|
|
194
|
+
}
|
|
195
|
+
catch { /* ignore */ }
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* 매칭 토큰 위치를 기준으로 컨텍스트 윈도우를 추출.
|
|
200
|
+
*/
|
|
201
|
+
export function extractContextWindow(content, tokens, windowSize = 120) {
|
|
202
|
+
const lower = content.toLowerCase();
|
|
203
|
+
const positions = [];
|
|
204
|
+
for (const token of tokens) {
|
|
205
|
+
const idx = lower.indexOf(token);
|
|
206
|
+
if (idx !== -1)
|
|
207
|
+
positions.push(idx);
|
|
208
|
+
}
|
|
209
|
+
if (positions.length === 0)
|
|
210
|
+
return content.slice(0, 200);
|
|
211
|
+
positions.sort((a, b) => a - b);
|
|
212
|
+
const center = positions[0];
|
|
213
|
+
const start = Math.max(0, center - windowSize);
|
|
214
|
+
const end = Math.min(content.length, center + windowSize);
|
|
215
|
+
const prefix = start > 0 ? '...' : '';
|
|
216
|
+
const suffix = end < content.length ? '...' : '';
|
|
217
|
+
return `${prefix}${content.slice(start, end)}${suffix}`;
|
|
218
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { CLAUDE_DIR, SETTINGS_PATH } from './paths.js';
|
|
2
|
+
export { CLAUDE_DIR, SETTINGS_PATH };
|
|
3
|
+
export declare const SETTINGS_BACKUP_PATH: string;
|
|
4
|
+
/** lockfile 획득 (최대 3초 대기, 100ms 간격 재시도) */
|
|
5
|
+
export declare function acquireLock(): void;
|
|
6
|
+
/** lockfile 해제 */
|
|
7
|
+
export declare function releaseLock(): void;
|
|
8
|
+
/** 임시파일에 쓴 후 rename으로 원자적 교체 */
|
|
9
|
+
export declare function atomicWriteFileSync(targetPath: string, data: string): void;
|
|
10
|
+
/**
|
|
11
|
+
* settings.json 안전 읽기.
|
|
12
|
+
* 파일이 없으면 빈 객체 반환. 파싱 실패 시 Error throw (빈 설정 덮어쓰기 방지).
|
|
13
|
+
*/
|
|
14
|
+
export declare function readSettings(): Record<string, unknown>;
|
|
15
|
+
/** settings.json 안전 쓰기. backup 생성 + lock + atomic write */
|
|
16
|
+
export declare function writeSettings(settings: Record<string, unknown>): void;
|
|
17
|
+
/** settings.json.forgen-backup 파일에서 원본 복원 */
|
|
18
|
+
export declare function rollbackSettings(): boolean;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* settings-lock — settings.json 동시접근 보호 유틸리티
|
|
3
|
+
*
|
|
4
|
+
* acquireLock/releaseLock + atomicWriteFileSync 패턴을
|
|
5
|
+
* settings.json을 조작하는 모든 모듈에서 재사용합니다.
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from 'node:fs';
|
|
8
|
+
import * as path from 'node:path';
|
|
9
|
+
import { createLogger } from './logger.js';
|
|
10
|
+
import { CLAUDE_DIR, SETTINGS_PATH } from './paths.js';
|
|
11
|
+
const log = createLogger('settings-lock');
|
|
12
|
+
export { CLAUDE_DIR, SETTINGS_PATH };
|
|
13
|
+
export const SETTINGS_BACKUP_PATH = path.join(CLAUDE_DIR, 'settings.json.forgen-backup');
|
|
14
|
+
const SETTINGS_LOCK_PATH = path.join(CLAUDE_DIR, 'settings.json.lock');
|
|
15
|
+
/** lockfile 내용에서 pid 추출 */
|
|
16
|
+
function readLockPid() {
|
|
17
|
+
try {
|
|
18
|
+
const content = fs.readFileSync(SETTINGS_LOCK_PATH, 'utf-8').trim();
|
|
19
|
+
const pid = parseInt(content, 10);
|
|
20
|
+
return Number.isNaN(pid) ? null : pid;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** 프로세스가 살아있는지 확인 (signal 0 전송) */
|
|
27
|
+
function isProcessAlive(pid) {
|
|
28
|
+
try {
|
|
29
|
+
process.kill(pid, 0);
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** lockfile 획득 (최대 3초 대기, 100ms 간격 재시도) */
|
|
37
|
+
export function acquireLock() {
|
|
38
|
+
const maxWaitMs = 3000;
|
|
39
|
+
const intervalMs = 100;
|
|
40
|
+
const start = Date.now();
|
|
41
|
+
while (Date.now() - start < maxWaitMs) {
|
|
42
|
+
try {
|
|
43
|
+
fs.writeFileSync(SETTINGS_LOCK_PATH, String(process.pid), { flag: 'wx' });
|
|
44
|
+
return; // 성공
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// lock 파일이 이미 존재 — 대기 후 재시도
|
|
48
|
+
const elapsed = Date.now() - start;
|
|
49
|
+
if (elapsed + intervalMs >= maxWaitMs)
|
|
50
|
+
break;
|
|
51
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, intervalMs);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// 타임아웃: lock을 잡고 있는 프로세스가 살아있는지 확인
|
|
55
|
+
const lockPid = readLockPid();
|
|
56
|
+
if (lockPid !== null && isProcessAlive(lockPid)) {
|
|
57
|
+
log.debug(`lockfile 타임아웃 — pid ${lockPid} 프로세스가 아직 활성 상태, 대기 중 강제 획득 보류`);
|
|
58
|
+
// 프로세스가 살아있으면 그래도 강제 획득 (데드락 방지)
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
log.debug(`lockfile 타임아웃 — stale lock 감지 (pid: ${lockPid ?? 'unknown'}, 프로세스 종료됨)`);
|
|
62
|
+
}
|
|
63
|
+
fs.writeFileSync(SETTINGS_LOCK_PATH, String(process.pid));
|
|
64
|
+
}
|
|
65
|
+
/** lockfile 해제 */
|
|
66
|
+
export function releaseLock() {
|
|
67
|
+
try {
|
|
68
|
+
fs.rmSync(SETTINGS_LOCK_PATH, { force: true });
|
|
69
|
+
}
|
|
70
|
+
catch { /* 이미 없으면 무시 */ }
|
|
71
|
+
}
|
|
72
|
+
/** 임시파일에 쓴 후 rename으로 원자적 교체 */
|
|
73
|
+
export function atomicWriteFileSync(targetPath, data) {
|
|
74
|
+
const tmpPath = `${targetPath}.tmp.${process.pid}`;
|
|
75
|
+
fs.writeFileSync(tmpPath, data);
|
|
76
|
+
fs.renameSync(tmpPath, targetPath);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* settings.json 안전 읽기.
|
|
80
|
+
* 파일이 없으면 빈 객체 반환. 파싱 실패 시 Error throw (빈 설정 덮어쓰기 방지).
|
|
81
|
+
*/
|
|
82
|
+
export function readSettings() {
|
|
83
|
+
fs.mkdirSync(CLAUDE_DIR, { recursive: true });
|
|
84
|
+
if (!fs.existsSync(SETTINGS_PATH))
|
|
85
|
+
return {};
|
|
86
|
+
const raw = fs.readFileSync(SETTINGS_PATH, 'utf-8');
|
|
87
|
+
return JSON.parse(raw); // 파싱 실패 시 throw → 호출자가 처리
|
|
88
|
+
}
|
|
89
|
+
/** settings.json 안전 쓰기. backup 생성 + lock + atomic write */
|
|
90
|
+
export function writeSettings(settings) {
|
|
91
|
+
fs.mkdirSync(CLAUDE_DIR, { recursive: true });
|
|
92
|
+
acquireLock();
|
|
93
|
+
try {
|
|
94
|
+
if (fs.existsSync(SETTINGS_PATH)) {
|
|
95
|
+
fs.copyFileSync(SETTINGS_PATH, SETTINGS_BACKUP_PATH);
|
|
96
|
+
}
|
|
97
|
+
atomicWriteFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
releaseLock();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/** settings.json.forgen-backup 파일에서 원본 복원 */
|
|
104
|
+
export function rollbackSettings() {
|
|
105
|
+
if (!fs.existsSync(SETTINGS_BACKUP_PATH))
|
|
106
|
+
return false;
|
|
107
|
+
acquireLock();
|
|
108
|
+
try {
|
|
109
|
+
// 현재 설정을 rollback 전 백업 (.pre-rollback) — 데이터 손실 방지
|
|
110
|
+
if (fs.existsSync(SETTINGS_PATH)) {
|
|
111
|
+
const preRollbackPath = `${SETTINGS_PATH}.pre-rollback`;
|
|
112
|
+
fs.copyFileSync(SETTINGS_PATH, preRollbackPath);
|
|
113
|
+
}
|
|
114
|
+
const backup = fs.readFileSync(SETTINGS_BACKUP_PATH, 'utf-8');
|
|
115
|
+
atomicWriteFileSync(SETTINGS_PATH, backup);
|
|
116
|
+
fs.rmSync(SETTINGS_BACKUP_PATH);
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
releaseLock();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { spawn, execFileSync } from 'node:child_process';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import * as os from 'node:os';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { buildEnv } from './config-injector.js';
|
|
7
|
+
import { loadGlobalConfig } from './global-config.js';
|
|
8
|
+
import { createLogger } from './logger.js';
|
|
9
|
+
const log = createLogger('spawn');
|
|
10
|
+
/** claude CLI 경로 탐색 */
|
|
11
|
+
function findClaude() {
|
|
12
|
+
return 'claude';
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* 가장 최근 transcript 파일을 찾는다.
|
|
16
|
+
* Claude Code는 세션 대화를 ~/.claude/projects/{sanitized-cwd}/{uuid}.jsonl에 저장.
|
|
17
|
+
*/
|
|
18
|
+
function findLatestTranscript(cwd) {
|
|
19
|
+
// Claude Code는 cwd의 /를 -로 치환하고 선행 -를 유지
|
|
20
|
+
const sanitized = cwd.replace(/\//g, '-');
|
|
21
|
+
const projectDir = path.join(os.homedir(), '.claude', 'projects', sanitized);
|
|
22
|
+
if (!fs.existsSync(projectDir))
|
|
23
|
+
return null;
|
|
24
|
+
const jsonlFiles = fs.readdirSync(projectDir)
|
|
25
|
+
.filter(f => f.endsWith('.jsonl'))
|
|
26
|
+
.map(f => ({ name: f, mtime: fs.statSync(path.join(projectDir, f)).mtimeMs }))
|
|
27
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
28
|
+
return jsonlFiles.length > 0 ? path.join(projectDir, jsonlFiles[0].name) : null;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 세션 종료 후 자동 compound 추출 + USER.md 업데이트.
|
|
32
|
+
* auto-compound-runner.ts를 동기 실행하여 솔루션 추출 + 사용자 패턴 관찰.
|
|
33
|
+
*/
|
|
34
|
+
async function runAutoCompound(cwd, transcriptPath, sessionId) {
|
|
35
|
+
console.log('\n[forgen] 세션 분석 중... (자동 compound)');
|
|
36
|
+
const runnerPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'auto-compound-runner.js');
|
|
37
|
+
try {
|
|
38
|
+
execFileSync('node', [runnerPath, cwd, transcriptPath, sessionId], {
|
|
39
|
+
cwd,
|
|
40
|
+
timeout: 120_000,
|
|
41
|
+
stdio: ['pipe', 'inherit', 'inherit'],
|
|
42
|
+
});
|
|
43
|
+
console.log('[forgen] 자동 compound 완료\n');
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
log.debug('auto-compound 실패', e);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Transcript를 SQLite FTS5에 인덱싱 (추후 session-search MCP 도구용).
|
|
51
|
+
*/
|
|
52
|
+
async function indexTranscriptToFTS(cwd, transcriptPath, sessionId) {
|
|
53
|
+
try {
|
|
54
|
+
const { indexSession } = await import('./session-store.js');
|
|
55
|
+
await indexSession(cwd, transcriptPath, sessionId);
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
log.debug('FTS5 인덱싱 실패 (session-store 미구현 시 정상)', e);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** Claude Code를 하네스 환경으로 실행 */
|
|
62
|
+
export async function spawnClaude(args, context) {
|
|
63
|
+
const claudePath = findClaude();
|
|
64
|
+
const env = buildEnv(context.cwd);
|
|
65
|
+
const cleanArgs = [...args];
|
|
66
|
+
// config.json에서 dangerouslySkipPermissions 기본값 적용
|
|
67
|
+
const globalConfig = loadGlobalConfig();
|
|
68
|
+
if (globalConfig.dangerouslySkipPermissions && !cleanArgs.includes('--dangerously-skip-permissions')) {
|
|
69
|
+
cleanArgs.unshift('--dangerously-skip-permissions');
|
|
70
|
+
}
|
|
71
|
+
// 세션 시작 전 timestamp 기록 (종료 후 transcript 찾기 위해)
|
|
72
|
+
const sessionStartTime = Date.now();
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
const child = spawn(claudePath, cleanArgs, {
|
|
75
|
+
stdio: 'inherit',
|
|
76
|
+
env: { ...process.env, ...env },
|
|
77
|
+
cwd: context.cwd,
|
|
78
|
+
});
|
|
79
|
+
child.on('error', (err) => {
|
|
80
|
+
if (err.code === 'ENOENT') {
|
|
81
|
+
reject(new Error('Claude Code is not installed. npm install -g @anthropic-ai/claude-code'));
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
reject(err);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
child.on('exit', async (code) => {
|
|
88
|
+
// 세션 종료 후 하네스 작업
|
|
89
|
+
try {
|
|
90
|
+
const transcript = findLatestTranscript(context.cwd);
|
|
91
|
+
if (!transcript) {
|
|
92
|
+
log.debug('transcript 파일을 찾을 수 없음');
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
const stat = fs.statSync(transcript);
|
|
96
|
+
// 이 세션에서 생성/수정된 transcript만
|
|
97
|
+
if (stat.mtimeMs <= sessionStartTime) {
|
|
98
|
+
log.debug(`transcript mtime(${stat.mtimeMs}) <= sessionStart(${sessionStartTime}), 건너뜀`);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
const sessionId = path.basename(transcript, '.jsonl');
|
|
102
|
+
// 1. FTS5 인덱싱
|
|
103
|
+
await indexTranscriptToFTS(context.cwd, transcript, sessionId);
|
|
104
|
+
// 2. 자동 compound (10+ user 메시지인 경우만)
|
|
105
|
+
const content = fs.readFileSync(transcript, 'utf-8');
|
|
106
|
+
const userMsgCount = content.split('\n')
|
|
107
|
+
.filter(l => { try {
|
|
108
|
+
const t = JSON.parse(l).type;
|
|
109
|
+
return t === 'user' || t === 'queue-operation';
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return false;
|
|
113
|
+
} })
|
|
114
|
+
.length;
|
|
115
|
+
if (userMsgCount >= 10) {
|
|
116
|
+
await runAutoCompound(context.cwd, transcript, sessionId);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
console.log(`[forgen] 세션이 짧아 auto-compound 생략 (${userMsgCount} messages)`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
console.error('[forgen] 세션 종료 후 처리 실패:', e instanceof Error ? e.message : e);
|
|
126
|
+
}
|
|
127
|
+
if (code === 0 || code === null) {
|
|
128
|
+
resolve();
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
process.exit(code);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
}
|