@triflux/core 10.1.0 → 10.25.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/hooks/agent-route-guard.mjs +14 -10
  2. package/hooks/cross-review-tracker.mjs +21 -8
  3. package/hooks/error-context.mjs +20 -8
  4. package/hooks/hook-adaptive-collector.mjs +18 -16
  5. package/hooks/hook-manager.mjs +93 -32
  6. package/hooks/hook-orchestrator.mjs +267 -35
  7. package/hooks/hook-registry.json +343 -256
  8. package/hooks/hooks.json +49 -1
  9. package/hooks/keyword-rules.json +183 -23
  10. package/hooks/lib/resolve-root.mjs +1 -1
  11. package/hooks/mcp-config-watcher.mjs +6 -2
  12. package/hooks/permission-safe-allow.mjs +233 -0
  13. package/hooks/pipeline-stop.mjs +369 -22
  14. package/hooks/post-tool-tips.mjs +306 -0
  15. package/hooks/pre-compact-snapshot.mjs +181 -0
  16. package/hooks/safety-guard.mjs +254 -33
  17. package/hooks/session-end-cleanup.mjs +345 -0
  18. package/hooks/session-start-fast.mjs +214 -0
  19. package/hooks/subagent-tracker.mjs +267 -0
  20. package/hooks/subagent-verifier.mjs +9 -4
  21. package/hub/account-broker.mjs +1036 -66
  22. package/hub/adaptive-diagnostic.mjs +75 -48
  23. package/hub/adaptive-inject.mjs +96 -58
  24. package/hub/adaptive-memory.mjs +156 -42
  25. package/hub/adaptive.mjs +60 -31
  26. package/hub/assign-callbacks.mjs +163 -126
  27. package/hub/bridge.mjs +831 -290
  28. package/hub/cli-adapter-base.mjs +321 -97
  29. package/hub/codex-adapter.mjs +179 -93
  30. package/hub/codex-compat.mjs +4 -4
  31. package/hub/codex-preflight.mjs +113 -37
  32. package/hub/delegator/contracts.mjs +19 -23
  33. package/hub/delegator/index.mjs +14 -14
  34. package/hub/delegator/service.mjs +88 -64
  35. package/hub/delegator/tool-definitions.mjs +35 -35
  36. package/hub/dynamic-routing-engine.mjs +511 -0
  37. package/hub/fullcycle.mjs +34 -18
  38. package/hub/gemini-adapter.mjs +77 -95
  39. package/hub/hitl.mjs +202 -143
  40. package/hub/intent.mjs +167 -38
  41. package/hub/lib/bash-path.mjs +73 -0
  42. package/hub/lib/cache-guard.mjs +43 -17
  43. package/hub/lib/env-detect.mjs +145 -0
  44. package/hub/lib/hosts-compat.mjs +319 -0
  45. package/hub/lib/mcp-response-cache.mjs +239 -0
  46. package/hub/lib/memory-store.mjs +285 -111
  47. package/hub/lib/path-utils.mjs +165 -0
  48. package/hub/lib/phase-manager.mjs +493 -0
  49. package/hub/lib/process-utils.mjs +797 -104
  50. package/hub/lib/prompt-tmp.mjs +33 -0
  51. package/hub/lib/risk-tier.mjs +53 -0
  52. package/hub/lib/spawn-trace.mjs +561 -0
  53. package/hub/lib/ssh-command.mjs +44 -24
  54. package/hub/lib/ssh-retry.mjs +5 -1
  55. package/hub/lib/staged-file-detect.mjs +43 -0
  56. package/hub/lib/state-snapshot.mjs +293 -0
  57. package/hub/lib/tfx-route-args.mjs +309 -0
  58. package/hub/lib/timeline-adapter.mjs +190 -0
  59. package/hub/lib/trace-recorder.mjs +153 -0
  60. package/hub/lib/uuidv7.mjs +4 -3
  61. package/hub/memory-doctor.mjs +266 -106
  62. package/hub/middleware/quota-middleware.mjs +70 -0
  63. package/hub/middleware/request-logger.mjs +61 -34
  64. package/hub/paths.mjs +9 -9
  65. package/hub/pipeline/gates/confidence.mjs +34 -15
  66. package/hub/pipeline/gates/consensus.mjs +27 -15
  67. package/hub/pipeline/gates/index.mjs +7 -3
  68. package/hub/pipeline/gates/selfcheck.mjs +57 -19
  69. package/hub/pipeline/index.mjs +77 -42
  70. package/hub/pipeline/state.mjs +10 -10
  71. package/hub/pipeline/transitions.mjs +40 -23
  72. package/hub/platform.mjs +65 -48
  73. package/hub/quality/deslop.mjs +70 -49
  74. package/hub/reflexion.mjs +87 -13
  75. package/hub/research.mjs +33 -25
  76. package/hub/router.mjs +941 -791
  77. package/hub/routing/complexity.mjs +132 -29
  78. package/hub/routing/index.mjs +17 -12
  79. package/hub/routing/q-learning.mjs +76 -28
  80. package/hub/routing-snapshot.mjs +261 -0
  81. package/hub/session-fingerprint.mjs +127 -61
  82. package/hub/state.mjs +86 -44
  83. package/hub/team/retry-state-machine.mjs +267 -0
  84. package/hub/token-mode.mjs +72 -71
  85. package/hub/workers/worker-utils.mjs +22 -16
  86. package/hud/colors.mjs +27 -9
  87. package/hud/constants.mjs +184 -27
  88. package/hud/context-monitor.mjs +133 -44
  89. package/hud/hud-qos-status.mjs +153 -50
  90. package/hud/mission-board.mjs +56 -0
  91. package/hud/providers/claude.mjs +398 -114
  92. package/hud/providers/codex.mjs +96 -21
  93. package/hud/providers/gemini.mjs +184 -51
  94. package/hud/renderers.mjs +413 -123
  95. package/hud/terminal.mjs +101 -31
  96. package/hud/utils.mjs +81 -39
  97. package/mesh/index.mjs +69 -0
  98. package/mesh/mesh-budget.mjs +137 -0
  99. package/mesh/mesh-heartbeat.mjs +100 -0
  100. package/mesh/mesh-protocol.mjs +96 -0
  101. package/mesh/mesh-queue.mjs +163 -0
  102. package/mesh/mesh-registry.mjs +78 -0
  103. package/mesh/mesh-router.mjs +77 -0
  104. package/package.json +5 -1
  105. package/scripts/lib/agent-json.mjs +27 -0
  106. package/scripts/lib/async.mjs +174 -0
  107. package/scripts/lib/claudemd-scanner.mjs +6 -1
  108. package/scripts/lib/cli-agy.mjs +62 -0
  109. package/scripts/lib/cli-claude.mjs +78 -0
  110. package/scripts/lib/cli-codex.mjs +199 -0
  111. package/scripts/lib/cli-gemini.mjs +67 -0
  112. package/scripts/lib/codex-recovery.sh +50 -0
  113. package/scripts/lib/context.mjs +3 -3
  114. package/scripts/lib/cross-review-utils.mjs +13 -5
  115. package/scripts/lib/doctor-env-checks.mjs +121 -0
  116. package/scripts/lib/dynamic-route-cli.mjs +107 -0
  117. package/scripts/lib/env-probe.mjs +58 -30
  118. package/scripts/lib/env.mjs +14 -0
  119. package/scripts/lib/gemini-profiles.mjs +44 -10
  120. package/scripts/lib/handoff.mjs +33 -17
  121. package/scripts/lib/hook-utils.mjs +8 -6
  122. package/scripts/lib/hub.mjs +48 -0
  123. package/scripts/lib/keyword-rules.mjs +48 -19
  124. package/scripts/lib/logger.mjs +24 -24
  125. package/scripts/lib/mcp-filter.mjs +391 -247
  126. package/scripts/lib/mcp-gateway-health-check.mjs +159 -0
  127. package/scripts/lib/mcp-gateway-servers.mjs +70 -0
  128. package/scripts/lib/mcp-gateway-wrapper-check.mjs +35 -0
  129. package/scripts/lib/mcp-guard-engine.mjs +1281 -115
  130. package/scripts/lib/mcp-health.mjs +773 -0
  131. package/scripts/lib/mcp-manifest.mjs +24 -14
  132. package/scripts/lib/mcp-server-catalog.mjs +355 -118
  133. package/scripts/lib/pid.mjs +63 -0
  134. package/scripts/lib/process-utils.mjs +26 -0
  135. package/scripts/lib/psmux-info.mjs +70 -10
  136. package/scripts/lib/quota.mjs +47 -0
  137. package/scripts/lib/remote-spawn-transfer.mjs +44 -14
  138. package/scripts/lib/skill-state.mjs +220 -0
  139. package/scripts/lib/skill-template.mjs +32 -7
  140. package/scripts/lib/team.mjs +75 -0
  141. package/scripts/lib/timeout.mjs +66 -0
  142. package/scripts/lib/tmp.mjs +26 -0
  143. package/scripts/lib/toml.mjs +113 -0
package/hud/terminal.mjs CHANGED
@@ -2,51 +2,111 @@
2
2
  // 터미널 감지 / 4-Tier 적응형 렌더링
3
3
  // ============================================================================
4
4
  import { execSync } from "node:child_process";
5
- import { DIM, RESET, GAUGE_WIDTH, coloredBar } from "./colors.mjs";
5
+ import { coloredBar, DIM, GAUGE_WIDTH, RESET } from "./colors.mjs";
6
+ import {
7
+ COMPACT_COLS_THRESHOLD,
8
+ HUD_CONFIG_PATH,
9
+ MINIMAL_COLS_THRESHOLD,
10
+ } from "./constants.mjs";
6
11
  import { readJson } from "./utils.mjs";
7
- import { HUD_CONFIG_PATH, COMPACT_COLS_THRESHOLD, MINIMAL_COLS_THRESHOLD } from "./constants.mjs";
8
12
 
9
13
  let _cachedColumns = 0;
10
14
  export function getTerminalColumns() {
11
15
  if (_cachedColumns > 0) return _cachedColumns;
12
16
  const envCols = Number(process.env.COLUMNS);
13
- if (envCols > 0) { _cachedColumns = envCols; return _cachedColumns; }
14
- if (process.stdout.columns) { _cachedColumns = process.stdout.columns; return _cachedColumns; }
15
- if (process.stderr.columns) { _cachedColumns = process.stderr.columns; return _cachedColumns; }
17
+ if (envCols > 0) {
18
+ _cachedColumns = envCols;
19
+ return _cachedColumns;
20
+ }
21
+ if (process.stdout.columns) {
22
+ _cachedColumns = process.stdout.columns;
23
+ return _cachedColumns;
24
+ }
25
+ if (process.stderr.columns) {
26
+ _cachedColumns = process.stderr.columns;
27
+ return _cachedColumns;
28
+ }
16
29
  try {
17
30
  if (process.platform === "win32") {
18
- const raw = execSync("mode con", { timeout: 2000, stdio: ["pipe", "pipe", "pipe"], windowsHide: true }).toString();
19
- const m = raw.match(/Columns[^:]*:\s*(\d+)/i) || raw.match(/열[^:]*:\s*(\d+)/);
20
- if (m) { _cachedColumns = Number(m[1]); return _cachedColumns; }
31
+ const raw = execSync("mode con", {
32
+ timeout: 2000,
33
+ stdio: ["pipe", "pipe", "pipe"],
34
+ windowsHide: true,
35
+ }).toString();
36
+ const m =
37
+ raw.match(/Columns[^:]*:\s*(\d+)/i) || raw.match(/열[^:]*:\s*(\d+)/);
38
+ if (m) {
39
+ _cachedColumns = Number(m[1]);
40
+ return _cachedColumns;
41
+ }
21
42
  } else {
22
- const raw = execSync("tput cols 2>/dev/null || stty size 2>/dev/null | awk '{print $2}'", {
23
- timeout: 2000, stdio: ["pipe", "pipe", "pipe"],
24
- }).toString().trim();
25
- if (raw && !isNaN(Number(raw))) { _cachedColumns = Number(raw); return _cachedColumns; }
43
+ const raw = execSync(
44
+ "tput cols 2>/dev/null || stty size 2>/dev/null | awk '{print $2}'",
45
+ {
46
+ timeout: 2000,
47
+ stdio: ["pipe", "pipe", "pipe"],
48
+ },
49
+ )
50
+ .toString()
51
+ .trim();
52
+ if (raw && !Number.isNaN(Number(raw))) {
53
+ _cachedColumns = Number(raw);
54
+ return _cachedColumns;
55
+ }
26
56
  }
27
- } catch { /* 감지 실패 */ }
57
+ } catch {
58
+ /* 감지 실패 */
59
+ }
28
60
  return 0;
29
61
  }
30
62
 
31
63
  let _cachedRows = 0;
32
64
  export function getTerminalRows() {
33
65
  if (_cachedRows > 0) return _cachedRows;
34
- if (process.stdout.rows) { _cachedRows = process.stdout.rows; return _cachedRows; }
35
- if (process.stderr.rows) { _cachedRows = process.stderr.rows; return _cachedRows; }
66
+ if (process.stdout.rows) {
67
+ _cachedRows = process.stdout.rows;
68
+ return _cachedRows;
69
+ }
70
+ if (process.stderr.rows) {
71
+ _cachedRows = process.stderr.rows;
72
+ return _cachedRows;
73
+ }
36
74
  const envLines = Number(process.env.LINES);
37
- if (envLines > 0) { _cachedRows = envLines; return _cachedRows; }
75
+ if (envLines > 0) {
76
+ _cachedRows = envLines;
77
+ return _cachedRows;
78
+ }
38
79
  try {
39
80
  if (process.platform === "win32") {
40
- const raw = execSync("mode con", { timeout: 2000, stdio: ["pipe", "pipe", "pipe"], windowsHide: true }).toString();
41
- const m = raw.match(/Lines[^:]*:\s*(\d+)/i) || raw.match(/줄[^:]*:\s*(\d+)/);
42
- if (m) { _cachedRows = Number(m[1]); return _cachedRows; }
81
+ const raw = execSync("mode con", {
82
+ timeout: 2000,
83
+ stdio: ["pipe", "pipe", "pipe"],
84
+ windowsHide: true,
85
+ }).toString();
86
+ const m =
87
+ raw.match(/Lines[^:]*:\s*(\d+)/i) || raw.match(/줄[^:]*:\s*(\d+)/);
88
+ if (m) {
89
+ _cachedRows = Number(m[1]);
90
+ return _cachedRows;
91
+ }
43
92
  } else {
44
- const raw = execSync("tput lines 2>/dev/null || stty size 2>/dev/null | awk '{print $1}'", {
45
- timeout: 2000, stdio: ["pipe", "pipe", "pipe"],
46
- }).toString().trim();
47
- if (raw && !isNaN(Number(raw))) { _cachedRows = Number(raw); return _cachedRows; }
93
+ const raw = execSync(
94
+ "tput lines 2>/dev/null || stty size 2>/dev/null | awk '{print $1}'",
95
+ {
96
+ timeout: 2000,
97
+ stdio: ["pipe", "pipe", "pipe"],
98
+ },
99
+ )
100
+ .toString()
101
+ .trim();
102
+ if (raw && !Number.isNaN(Number(raw))) {
103
+ _cachedRows = Number(raw);
104
+ return _cachedRows;
105
+ }
48
106
  }
49
- } catch { /* 감지 실패 */ }
107
+ } catch {
108
+ /* 감지 실패 */
109
+ }
50
110
  return 0;
51
111
  }
52
112
 
@@ -60,12 +120,15 @@ export function detectCompactMode() {
60
120
  if (process.env.OMC_HUD_COMPACT === "0") return false;
61
121
  // 3. 설정 파일 (~/.omc/config/hud.json)
62
122
  const hudConfig = readJson(HUD_CONFIG_PATH, null);
63
- if (hudConfig?.compact === true || hudConfig?.compact === "always") return true;
64
- if (hudConfig?.compact === false || hudConfig?.compact === "never") return false;
123
+ if (hudConfig?.compact === true || hudConfig?.compact === "always")
124
+ return true;
125
+ if (hudConfig?.compact === false || hudConfig?.compact === "never")
126
+ return false;
65
127
  // 4. maxLines < 3이면 자동 컴팩트 (알림 배너 공존 대응)
66
128
  if (Number(hudConfig?.lines) > 0 && Number(hudConfig?.lines) < 3) return true;
67
129
  // 5. 터미널 폭 자동 감지 (TTY 있을 때만 유효)
68
- const threshold = Number(hudConfig?.compactThreshold) || COMPACT_COLS_THRESHOLD;
130
+ const threshold =
131
+ Number(hudConfig?.compactThreshold) || COMPACT_COLS_THRESHOLD;
69
132
  const cols = getTerminalColumns();
70
133
  if (cols > 0 && cols < threshold) return true;
71
134
  return false;
@@ -95,7 +158,8 @@ export function selectTier(stdin, claudeUsage = null) {
95
158
 
96
159
  // 1) 명시적 tier 강제 설정
97
160
  const forcedTier = hudConfig?.tier;
98
- if (["full", "compact", "minimal", "micro", "nano"].includes(forcedTier)) return forcedTier;
161
+ if (["full", "compact", "minimal", "micro", "nano"].includes(forcedTier))
162
+ return forcedTier;
99
163
 
100
164
  // 1.5) maxLines=1 → nano (1줄 모드: 알림 배너/분할화면 대응)
101
165
  if (Number(hudConfig?.lines) === 1) return "nano";
@@ -123,14 +187,20 @@ export function selectTier(stdin, claudeUsage = null) {
123
187
 
124
188
  // full tier 전용: 게이지 바 접두사 (normal 이하 tier에서는 빈 문자열)
125
189
  export function tierBar(currentTier, percent, baseColor = null) {
126
- return currentTier === "full" ? coloredBar(percent, GAUGE_WIDTH, baseColor) + " " : "";
190
+ return currentTier === "full"
191
+ ? coloredBar(percent, GAUGE_WIDTH, baseColor) + " "
192
+ : "";
127
193
  }
128
194
  export function tierDimBar(currentTier) {
129
- return currentTier === "full" ? DIM + "░".repeat(GAUGE_WIDTH) + RESET + " " : "";
195
+ return currentTier === "full"
196
+ ? DIM + "░".repeat(GAUGE_WIDTH) + RESET + " "
197
+ : "";
130
198
  }
131
199
  // Gemini ∞% 전용: 무한 쿼터이므로 dim 회색 바
132
200
  export function tierInfBar(currentTier) {
133
- return currentTier === "full" ? DIM + "█".repeat(GAUGE_WIDTH) + RESET + " " : "";
201
+ return currentTier === "full"
202
+ ? DIM + "█".repeat(GAUGE_WIDTH) + RESET + " "
203
+ : "";
134
204
  }
135
205
 
136
206
  // 테스트 지원: 캐시 초기화
package/hud/utils.mjs CHANGED
@@ -1,11 +1,14 @@
1
1
  // ============================================================================
2
2
  // 유틸리티 함수
3
3
  // ============================================================================
4
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
5
- import { dirname } from "node:path";
4
+
6
5
  import { createHash } from "node:crypto";
6
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
7
+ import { dirname } from "node:path";
7
8
  import {
8
- PERCENT_CELL_WIDTH, TIME_CELL_INNER_WIDTH, SV_CELL_WIDTH,
9
+ PERCENT_CELL_WIDTH,
10
+ SV_CELL_WIDTH,
11
+ TIME_CELL_INNER_WIDTH,
9
12
  } from "./constants.mjs";
10
13
 
11
14
  export async function readStdinJson() {
@@ -20,8 +23,15 @@ export async function readStdinJson() {
20
23
  process.stdin.on("end", () => {
21
24
  clearTimeout(timeout);
22
25
  const raw = chunks.join("").trim();
23
- if (!raw) { resolve({}); return; }
24
- try { resolve(JSON.parse(raw)); } catch { resolve({}); }
26
+ if (!raw) {
27
+ resolve({});
28
+ return;
29
+ }
30
+ try {
31
+ resolve(JSON.parse(raw));
32
+ } catch {
33
+ resolve({});
34
+ }
25
35
  });
26
36
  process.stdin.on("error", () => {
27
37
  clearTimeout(timeout);
@@ -33,14 +43,20 @@ export async function readStdinJson() {
33
43
 
34
44
  export function readJson(filePath, fallback) {
35
45
  if (!existsSync(filePath)) return fallback;
36
- try { return JSON.parse(readFileSync(filePath, "utf-8")); } catch { return fallback; }
46
+ try {
47
+ return JSON.parse(readFileSync(filePath, "utf-8"));
48
+ } catch {
49
+ return fallback;
50
+ }
37
51
  }
38
52
 
39
53
  export function writeJsonSafe(filePath, data) {
40
54
  try {
41
55
  mkdirSync(dirname(filePath), { recursive: true });
42
56
  writeFileSync(filePath, JSON.stringify(data), { mode: 0o600 });
43
- } catch { /* 쓰기 실패 무시 */ }
57
+ } catch {
58
+ /* 쓰기 실패 무시 */
59
+ }
44
60
  }
45
61
 
46
62
  // .omc/ → .claude/cache/ 마이그레이션: 새 경로 우선, 없으면 레거시 읽고 복사
@@ -48,7 +64,10 @@ export function readJsonMigrate(newPath, legacyPath, fallback) {
48
64
  const data = readJson(newPath, null);
49
65
  if (data != null) return data;
50
66
  const legacy = readJson(legacyPath, null);
51
- if (legacy != null) { writeJsonSafe(newPath, legacy); return legacy; }
67
+ if (legacy != null) {
68
+ writeJsonSafe(newPath, legacy);
69
+ return legacy;
70
+ }
52
71
  return fallback;
53
72
  }
54
73
 
@@ -109,7 +128,10 @@ export function truncateAnsi(text, maxWidth) {
109
128
  }
110
129
 
111
130
  export function makeHash(text) {
112
- return createHash("sha256").update(String(text || ""), "utf8").digest("hex").slice(0, 16);
131
+ return createHash("sha256")
132
+ .update(String(text || ""), "utf8")
133
+ .digest("hex")
134
+ .slice(0, 16);
113
135
  }
114
136
 
115
137
  export function clampPercent(value) {
@@ -177,29 +199,34 @@ export function formatTokenCount(n) {
177
199
 
178
200
  export function getContextPercent(stdin) {
179
201
  const nativePercent = stdin?.context_window?.used_percentage;
180
- if (typeof nativePercent === "number" && Number.isFinite(nativePercent)) return clampPercent(nativePercent);
202
+ if (typeof nativePercent === "number" && Number.isFinite(nativePercent))
203
+ return clampPercent(nativePercent);
181
204
  const usage = stdin?.context_window?.current_usage || {};
182
- const totalTokens = Number(usage.input_tokens || 0)
183
- + Number(usage.cache_creation_input_tokens || 0)
184
- + Number(usage.cache_read_input_tokens || 0);
205
+ const totalTokens =
206
+ Number(usage.input_tokens || 0) +
207
+ Number(usage.cache_creation_input_tokens || 0) +
208
+ Number(usage.cache_read_input_tokens || 0);
185
209
  const capacity = Number(stdin?.context_window?.context_window_size || 0);
186
210
  if (!capacity || capacity <= 0) return 0;
187
211
  return clampPercent((totalTokens / capacity) * 100);
188
212
  }
189
213
 
190
214
  // 과거 리셋 시간 → 다음 주기로 순환하여 미래 시점 반환
215
+ // elapsed가 cycleMs의 정수배일 때 ceil은 target=now를 반환해 diff=0이 되므로
216
+ // floor+1로 항상 다음 사이클을 가리키도록 한다.
191
217
  export function advanceToNextCycle(epochMs, cycleMs) {
192
218
  const now = Date.now();
193
219
  if (epochMs >= now || !cycleMs) return epochMs;
194
220
  const elapsed = now - epochMs;
195
- return epochMs + Math.ceil(elapsed / cycleMs) * cycleMs;
221
+ return epochMs + (Math.floor(elapsed / cycleMs) + 1) * cycleMs;
196
222
  }
197
223
 
198
224
  function parseResetDate(isoOrUnix) {
199
225
  if (!isoOrUnix) return null;
200
- const date = typeof isoOrUnix === "string"
201
- ? new Date(isoOrUnix)
202
- : new Date(isoOrUnix * 1000);
226
+ const date =
227
+ typeof isoOrUnix === "string"
228
+ ? new Date(isoOrUnix)
229
+ : new Date(isoOrUnix * 1000);
203
230
  return Number.isNaN(date.getTime()) ? null : date;
204
231
  }
205
232
 
@@ -261,9 +288,13 @@ export function decodeJwtEmail(idToken) {
261
288
  let payload = parts[1].replace(/-/g, "+").replace(/_/g, "/");
262
289
  while (payload.length % 4) payload += "=";
263
290
  try {
264
- const decoded = JSON.parse(Buffer.from(payload, "base64").toString("utf-8"));
291
+ const decoded = JSON.parse(
292
+ Buffer.from(payload, "base64").toString("utf-8"),
293
+ );
265
294
  return decoded.email || null;
266
- } catch { return null; }
295
+ } catch {
296
+ return null;
297
+ }
267
298
  }
268
299
 
269
300
  // HTTPS POST (타임아웃 포함) — https 모듈은 호출자가 주입
@@ -272,26 +303,35 @@ export function createHttpsPost(https, timeoutMs) {
272
303
  return new Promise((resolve) => {
273
304
  const urlObj = new URL(url);
274
305
  const data = JSON.stringify(body);
275
- const req = https.request({
276
- hostname: urlObj.hostname,
277
- path: urlObj.pathname + urlObj.search,
278
- method: "POST",
279
- headers: {
280
- "Content-Type": "application/json",
281
- "Authorization": `Bearer ${accessToken}`,
282
- "Content-Length": Buffer.byteLength(data),
306
+ const req = https.request(
307
+ {
308
+ hostname: urlObj.hostname,
309
+ path: urlObj.pathname + urlObj.search,
310
+ method: "POST",
311
+ headers: {
312
+ "Content-Type": "application/json",
313
+ Authorization: `Bearer ${accessToken}`,
314
+ "Content-Length": Buffer.byteLength(data),
315
+ },
316
+ timeout: timeoutMs,
283
317
  },
284
- timeout: timeoutMs,
285
- }, (res) => {
286
- const chunks = [];
287
- res.on("data", (c) => chunks.push(c));
288
- res.on("end", () => {
289
- try { resolve(JSON.parse(Buffer.concat(chunks).toString())); }
290
- catch { resolve(null); }
291
- });
292
- });
318
+ (res) => {
319
+ const chunks = [];
320
+ res.on("data", (c) => chunks.push(c));
321
+ res.on("end", () => {
322
+ try {
323
+ resolve(JSON.parse(Buffer.concat(chunks).toString()));
324
+ } catch {
325
+ resolve(null);
326
+ }
327
+ });
328
+ },
329
+ );
293
330
  req.on("error", () => resolve(null));
294
- req.on("timeout", () => { req.destroy(); resolve(null); });
331
+ req.on("timeout", () => {
332
+ req.destroy();
333
+ resolve(null);
334
+ });
295
335
  req.write(data);
296
336
  req.end();
297
337
  });
@@ -301,8 +341,10 @@ export function createHttpsPost(https, timeoutMs) {
301
341
  // sv 퍼센트 포맷 (1000+ → k 표기, 5자 고정폭)
302
342
  export function formatSvPct(value) {
303
343
  if (value == null) return "--%".padStart(SV_CELL_WIDTH);
304
- if (value >= 10000) return `${Math.round(value / 1000)}k%`.padStart(SV_CELL_WIDTH);
305
- if (value >= 1000) return `${(value / 1000).toFixed(1)}k%`.padStart(SV_CELL_WIDTH);
344
+ if (value >= 10000)
345
+ return `${Math.round(value / 1000)}k%`.padStart(SV_CELL_WIDTH);
346
+ if (value >= 1000)
347
+ return `${(value / 1000).toFixed(1)}k%`.padStart(SV_CELL_WIDTH);
306
348
  return `${value}%`.padStart(SV_CELL_WIDTH);
307
349
  }
308
350
 
package/mesh/index.mjs ADDED
@@ -0,0 +1,69 @@
1
+ import { readdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ export { createMeshBudget } from "./mesh-budget.mjs";
5
+ export { createHeartbeatMonitor } from "./mesh-heartbeat.mjs";
6
+ export {
7
+ createMessage,
8
+ deserialize,
9
+ MSG_TYPES,
10
+ serialize,
11
+ validate,
12
+ } from "./mesh-protocol.mjs";
13
+ export { createMessageQueue } from "./mesh-queue.mjs";
14
+ export { createRegistry } from "./mesh-registry.mjs";
15
+ export { routeMessage, routeOrDeadLetter } from "./mesh-router.mjs";
16
+
17
+ /**
18
+ * Loads skills assigned to a specific agent from a skills directory.
19
+ * Reuses the same directory-scan approach as generateSkillDocs().
20
+ *
21
+ * @param {string} agentId - The agent identifier
22
+ * @param {string} skillsDir - Path to the skills directory
23
+ * @returns {Promise<string[]>} Array of skill names available to this agent
24
+ */
25
+ export async function loadSkillsForAgent(agentId, skillsDir) {
26
+ if (!agentId || typeof agentId !== "string") {
27
+ throw new TypeError("agentId must be a non-empty string");
28
+ }
29
+ if (!skillsDir || typeof skillsDir !== "string") {
30
+ throw new TypeError("skillsDir must be a non-empty string");
31
+ }
32
+
33
+ let entries;
34
+ try {
35
+ entries = readdirSync(skillsDir, { withFileTypes: true });
36
+ } catch {
37
+ return [];
38
+ }
39
+
40
+ const skills = [];
41
+ for (const entry of entries) {
42
+ if (!entry.isDirectory()) continue;
43
+ const skillName = entry.name;
44
+ const skillPath = join(skillsDir, skillName, "SKILL.md");
45
+ let skillContent = null;
46
+ try {
47
+ const { readFileSync } = await import("node:fs");
48
+ skillContent = readFileSync(skillPath, "utf8");
49
+ } catch {
50
+ // Skill has no SKILL.md — include it anyway
51
+ }
52
+
53
+ // If SKILL.md mentions the agentId or no agent restriction, include it
54
+ const isRestricted = skillContent
55
+ ? /^agents?\s*:/im.test(skillContent)
56
+ : false;
57
+
58
+ if (!isRestricted) {
59
+ skills.push(skillName);
60
+ continue;
61
+ }
62
+
63
+ if (skillContent?.includes(agentId)) {
64
+ skills.push(skillName);
65
+ }
66
+ }
67
+
68
+ return skills;
69
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Warning level thresholds mirroring context-monitor.mjs classifyContextThreshold().
3
+ * Reproduced here to avoid cross-module dependency.
4
+ */
5
+ const WARNING_LEVELS = Object.freeze({
6
+ critical: 90,
7
+ warn: 80,
8
+ info: 60,
9
+ ok: 0,
10
+ });
11
+
12
+ /**
13
+ * Clamps a percentage value to [0, 100].
14
+ * @param {number} value
15
+ * @returns {number}
16
+ */
17
+ function clampPercent(value) {
18
+ const n = Number(value);
19
+ if (!Number.isFinite(n)) return 0;
20
+ return Math.min(100, Math.max(0, n));
21
+ }
22
+
23
+ /**
24
+ * Classifies a usage percentage into a warning level.
25
+ * Mirrors context-monitor.mjs classifyContextThreshold().
26
+ * @param {number} percent
27
+ * @returns {{ level: string, message: string }}
28
+ */
29
+ function classifyLevel(percent) {
30
+ const p = clampPercent(percent);
31
+ if (p >= WARNING_LEVELS.critical)
32
+ return { level: "critical", message: "에이전트 분할 또는 세션 교체 권장" };
33
+ if (p >= WARNING_LEVELS.warn) return { level: "warn", message: "압축 권장" };
34
+ if (p >= WARNING_LEVELS.info)
35
+ return { level: "info", message: "컨텍스트 절반 이상 사용" };
36
+ return { level: "ok", message: "" };
37
+ }
38
+
39
+ /**
40
+ * Creates a per-agent token budget manager.
41
+ * @returns {object} Budget API
42
+ */
43
+ export function createMeshBudget() {
44
+ // Map<agentId, { allocated: number, consumed: number }>
45
+ const budgets = new Map();
46
+
47
+ /**
48
+ * Allocates a token budget to an agent.
49
+ * @param {string} agentId
50
+ * @param {number} tokenLimit
51
+ */
52
+ function allocate(agentId, tokenLimit) {
53
+ if (!agentId || typeof agentId !== "string") {
54
+ throw new TypeError("agentId must be a non-empty string");
55
+ }
56
+ const limit = Math.max(0, Math.round(Number(tokenLimit) || 0));
57
+ const existing = budgets.get(agentId);
58
+ budgets.set(agentId, {
59
+ allocated: limit,
60
+ consumed: existing?.consumed ?? 0,
61
+ });
62
+ }
63
+
64
+ /**
65
+ * Records token consumption for an agent.
66
+ * @param {string} agentId
67
+ * @param {number} tokens
68
+ * @returns {{ remaining: number, percent: number, level: string }}
69
+ */
70
+ function consume(agentId, tokens) {
71
+ const budget = budgets.get(agentId);
72
+ if (!budget) {
73
+ throw new Error(`No budget allocated for agent: ${agentId}`);
74
+ }
75
+ const amount = Math.max(0, Math.round(Number(tokens) || 0));
76
+ const updated = {
77
+ allocated: budget.allocated,
78
+ consumed: budget.consumed + amount,
79
+ };
80
+ budgets.set(agentId, updated);
81
+
82
+ const remaining = Math.max(0, updated.allocated - updated.consumed);
83
+ const percent =
84
+ updated.allocated > 0
85
+ ? clampPercent((updated.consumed / updated.allocated) * 100)
86
+ : 100;
87
+ const { level } = classifyLevel(percent);
88
+ return { remaining, percent, level };
89
+ }
90
+
91
+ /**
92
+ * Returns the budget status for an agent.
93
+ * @param {string} agentId
94
+ * @returns {{ allocated: number, consumed: number, remaining: number, level: string }}
95
+ */
96
+ function getStatus(agentId) {
97
+ const budget = budgets.get(agentId);
98
+ if (!budget) {
99
+ return { allocated: 0, consumed: 0, remaining: 0, level: "ok" };
100
+ }
101
+ const remaining = Math.max(0, budget.allocated - budget.consumed);
102
+ const percent =
103
+ budget.allocated > 0
104
+ ? clampPercent((budget.consumed / budget.allocated) * 100)
105
+ : 0;
106
+ const { level } = classifyLevel(percent);
107
+ return {
108
+ allocated: budget.allocated,
109
+ consumed: budget.consumed,
110
+ remaining,
111
+ level,
112
+ };
113
+ }
114
+
115
+ /**
116
+ * Resets consumed tokens for all agents (keeps allocations).
117
+ */
118
+ function resetAll() {
119
+ for (const [agentId, budget] of budgets) {
120
+ budgets.set(agentId, { allocated: budget.allocated, consumed: 0 });
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Returns a snapshot of all current allocations.
126
+ * @returns {Map<string, { allocated: number, consumed: number }>}
127
+ */
128
+ function listAllocations() {
129
+ const snap = new Map();
130
+ for (const [id, b] of budgets) {
131
+ snap.set(id, Object.freeze({ ...b }));
132
+ }
133
+ return snap;
134
+ }
135
+
136
+ return { allocate, consume, getStatus, resetAll, listAllocations };
137
+ }