@triflux/core 10.0.0-alpha.1 → 10.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/hooks/hook-adaptive-collector.mjs +86 -0
  2. package/hooks/hook-manager.mjs +15 -2
  3. package/hooks/hook-registry.json +37 -4
  4. package/hooks/keyword-rules.json +2 -1
  5. package/hooks/mcp-config-watcher.mjs +2 -7
  6. package/hooks/safety-guard.mjs +37 -0
  7. package/hub/account-broker.mjs +251 -0
  8. package/hub/adaptive-diagnostic.mjs +323 -0
  9. package/hub/adaptive-inject.mjs +186 -0
  10. package/hub/adaptive-memory.mjs +163 -0
  11. package/hub/adaptive.mjs +143 -0
  12. package/hub/cli-adapter-base.mjs +89 -1
  13. package/hub/codex-adapter.mjs +12 -3
  14. package/hub/codex-compat.mjs +11 -78
  15. package/hub/codex-preflight.mjs +20 -1
  16. package/hub/gemini-adapter.mjs +1 -0
  17. package/hub/index.mjs +34 -0
  18. package/hub/lib/cache-guard.mjs +114 -0
  19. package/hub/lib/known-errors.json +72 -0
  20. package/hub/lib/memory-store.mjs +748 -0
  21. package/hub/lib/ssh-command.mjs +150 -0
  22. package/hub/lib/uuidv7.mjs +44 -0
  23. package/hub/memory-doctor.mjs +480 -0
  24. package/hub/middleware/request-logger.mjs +80 -0
  25. package/hub/router.mjs +1 -1
  26. package/hub/team-bridge.mjs +21 -19
  27. package/hud/constants.mjs +7 -0
  28. package/hud/context-monitor.mjs +403 -0
  29. package/hud/hud-qos-status.mjs +8 -4
  30. package/hud/providers/claude.mjs +5 -0
  31. package/hud/renderers.mjs +32 -14
  32. package/hud/utils.mjs +26 -0
  33. package/package.json +3 -2
  34. package/scripts/lib/claudemd-scanner.mjs +218 -0
  35. package/scripts/lib/handoff.mjs +171 -0
  36. package/scripts/lib/mcp-guard-engine.mjs +20 -6
  37. package/scripts/lib/skill-template.mjs +269 -0
  38. package/scripts/lib/claudemd-manager.mjs +0 -325
@@ -0,0 +1,143 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { join } from 'node:path';
3
+
4
+ import { createAdaptiveMemory } from './adaptive-memory.mjs';
5
+ import { createDiagnosticPipeline } from './adaptive-diagnostic.mjs';
6
+ import { createAdaptiveInjector } from './adaptive-inject.mjs';
7
+ import { createAdaptiveFingerprintService } from './session-fingerprint.mjs';
8
+
9
+ let singletonEngine = null;
10
+
11
+ function clone(value) {
12
+ return value == null ? value : JSON.parse(JSON.stringify(value));
13
+ }
14
+
15
+ function toRuleList(value) {
16
+ return Array.isArray(value) ? value : [];
17
+ }
18
+
19
+ function resolveTierCount(memory, tier) {
20
+ if (typeof memory.getTier === 'function') {
21
+ const rules = memory.getTier(tier);
22
+ return Array.isArray(rules) ? rules.length : 0;
23
+ }
24
+ return toRuleList(memory.getAllRules?.()).filter((rule) => Number(rule?.tier) === tier).length;
25
+ }
26
+
27
+ function resolveActiveRuleIds(memory, decayResult) {
28
+ if (Array.isArray(decayResult?.activeRuleIds)) return [...decayResult.activeRuleIds];
29
+ return toRuleList(memory.getAllRules?.()).map((rule) => rule.id).filter(Boolean);
30
+ }
31
+
32
+ function resolveRecordedRule(recordResult) {
33
+ if (recordResult?.rule) return { rule: recordResult.rule, promoted: Boolean(recordResult.promoted) };
34
+ return { rule: recordResult || null, promoted: false };
35
+ }
36
+
37
+ function createEngineInstance(opts = {}) {
38
+ const repoRoot = opts.repoRoot || process.cwd();
39
+ const claudeMdPath = opts.claudeMdPath || join(repoRoot, 'CLAUDE.md');
40
+ const projectSlug = String(opts.projectSlug || 'default').trim() || 'default';
41
+ const now = opts.now || (() => Date.now());
42
+ const sessionIdFactory = opts.sessionIdFactory || (() => randomUUID());
43
+
44
+ const memory = opts.memory || opts.memoryFactory?.(opts) || createAdaptiveMemory({ projectSlug, ...opts.memoryOptions });
45
+ const diagnostic = opts.diagnostic || opts.diagnosticFactory?.(opts) || createDiagnosticPipeline(opts.diagnosticOptions);
46
+ const injector = opts.injector || opts.injectorFactory?.(opts) || createAdaptiveInjector({ claudeMdPath, ...opts.injectorOptions });
47
+ const fingerprintService = opts.fingerprintService
48
+ || opts.fingerprintFactory?.(opts)
49
+ || createAdaptiveFingerprintService(opts.fingerprintOptions);
50
+
51
+ let started = false;
52
+ let currentSessionId = null;
53
+ let lastFingerprintId = null;
54
+ const counters = {
55
+ totalErrors: 0,
56
+ diagnosedErrors: 0,
57
+ };
58
+
59
+ function refreshFingerprint() {
60
+ if (typeof fingerprintService.computeFingerprint !== 'function') return;
61
+ const fingerprint = fingerprintService.computeFingerprint({
62
+ scope: projectSlug,
63
+ cwd: repoRoot,
64
+ filePath: claudeMdPath,
65
+ workType: 'adaptive',
66
+ timestamp: now(),
67
+ });
68
+ lastFingerprintId = fingerprint?.fingerprint_id || lastFingerprintId;
69
+ }
70
+
71
+ function startSession() {
72
+ if (started) return;
73
+ currentSessionId = sessionIdFactory();
74
+ const decayResult = typeof memory.decay === 'function' ? memory.decay(currentSessionId) : null;
75
+ injector.cleanup?.(resolveActiveRuleIds(memory, decayResult));
76
+ refreshFingerprint();
77
+ started = true;
78
+ }
79
+
80
+ function handleError(errorContext = {}) {
81
+ if (!started) startSession();
82
+ counters.totalErrors += 1;
83
+
84
+ const diagnosis = diagnostic.diagnose({
85
+ ...errorContext,
86
+ projectSlug,
87
+ sessionId: currentSessionId,
88
+ });
89
+ if (!diagnosis?.matched || !diagnosis.rule) {
90
+ return Object.freeze({ diagnosed: false, rule: null, promoted: false });
91
+ }
92
+
93
+ counters.diagnosedErrors += 1;
94
+ const { rule: recordedRule, promoted } = resolveRecordedRule(memory.record({
95
+ ...diagnosis.rule,
96
+ confidence: diagnosis.confidence,
97
+ dnaFactor: diagnosis.dnaFactor,
98
+ sessionId: currentSessionId,
99
+ lastSeen: new Date(now()).toISOString().slice(0, 10),
100
+ }));
101
+
102
+ const nextRule = clone(recordedRule || diagnosis.rule);
103
+ if (nextRule?.tier >= 3 && promoted) {
104
+ injector.inject?.(nextRule);
105
+ }
106
+
107
+ return Object.freeze({
108
+ diagnosed: true,
109
+ rule: nextRule,
110
+ promoted: Boolean(promoted),
111
+ });
112
+ }
113
+
114
+ function endSession() {
115
+ if (!started) return;
116
+ memory.reset?.({ tier: 1, sessionId: currentSessionId });
117
+ started = false;
118
+ currentSessionId = null;
119
+ }
120
+
121
+ function getStats() {
122
+ return Object.freeze({
123
+ tier1Count: resolveTierCount(memory, 1),
124
+ tier2Count: resolveTierCount(memory, 2),
125
+ tier3Count: resolveTierCount(memory, 3),
126
+ totalErrors: counters.totalErrors,
127
+ });
128
+ }
129
+
130
+ return Object.freeze({ handleError, startSession, endSession, getStats, __lastFingerprintId: () => lastFingerprintId });
131
+ }
132
+
133
+ export function createAdaptiveEngine(opts = {}) {
134
+ if (singletonEngine) return singletonEngine;
135
+ singletonEngine = createEngineInstance(opts);
136
+ return singletonEngine;
137
+ }
138
+
139
+ export function __resetAdaptiveEngineForTests() {
140
+ singletonEngine = null;
141
+ }
142
+
143
+ export default createAdaptiveEngine;
@@ -1,11 +1,56 @@
1
1
  // hub/cli-adapter-base.mjs — codex/gemini 공통 CLI adapter 인터페이스
2
2
  // Phase 2: codex-adapter.mjs에서 추출한 재사용 가능 유틸리티
3
3
 
4
- import { spawn } from 'node:child_process';
4
+ import { execSync, spawn } from 'node:child_process';
5
5
  import { existsSync, readFileSync } from 'node:fs';
6
6
 
7
7
  import { killProcess, IS_WINDOWS } from './platform.mjs';
8
8
 
9
+ // ── Codex CLI compatibility ─────────────────────────────────────
10
+
11
+ let _cachedVersion = null;
12
+
13
+ /**
14
+ * `codex --version` 실행 결과를 파싱하여 마이너 버전 숫자 반환.
15
+ * 파싱 실패 시 0 반환 (구버전으로 간주).
16
+ * @returns {number} 마이너 버전 (예: 0.117.0 → 117)
17
+ */
18
+ export function getCodexVersion() {
19
+ if (_cachedVersion !== null) return _cachedVersion;
20
+ try {
21
+ const out = execSync('codex --version', { encoding: 'utf8', timeout: 5000 }).trim();
22
+ const match = out.match(/(\d+)\.(\d+)\.(\d+)/);
23
+ _cachedVersion = match ? Number.parseInt(match[2], 10) : 0;
24
+ } catch {
25
+ _cachedVersion = 0;
26
+ }
27
+ return _cachedVersion;
28
+ }
29
+
30
+ /**
31
+ * 최소 마이너 버전 이상인지 확인.
32
+ * @param {number} minMinor
33
+ * @returns {boolean}
34
+ */
35
+ export function gte(minMinor) {
36
+ return getCodexVersion() >= minMinor;
37
+ }
38
+
39
+ /**
40
+ * Codex CLI 기능별 분기 객체.
41
+ * 117 = 0.117.0 (Rust 리라이트, exec 서브커맨드 도입)
42
+ */
43
+ export const FEATURES = {
44
+ /** exec 서브커맨드 사용 가능 여부 */
45
+ get execSubcommand() { return gte(117); },
46
+ /** --output-last-message 플래그 지원 여부 */
47
+ get outputLastMessage() { return gte(117); },
48
+ /** --color never 플래그 지원 여부 */
49
+ get colorNever() { return gte(117); },
50
+ /** 플러그인 시스템 지원 여부 (향후 확장용) */
51
+ get pluginSystem() { return gte(120); },
52
+ };
53
+
9
54
  // ── Shell utilities ─────────────────────────────────────────────
10
55
 
11
56
  export function normalizePathForShell(value) {
@@ -16,6 +61,49 @@ export function shellQuote(value) {
16
61
  return JSON.stringify(String(value));
17
62
  }
18
63
 
64
+ export function escapePwshSingleQuoted(value) {
65
+ return String(value).replace(/'/g, "''");
66
+ }
67
+
68
+ export const CODEX_MCP_TRANSPORT_EXIT_CODE = 70;
69
+ export const CODEX_MCP_EXECUTION_EXIT_CODE = 1;
70
+
71
+ /**
72
+ * long-form 플래그 기반 명령 빌더.
73
+ * @param {string} prompt
74
+ * @param {string|null} resultFile — null이면 --output-last-message 생략
75
+ * @param {{ profile?: string, skipGitRepoCheck?: boolean, sandboxBypass?: boolean, cwd?: string, mcpServers?: string[] }} [opts]
76
+ * @returns {string} 실행할 셸 커맨드
77
+ */
78
+ export function buildExecCommand(prompt, resultFile = null, opts = {}) {
79
+ const { profile, skipGitRepoCheck = true, sandboxBypass = true, cwd, mcpServers } = opts;
80
+
81
+ const parts = ['codex'];
82
+ if (profile) parts.push('--profile', profile);
83
+
84
+ if (FEATURES.execSubcommand) {
85
+ parts.push('exec');
86
+ if (sandboxBypass) parts.push('--dangerously-bypass-approvals-and-sandbox');
87
+ if (skipGitRepoCheck) parts.push('--skip-git-repo-check');
88
+ if (resultFile && FEATURES.outputLastMessage) {
89
+ parts.push('--output-last-message', resultFile);
90
+ }
91
+ if (FEATURES.colorNever) parts.push('--color', 'never');
92
+ if (cwd) parts.push('--cwd', `'${escapePwshSingleQuoted(cwd)}'`);
93
+ if (Array.isArray(mcpServers)) {
94
+ for (const server of mcpServers) {
95
+ parts.push('-c', `mcp_servers.${server}.enabled=true`);
96
+ }
97
+ }
98
+ } else {
99
+ parts.push('--dangerously-bypass-approvals-and-sandbox');
100
+ if (skipGitRepoCheck) parts.push('--skip-git-repo-check');
101
+ }
102
+
103
+ parts.push(JSON.stringify(prompt));
104
+ return parts.join(' ');
105
+ }
106
+
19
107
  // ── Sleep ───────────────────────────────────────────────────────
20
108
 
21
109
  export function sleep(ms) {
@@ -4,11 +4,11 @@ import { tmpdir } from 'node:os';
4
4
 
5
5
  import { runPreflight } from './codex-preflight.mjs';
6
6
  import { withRetry } from './workers/worker-utils.mjs';
7
- import { buildExecCommand } from './codex-compat.mjs';
8
7
  import {
9
8
  createCircuitBreaker,
10
9
  createResult,
11
10
  appendWarnings,
11
+ buildExecCommand,
12
12
  normalizePathForShell,
13
13
  shellQuote,
14
14
  runProcess,
@@ -20,6 +20,7 @@ const breaker = createCircuitBreaker();
20
20
 
21
21
  function inferStallMode(stdout, stderr) {
22
22
  const text = `${stdout}\n${stderr}`.toLowerCase();
23
+ if (/(rate.?limit|quota|throttl|too.many.requests|429|usage.limit)/u.test(text)) return 'rate_limited';
23
24
  if (/(approval|approve|permission|sandbox|bypass)/u.test(text)) return 'approval_stall';
24
25
  if (/\bmcp\b|context7|playwright|tavily|exa|brave|sequential|server/u.test(text)) return 'mcp_stall';
25
26
  return 'timeout';
@@ -97,16 +98,24 @@ export function buildExecArgs(opts = {}) {
97
98
  profile: opts.profile,
98
99
  skipGitRepoCheck: true,
99
100
  sandboxBypass: true,
101
+ cwd: opts.cwd,
100
102
  });
101
103
 
102
104
  if (!prompt) return command.replace(/\s+""$/u, '');
103
105
 
106
+ let result;
104
107
  const quotedPrompt = JSON.stringify(prompt);
105
108
  if (/^\(Get-Content\b[\s\S]*\)$/u.test(prompt) && command.endsWith(quotedPrompt)) {
106
- return `${command.slice(0, -quotedPrompt.length)}${prompt}`;
109
+ result = `${command.slice(0, -quotedPrompt.length)}${prompt}`;
110
+ } else {
111
+ result = command;
107
112
  }
108
113
 
109
- return command;
114
+ // stderr 캡처: codex 실패 시에도 원인 추적 가능 (resultFile.err)
115
+ if (opts.resultFile) {
116
+ result += ` 2>'${opts.resultFile}.err'`;
117
+ }
118
+ return result;
110
119
  }
111
120
 
112
121
  // ── Codex execution ─────────────────────────────────────────────
@@ -1,78 +1,11 @@
1
- // hub/team/codex-compat.mjs — Codex CLI 버전 어댑터
2
- // Codex 0.117.0+ (Rust 리라이트): exec 서브커맨드 기반
3
- import { execSync } from "node:child_process";
4
-
5
- let _cachedVersion = null;
6
-
7
- /**
8
- * `codex --version` 실행 결과를 파싱하여 마이너 버전 숫자 반환.
9
- * 파싱 실패 시 0 반환 (구버전으로 간주).
10
- * @returns {number} 마이너 버전 (예: 0.117.0 → 117)
11
- */
12
- export function getCodexVersion() {
13
- if (_cachedVersion !== null) return _cachedVersion;
14
- try {
15
- const out = execSync("codex --version", { encoding: "utf8", timeout: 5000 }).trim();
16
- // "codex 0.117.0" 또는 "0.117.0" 형식 대응
17
- const m = out.match(/(\d+)\.(\d+)\.(\d+)/);
18
- _cachedVersion = m ? parseInt(m[2], 10) : 0;
19
- } catch {
20
- _cachedVersion = 0;
21
- }
22
- return _cachedVersion;
23
- }
24
-
25
- /**
26
- * 최소 마이너 버전 이상인지 확인.
27
- * @param {number} minMinor
28
- * @returns {boolean}
29
- */
30
- export function gte(minMinor) {
31
- return getCodexVersion() >= minMinor;
32
- }
33
-
34
- /**
35
- * Codex CLI 기능별 분기 객체.
36
- * 117 = 0.117.0 (Rust 리라이트, exec 서브커맨드 도입)
37
- */
38
- export const FEATURES = {
39
- /** exec 서브커맨드 사용 가능 여부 */
40
- get execSubcommand() { return gte(117); },
41
- /** --output-last-message 플래그 지원 여부 */
42
- get outputLastMessage() { return gte(117); },
43
- /** --color never 플래그 지원 여부 */
44
- get colorNever() { return gte(117); },
45
- /** 플러그인 시스템 지원 여부 (향후 확장용) */
46
- get pluginSystem() { return gte(120); },
47
- };
48
-
49
- /**
50
- * long-form 플래그 기반 명령 빌더.
51
- * @param {string} prompt
52
- * @param {string|null} resultFile — null이면 --output-last-message 생략
53
- * @param {{ profile?: string, skipGitRepoCheck?: boolean, sandboxBypass?: boolean }} [opts]
54
- * @returns {string} 실행할 셸 커맨드
55
- */
56
- export function buildExecCommand(prompt, resultFile = null, opts = {}) {
57
- const { profile, skipGitRepoCheck = true, sandboxBypass = true } = opts;
58
-
59
- const parts = ["codex"];
60
- if (profile) parts.push("--profile", profile);
61
-
62
- if (FEATURES.execSubcommand) {
63
- parts.push("exec");
64
- if (sandboxBypass) parts.push("--dangerously-bypass-approvals-and-sandbox");
65
- if (skipGitRepoCheck) parts.push("--skip-git-repo-check");
66
- if (resultFile && FEATURES.outputLastMessage) {
67
- parts.push("--output-last-message", resultFile);
68
- }
69
- if (FEATURES.colorNever) parts.push("--color", "never");
70
- } else {
71
- // 구버전 fallback
72
- parts.push("--dangerously-bypass-approvals-and-sandbox");
73
- if (skipGitRepoCheck) parts.push("--skip-git-repo-check");
74
- }
75
-
76
- parts.push(JSON.stringify(prompt));
77
- return parts.join(" ");
78
- }
1
+ // hub/codex-compat.mjs — backward-compatible facade for legacy imports
2
+
3
+ export {
4
+ CODEX_MCP_EXECUTION_EXIT_CODE,
5
+ CODEX_MCP_TRANSPORT_EXIT_CODE,
6
+ FEATURES,
7
+ buildExecCommand,
8
+ escapePwshSingleQuoted,
9
+ getCodexVersion,
10
+ gte,
11
+ } from './cli-adapter-base.mjs';
@@ -1,11 +1,12 @@
1
1
  import { readFileSync, existsSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { homedir } from 'node:os';
4
+ import { execSync } from 'node:child_process';
4
5
 
5
6
  import { whichCommandAsync } from './platform.mjs';
6
- import { getCodexVersion } from './codex-compat.mjs';
7
7
 
8
8
  const MIN_RECOMMENDED_MINOR = 118;
9
+ let _cachedVersion = null;
9
10
 
10
11
  function escapeRegExp(value) {
11
12
  return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
@@ -38,6 +39,24 @@ function readSection(text, name) {
38
39
  return body.join('\n');
39
40
  }
40
41
 
42
+ /**
43
+ * `codex --version` 실행 결과를 파싱하여 마이너 버전 숫자 반환.
44
+ * 파싱 실패 시 0 반환 (구버전으로 간주).
45
+ * @returns {number} 마이너 버전 (예: 0.117.0 → 117)
46
+ */
47
+ export function getCodexVersion() {
48
+ if (_cachedVersion !== null) return _cachedVersion;
49
+ try {
50
+ const out = execSync('codex --version', { encoding: 'utf8', timeout: 5000 }).trim();
51
+ // "codex 0.117.0" 또는 "0.117.0" 형식 대응
52
+ const match = out.match(/(\d+)\.(\d+)\.(\d+)/);
53
+ _cachedVersion = match ? parseInt(match[2], 10) : 0;
54
+ } catch {
55
+ _cachedVersion = 0;
56
+ }
57
+ return _cachedVersion;
58
+ }
59
+
41
60
  async function checkCodexInstalled() {
42
61
  const codexPath = await whichCommandAsync('codex');
43
62
  if (codexPath) return { codexPath, ok: true, warnings: [] };
@@ -22,6 +22,7 @@ const breaker = createCircuitBreaker();
22
22
 
23
23
  function inferStallMode(stdout, stderr) {
24
24
  const text = `${stdout}\n${stderr}`.toLowerCase();
25
+ if (/(rate.?limit|quota|resource.?exhaust|429)/u.test(text)) return 'rate_limited';
25
26
  if (/(unauthorized|forbidden|auth|login|token|credential|api.?key)/u.test(text)) return 'auth_stall';
26
27
  if (/\bmcp\b|playwright|tavily|brave|sequential|server/u.test(text)) return 'mcp_stall';
27
28
  return 'timeout';
package/hub/index.mjs ADDED
@@ -0,0 +1,34 @@
1
+ // @triflux/core — barrel export (auto-generated by pack.mjs)
2
+ // Adapters
3
+ export { buildExecCommand, sleep, gte, FEATURES, CODEX_MCP_TRANSPORT_EXIT_CODE, normalizePathForShell, shellQuote } from './cli-adapter-base.mjs';
4
+ export { execute as executeCodex, buildExecArgs as buildCodexArgs, buildLaunchScript, getCircuitState as getCodexCircuit } from './codex-adapter.mjs';
5
+ export { execute as executeGemini, buildExecArgs as buildGeminiArgs, getCircuitState as getGeminiCircuit } from './gemini-adapter.mjs';
6
+ export { getCodexVersion, runPreflight } from './codex-preflight.mjs';
7
+
8
+ // Routing & Intent
9
+ export { createRouter } from './router.mjs';
10
+ export { classifyIntent, quickClassify, INTENT_CATEGORIES, refineClassification } from './intent.mjs';
11
+
12
+ // State & Paths & Platform
13
+ export * from './paths.mjs';
14
+ export { IS_WINDOWS, IS_MAC, IS_LINUX, TEMP_DIR, normalizePath, whichCommand, killProcess, pipePath } from './platform.mjs';
15
+
16
+ // Core services
17
+ export { createHitlManager } from './hitl.mjs';
18
+ export { createAssignCallbackServer } from './assign-callbacks.mjs';
19
+ export { getHubUrl, getHubPipePath, requestJson, post, connectPipe, parseArgs, parseJsonSafe } from './bridge.mjs';
20
+
21
+ // Fullcycle & Research
22
+ export { createFullcycleRunId, getFullcycleRunDir, ensureFullcycleRunDir, saveFullcycleArtifact, readFullcycleArtifact, writeFullcycleState, readFullcycleState, shouldStopQaLoop } from './fullcycle.mjs';
23
+ export { generateQueries, normalizeResults, buildReport, saveReport } from './research.mjs';
24
+
25
+ // Reflexion (adaptive error learning)
26
+ export { normalizeError, lookupSolution, learnFromError, reportOutcome, adaptiveRuleFromError, getActiveAdaptiveRules } from './reflexion.mjs';
27
+
28
+ // Neural Memory (Lake 1.5)
29
+ export { createAdaptiveEngine } from './adaptive.mjs';
30
+ export { createAdaptiveMemory } from './adaptive-memory.mjs';
31
+ export { createDiagnosticPipeline } from './adaptive-diagnostic.mjs';
32
+ export { createAdaptiveInjector } from './adaptive-inject.mjs';
33
+ export { buildAdaptiveFingerprint, createAdaptiveFingerprintService } from './session-fingerprint.mjs';
34
+ export { broker as accountBroker } from './account-broker.mjs';
@@ -0,0 +1,114 @@
1
+ import { accessSync, constants, existsSync, readFileSync, readdirSync } from "node:fs";
2
+ import http from "node:http";
3
+ import https from "node:https";
4
+ import { basename, join, relative } from "node:path";
5
+
6
+ const NETWORK_TIMEOUT_MS = 3_000;
7
+
8
+ function toIssuePath(cacheDir, filePath) {
9
+ const relPath = relative(cacheDir, filePath);
10
+ return (relPath && relPath.length > 0 ? relPath : basename(filePath)).replace(/\\/g, "/");
11
+ }
12
+
13
+ function collectCacheFiles(cacheDir) {
14
+ return readdirSync(cacheDir, { withFileTypes: true }).flatMap((entry) => {
15
+ const filePath = join(cacheDir, entry.name);
16
+ if (entry.isDirectory()) return collectCacheFiles(filePath);
17
+ return entry.isFile() ? [filePath] : [];
18
+ });
19
+ }
20
+
21
+ function validateCacheFile(cacheDir, filePath) {
22
+ try {
23
+ accessSync(filePath, constants.R_OK);
24
+ if (filePath.endsWith(".json")) {
25
+ JSON.parse(readFileSync(filePath, "utf8"));
26
+ }
27
+ return null;
28
+ } catch (error) {
29
+ return {
30
+ file: toIssuePath(cacheDir, filePath),
31
+ error: error instanceof Error ? error.message : String(error),
32
+ };
33
+ }
34
+ }
35
+
36
+ function probeUrl(url) {
37
+ return new Promise((resolve) => {
38
+ let settled = false;
39
+
40
+ const finish = (ok) => {
41
+ if (settled) return;
42
+ settled = true;
43
+ resolve({ url, ok });
44
+ };
45
+
46
+ try {
47
+ const parsed = new URL(url);
48
+ const transport = parsed.protocol === "https:" ? https : parsed.protocol === "http:" ? http : null;
49
+ if (!transport) {
50
+ finish(false);
51
+ return;
52
+ }
53
+
54
+ const request = transport.request(parsed, {
55
+ method: "HEAD",
56
+ headers: { "user-agent": "triflux-cache-guard" },
57
+ }, (response) => {
58
+ response.resume();
59
+ finish(true);
60
+ });
61
+
62
+ request.setTimeout(NETWORK_TIMEOUT_MS, () => {
63
+ request.destroy(new Error("timeout"));
64
+ });
65
+ request.on("error", () => finish(false));
66
+ request.end();
67
+ } catch {
68
+ finish(false);
69
+ }
70
+ });
71
+ }
72
+
73
+ export function validateRuntimeCachePaths(cacheDir) {
74
+ if (!cacheDir || !existsSync(cacheDir)) {
75
+ return { ok: true, issues: [] };
76
+ }
77
+
78
+ try {
79
+ const files = collectCacheFiles(cacheDir);
80
+ const issues = files
81
+ .map((filePath) => validateCacheFile(cacheDir, filePath))
82
+ .filter((issue) => issue !== null);
83
+
84
+ return {
85
+ ok: issues.length === 0,
86
+ issues,
87
+ };
88
+ } catch (error) {
89
+ return {
90
+ ok: false,
91
+ issues: [{
92
+ file: ".",
93
+ error: error instanceof Error ? error.message : String(error),
94
+ }],
95
+ };
96
+ }
97
+ }
98
+
99
+ export async function checkNetworkAvailability(urls) {
100
+ const targets = [...new Set((Array.isArray(urls) ? urls : []).filter(Boolean))];
101
+ if (targets.length === 0) {
102
+ return { online: true, reachable: [], unreachable: [] };
103
+ }
104
+
105
+ const results = await Promise.all(targets.map((url) => probeUrl(url)));
106
+ const reachable = results.filter((result) => result.ok).map((result) => result.url);
107
+ const unreachable = results.filter((result) => !result.ok).map((result) => result.url);
108
+
109
+ return {
110
+ online: unreachable.length === 0,
111
+ reachable,
112
+ unreachable,
113
+ };
114
+ }
@@ -0,0 +1,72 @@
1
+ {
2
+ "$schema": "Neural Memory 에러 시그니처 시드 — 자동 학습 전 수동 등록 패턴",
3
+ "version": 1,
4
+ "signatures": {
5
+ "ssh-powershell-devnull": {
6
+ "pattern": "Could not find a part of the path.*dev.null",
7
+ "tool": "Bash",
8
+ "context": "SSH 명령 실행",
9
+ "root_cause": "원격 호스트가 PowerShell인데 bash 문법(2>/dev/null)을 사용",
10
+ "dna_factor": "remote.os",
11
+ "rule_template": "원격 호스트 {host}는 PowerShell 기본 셸. 2>/dev/null 대신 2>$null, && 대신 ;, $(cmd) 대신 $(cmd) PowerShell 구문 사용.",
12
+ "fix": "hub/lib/ssh-command.mjs의 suppressStderr(os) 사용",
13
+ "confidence_base": 0.95,
14
+ "severity": "high"
15
+ },
16
+ "ssh-powershell-ampamp": {
17
+ "pattern": "The token '&&' is not a valid statement separator",
18
+ "tool": "Bash",
19
+ "context": "SSH 명령 실행",
20
+ "root_cause": "원격 호스트가 PowerShell인데 bash의 && 연결자를 사용",
21
+ "dna_factor": "remote.os",
22
+ "rule_template": "원격 호스트 {host}는 PowerShell 기본 셸. && 대신 ; 사용.",
23
+ "fix": "hub/lib/ssh-command.mjs의 commandJoin(os) 사용",
24
+ "confidence_base": 0.95,
25
+ "severity": "high"
26
+ },
27
+ "codex-sandbox-duplicate": {
28
+ "pattern": "cannot be used multiple times",
29
+ "tool": "Bash",
30
+ "context": "codex exec 실행",
31
+ "root_cause": "config.toml sandbox 설정과 CLI 플래그(--full-auto) 중복",
32
+ "dna_factor": "cli.codex.config.sandbox",
33
+ "rule_template": "Codex config.toml에 sandbox=\"{value}\" 설정이 있으므로 CLI에서 --full-auto 플래그 사용 금지",
34
+ "fix": "config.toml에 기본값을 두고, CLI에서는 --profile만 선택",
35
+ "confidence_base": 0.95,
36
+ "severity": "critical"
37
+ },
38
+ "codex-exec-approval-stall": {
39
+ "pattern": "Reading additional input from stdin",
40
+ "tool": "Bash",
41
+ "context": "codex exec 비대화형 실행",
42
+ "root_cause": "codex exec이 config.toml approval_mode를 무시하여 무한 대기",
43
+ "dna_factor": null,
44
+ "rule_template": "codex exec은 config.toml approval_mode를 무시함. 반드시 --dangerously-bypass-approvals-and-sandbox 명시적 추가 필수",
45
+ "fix": "codex exec 호출 시 항상 --dangerously-bypass-approvals-and-sandbox 플래그 포함",
46
+ "confidence_base": 1.0,
47
+ "severity": "critical"
48
+ },
49
+ "ssh-stdin-hang": {
50
+ "pattern": "Reading additional input from stdin|Pseudo-terminal will not be allocated",
51
+ "tool": "Bash",
52
+ "context": "SSH 원격 CLI 실행",
53
+ "root_cause": "SSH 세션에서 stdin이 리다이렉트되지 않음",
54
+ "dna_factor": null,
55
+ "rule_template": "SSH 원격 실행 시 ssh -n 또는 stdin: 'ignore' 옵션으로 stdin 리다이렉트 필수",
56
+ "fix": "execFileSync('ssh', [...], { stdio: ['ignore', 'pipe', 'pipe'] })",
57
+ "confidence_base": 0.9,
58
+ "severity": "high"
59
+ },
60
+ "psmux-node-not-found": {
61
+ "pattern": "node: not found|node: command not found",
62
+ "tool": "Bash",
63
+ "context": "psmux 세션에서 node 실행",
64
+ "root_cause": "psmux 세션이 PowerShell 7인데 bash 경로의 node를 참조",
65
+ "dna_factor": "multiplexer.name",
66
+ "rule_template": "psmux pane은 PowerShell 7 환경. bash PATH의 node 경로가 아닌 PowerShell PATH를 사용해야 함. wrapCliForBash 래핑 금지.",
67
+ "fix": "PowerShell 네이티브 명령 사용, bash 래핑 금지",
68
+ "confidence_base": 0.9,
69
+ "severity": "high"
70
+ }
71
+ }
72
+ }