@triflux/core 10.1.0 → 10.26.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/hooks/agent-route-guard.mjs +14 -10
- package/hooks/cross-review-tracker.mjs +21 -8
- package/hooks/error-context.mjs +20 -8
- package/hooks/hook-adaptive-collector.mjs +18 -16
- package/hooks/hook-manager.mjs +93 -32
- package/hooks/hook-orchestrator.mjs +267 -35
- package/hooks/hook-registry.json +343 -256
- package/hooks/hooks.json +49 -1
- package/hooks/keyword-rules.json +191 -31
- package/hooks/lib/resolve-root.mjs +1 -1
- package/hooks/mcp-config-watcher.mjs +6 -2
- package/hooks/permission-safe-allow.mjs +233 -0
- package/hooks/pipeline-stop.mjs +369 -22
- package/hooks/post-tool-tips.mjs +306 -0
- package/hooks/pre-compact-snapshot.mjs +181 -0
- package/hooks/safety-guard.mjs +262 -34
- package/hooks/session-end-cleanup.mjs +345 -0
- package/hooks/session-start-fast.mjs +214 -0
- package/hooks/subagent-tracker.mjs +267 -0
- package/hooks/subagent-verifier.mjs +9 -4
- package/hub/account-broker.mjs +1036 -66
- package/hub/adaptive-diagnostic.mjs +75 -48
- package/hub/adaptive-inject.mjs +96 -58
- package/hub/adaptive-memory.mjs +156 -42
- package/hub/adaptive.mjs +60 -31
- package/hub/assign-callbacks.mjs +163 -126
- package/hub/bridge.mjs +1051 -290
- package/hub/cli-adapter-base.mjs +321 -97
- package/hub/codex-adapter.mjs +179 -93
- package/hub/codex-compat.mjs +4 -4
- package/hub/codex-preflight.mjs +113 -37
- package/hub/delegator/contracts.mjs +22 -21
- package/hub/delegator/index.mjs +14 -14
- package/hub/delegator/schema/delegator-tools.schema.json +1 -1
- package/hub/delegator/service.mjs +93 -64
- package/hub/delegator/tool-definitions.mjs +35 -35
- package/hub/dynamic-routing-engine.mjs +511 -0
- package/hub/fullcycle.mjs +34 -18
- package/hub/gemini-adapter.mjs +82 -108
- package/hub/hitl.mjs +202 -143
- package/hub/intent.mjs +167 -38
- package/hub/lib/bash-path.mjs +73 -0
- package/hub/lib/cache-guard.mjs +43 -17
- package/hub/lib/env-detect.mjs +145 -0
- package/hub/lib/hosts-compat.mjs +319 -0
- package/hub/lib/mcp-response-cache.mjs +239 -0
- package/hub/lib/memory-store.mjs +285 -111
- package/hub/lib/path-utils.mjs +165 -0
- package/hub/lib/phase-manager.mjs +493 -0
- package/hub/lib/process-utils.mjs +797 -104
- package/hub/lib/prompt-tmp.mjs +33 -0
- package/hub/lib/risk-tier.mjs +53 -0
- package/hub/lib/spawn-trace.mjs +561 -0
- package/hub/lib/ssh-command.mjs +44 -24
- package/hub/lib/ssh-retry.mjs +5 -1
- package/hub/lib/staged-file-detect.mjs +43 -0
- package/hub/lib/state-snapshot.mjs +293 -0
- package/hub/lib/tfx-route-args.mjs +309 -0
- package/hub/lib/timeline-adapter.mjs +190 -0
- package/hub/lib/trace-recorder.mjs +153 -0
- package/hub/lib/uuidv7.mjs +4 -3
- package/hub/memory-doctor.mjs +266 -106
- package/hub/middleware/quota-middleware.mjs +70 -0
- package/hub/middleware/request-logger.mjs +61 -34
- package/hub/paths.mjs +9 -9
- package/hub/pipeline/gates/confidence.mjs +34 -15
- package/hub/pipeline/gates/consensus.mjs +27 -15
- package/hub/pipeline/gates/index.mjs +7 -3
- package/hub/pipeline/gates/selfcheck.mjs +57 -19
- package/hub/pipeline/index.mjs +77 -42
- package/hub/pipeline/state.mjs +10 -10
- package/hub/pipeline/transitions.mjs +40 -23
- package/hub/platform.mjs +65 -48
- package/hub/quality/deslop.mjs +70 -49
- package/hub/reflexion.mjs +87 -13
- package/hub/research.mjs +33 -25
- package/hub/router.mjs +941 -791
- package/hub/routing/complexity.mjs +132 -29
- package/hub/routing/index.mjs +17 -12
- package/hub/routing/q-learning.mjs +78 -30
- package/hub/routing-snapshot.mjs +261 -0
- package/hub/session-fingerprint.mjs +127 -61
- package/hub/state.mjs +86 -44
- package/hub/team/claude-daemon-control.mjs +1130 -0
- package/hub/team/retry-state-machine.mjs +336 -0
- package/hub/token-mode.mjs +72 -71
- package/hub/workers/worker-utils.mjs +23 -17
- package/hud/colors.mjs +27 -9
- package/hud/constants.mjs +190 -27
- package/hud/context-monitor.mjs +133 -44
- package/hud/hud-qos-status.mjs +157 -50
- package/hud/mission-board.mjs +56 -0
- package/hud/providers/claude.mjs +398 -114
- package/hud/providers/codex.mjs +96 -21
- package/hud/providers/gemini.mjs +210 -52
- package/hud/renderers.mjs +440 -127
- package/hud/terminal.mjs +101 -31
- package/hud/utils.mjs +81 -39
- package/mesh/index.mjs +69 -0
- package/mesh/mesh-budget.mjs +137 -0
- package/mesh/mesh-heartbeat.mjs +100 -0
- package/mesh/mesh-protocol.mjs +96 -0
- package/mesh/mesh-queue.mjs +163 -0
- package/mesh/mesh-registry.mjs +78 -0
- package/mesh/mesh-router.mjs +77 -0
- package/package.json +5 -1
- package/scripts/lib/agent-json.mjs +27 -0
- package/scripts/lib/async.mjs +174 -0
- package/scripts/lib/claudemd-scanner.mjs +6 -1
- package/scripts/lib/cli-agy.mjs +62 -0
- package/scripts/lib/cli-claude.mjs +78 -0
- package/scripts/lib/cli-codex.mjs +199 -0
- package/scripts/lib/cli-gemini.mjs +37 -0
- package/scripts/lib/codex-recovery.sh +50 -0
- package/scripts/lib/context.mjs +3 -3
- package/scripts/lib/cross-review-utils.mjs +13 -5
- package/scripts/lib/doctor-env-checks.mjs +121 -0
- package/scripts/lib/dynamic-route-cli.mjs +107 -0
- package/scripts/lib/env-probe.mjs +58 -30
- package/scripts/lib/env.mjs +14 -0
- package/scripts/lib/gemini-profiles.mjs +44 -10
- package/scripts/lib/handoff.mjs +33 -17
- package/scripts/lib/hook-utils.mjs +8 -6
- package/scripts/lib/hub.mjs +48 -0
- package/scripts/lib/keyword-rules.mjs +49 -20
- package/scripts/lib/logger.mjs +24 -24
- package/scripts/lib/mcp-filter.mjs +391 -247
- package/scripts/lib/mcp-gateway-health-check.mjs +159 -0
- package/scripts/lib/mcp-gateway-servers.mjs +70 -0
- package/scripts/lib/mcp-gateway-wrapper-check.mjs +35 -0
- package/scripts/lib/mcp-guard-engine.mjs +1281 -115
- package/scripts/lib/mcp-health.mjs +773 -0
- package/scripts/lib/mcp-manifest.mjs +24 -14
- package/scripts/lib/mcp-server-catalog.mjs +355 -118
- package/scripts/lib/pid.mjs +63 -0
- package/scripts/lib/process-utils.mjs +26 -0
- package/scripts/lib/psmux-info.mjs +117 -12
- package/scripts/lib/quota.mjs +47 -0
- package/scripts/lib/remote-spawn-transfer.mjs +44 -14
- package/scripts/lib/skill-state.mjs +220 -0
- package/scripts/lib/skill-template.mjs +32 -7
- package/scripts/lib/team.mjs +75 -0
- package/scripts/lib/timeout.mjs +66 -0
- package/scripts/lib/tmp.mjs +26 -0
- 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 {
|
|
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) {
|
|
14
|
-
|
|
15
|
-
|
|
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", {
|
|
19
|
-
|
|
20
|
-
|
|
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(
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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) {
|
|
35
|
-
|
|
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) {
|
|
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", {
|
|
41
|
-
|
|
42
|
-
|
|
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(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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")
|
|
64
|
-
|
|
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 =
|
|
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))
|
|
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"
|
|
190
|
+
return currentTier === "full"
|
|
191
|
+
? coloredBar(percent, GAUGE_WIDTH, baseColor) + " "
|
|
192
|
+
: "";
|
|
127
193
|
}
|
|
128
194
|
export function tierDimBar(currentTier) {
|
|
129
|
-
return currentTier === "full"
|
|
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"
|
|
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
|
-
|
|
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,
|
|
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) {
|
|
24
|
-
|
|
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 {
|
|
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) {
|
|
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")
|
|
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))
|
|
202
|
+
if (typeof nativePercent === "number" && Number.isFinite(nativePercent))
|
|
203
|
+
return clampPercent(nativePercent);
|
|
181
204
|
const usage = stdin?.context_window?.current_usage || {};
|
|
182
|
-
const totalTokens =
|
|
183
|
-
|
|
184
|
-
|
|
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.
|
|
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 =
|
|
201
|
-
|
|
202
|
-
|
|
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(
|
|
291
|
+
const decoded = JSON.parse(
|
|
292
|
+
Buffer.from(payload, "base64").toString("utf-8"),
|
|
293
|
+
);
|
|
265
294
|
return decoded.email || null;
|
|
266
|
-
} catch {
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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", () => {
|
|
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)
|
|
305
|
-
|
|
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
|
+
}
|