triflux 2.4.7 → 2.5.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.
- package/README.ko.md +9 -7
- package/README.md +9 -7
- package/bin/tfx-doctor.mjs +1 -1
- package/bin/tfx-setup.mjs +1 -1
- package/bin/triflux.mjs +1 -1
- package/hud/hud-qos-status.mjs +201 -102
- package/package.json +1 -1
- package/scripts/cli-route.sh +236 -9
- package/scripts/notion-read.mjs +103 -48
package/README.ko.md
CHANGED
|
@@ -187,7 +187,8 @@ tfx doctor
|
|
|
187
187
|
- **Codex CLI** (선택): `npm install -g @openai/codex`
|
|
188
188
|
- **Gemini CLI** (선택): `npm install -g @google/gemini-cli`
|
|
189
189
|
|
|
190
|
-
>
|
|
190
|
+
> [!TIP]
|
|
191
|
+
> **triflux는 100% 독립적으로 동작합니다.** 기능을 수행하기 위해 [oh-my-claudecode (OMC)](https://github.com/nicepkg/oh-my-claudecode)가 필요하지 않습니다. OMC가 설치된 경우에만 이를 감지하여 선택적인 통합 기능을 제공합니다. Codex/Gemini 없이도 자동으로 Claude 네이티브 에이전트로 fallback되어 동작합니다.
|
|
191
192
|
|
|
192
193
|
### 설치 후
|
|
193
194
|
|
|
@@ -224,14 +225,15 @@ Claude Code 상태줄에서 실시간 모니터링:
|
|
|
224
225
|
</details>
|
|
225
226
|
|
|
226
227
|
<details>
|
|
227
|
-
<summary><strong
|
|
228
|
+
<summary><strong>선택 사항: oh-my-claudecode (OMC) 통합</strong></summary>
|
|
228
229
|
|
|
229
|
-
triflux는
|
|
230
|
+
triflux는 **100% 독립적으로 동작**하며 기능을 수행하기 위해 외부 도구에 의존하지 않습니다. 다만, [oh-my-claudecode](https://github.com/nicepkg/oh-my-claudecode) 생태계를 사용하는 사용자를 위해 선택적 호환성을 제공합니다:
|
|
230
231
|
|
|
231
|
-
- OMC
|
|
232
|
-
-
|
|
233
|
-
-
|
|
234
|
-
- OMC
|
|
232
|
+
- **완전한 독립성**: OMC나 다른 래퍼 없이 단독으로 100% 동작합니다.
|
|
233
|
+
- **캐시 호환성**: OMC 캐시 경로(예: `~/.omc/state/`)가 존재할 경우 이를 감지하고 배려하지만, 기본적으로 자체 격리된 상태를 유지합니다.
|
|
234
|
+
- **심리스한 플러그인**: OMC가 설치된 경우 OMC 플러그인 시스템을 통해 스킬이 자동 등록됩니다.
|
|
235
|
+
- **확장된 HUD**: OMC가 감지되면 HUD가 자동으로 OMC 상태줄을 확장하여 표시합니다.
|
|
236
|
+
- **모드 지원**: OMC autopilot, ralph, team, ultrawork 모드와 완벽히 호환됩니다.
|
|
235
237
|
|
|
236
238
|
</details>
|
|
237
239
|
|
package/README.md
CHANGED
|
@@ -187,7 +187,8 @@ User: "/tfx-auto refactor auth + improve UI + add tests"
|
|
|
187
187
|
- **Codex CLI** (optional): `npm install -g @openai/codex`
|
|
188
188
|
- **Gemini CLI** (optional): `npm install -g @google/gemini-cli`
|
|
189
189
|
|
|
190
|
-
>
|
|
190
|
+
> [!TIP]
|
|
191
|
+
> **triflux is 100% standalone.** It does not require [oh-my-claudecode (OMC)](https://github.com/nicepkg/oh-my-claudecode) to function. It will automatically detect and provide optional integration only if OMC is present. Without Codex or Gemini, triflux falls back to Claude native agents.
|
|
191
192
|
|
|
192
193
|
### Post-install
|
|
193
194
|
|
|
@@ -224,14 +225,15 @@ Configured automatically via `tfx setup`.
|
|
|
224
225
|
</details>
|
|
225
226
|
|
|
226
227
|
<details>
|
|
227
|
-
<summary><strong>oh-my-claudecode Integration</strong></summary>
|
|
228
|
+
<summary><strong>Optional: oh-my-claudecode (OMC) Integration</strong></summary>
|
|
228
229
|
|
|
229
|
-
triflux
|
|
230
|
+
triflux is **100% independent** and does not require any external tools to function. However, it provides optional compatibility with [oh-my-claudecode](https://github.com/nicepkg/oh-my-claudecode) for users who prefer that ecosystem:
|
|
230
231
|
|
|
231
|
-
-
|
|
232
|
-
-
|
|
233
|
-
-
|
|
234
|
-
-
|
|
232
|
+
- **Full Independence**: Works standalone without OMC or any other wrappers.
|
|
233
|
+
- **Cache Compatibility**: Detects and respects OMC cache paths (e.g., `~/.omc/state/`) if present, but maintains its own isolated state by default.
|
|
234
|
+
- **Seamless Plugin**: Skills auto-registered via OMC's plugin system if OMC is installed.
|
|
235
|
+
- **Extended HUD**: HUD automatically extends OMC's status line when detected.
|
|
236
|
+
- **Mode Support**: Compatible with OMC autopilot, ralph, team, and ultrawork modes.
|
|
235
237
|
|
|
236
238
|
</details>
|
|
237
239
|
|
package/bin/tfx-doctor.mjs
CHANGED
package/bin/tfx-setup.mjs
CHANGED
package/bin/triflux.mjs
CHANGED
package/hud/hud-qos-status.mjs
CHANGED
|
@@ -121,6 +121,9 @@ function getClaudeUsageStaleMs() {
|
|
|
121
121
|
const CLAUDE_USAGE_429_BACKOFF_MS = 10 * 60 * 1000; // 429 에러 시 10분 backoff
|
|
122
122
|
const CLAUDE_USAGE_ERROR_BACKOFF_MS = 3 * 60 * 1000; // 기타 에러 시 3분 backoff
|
|
123
123
|
const CLAUDE_API_TIMEOUT_MS = 10_000;
|
|
124
|
+
const FIVE_HOUR_MS = 5 * 60 * 60 * 1000;
|
|
125
|
+
const SEVEN_DAY_MS = 7 * 24 * 60 * 60 * 1000;
|
|
126
|
+
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
|
|
124
127
|
const DEFAULT_OAUTH_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
|
|
125
128
|
const CODEX_AUTH_PATH = join(homedir(), ".codex", "auth.json");
|
|
126
129
|
const CODEX_QUOTA_CACHE_PATH = join(homedir(), ".claude", "cache", "codex-rate-limits-cache.json");
|
|
@@ -152,6 +155,14 @@ function getGeminiModelLabel(model) {
|
|
|
152
155
|
if (model.includes("flash")) return "[Flash3]";
|
|
153
156
|
return "";
|
|
154
157
|
}
|
|
158
|
+
// rows 임계값 상수 (selectTier 에서 tier 결정에 사용)
|
|
159
|
+
const ROWS_BUDGET_FULL = 40;
|
|
160
|
+
const ROWS_BUDGET_LARGE = 35;
|
|
161
|
+
const ROWS_BUDGET_MEDIUM = 28;
|
|
162
|
+
const ROWS_BUDGET_SMALL = 22;
|
|
163
|
+
// Codex rate_limits에서 최소 수집할 버킷 수
|
|
164
|
+
const CODEX_MIN_BUCKETS = 2;
|
|
165
|
+
|
|
155
166
|
const GEMINI_RPM_WINDOW_MS = 60 * 1000; // 60초 슬라이딩 윈도우
|
|
156
167
|
const GEMINI_QUOTA_STALE_MS = 5 * 60 * 1000; // 5분
|
|
157
168
|
const GEMINI_SESSION_STALE_MS = 15 * 1000; // 15초
|
|
@@ -291,10 +302,10 @@ function selectTier(stdin, claudeUsage = null) {
|
|
|
291
302
|
const cols = getTerminalColumns() || 120;
|
|
292
303
|
|
|
293
304
|
let budget;
|
|
294
|
-
if (rows >=
|
|
295
|
-
else if (rows >=
|
|
296
|
-
else if (rows >=
|
|
297
|
-
else if (rows >=
|
|
305
|
+
if (rows >= ROWS_BUDGET_FULL) budget = 6;
|
|
306
|
+
else if (rows >= ROWS_BUDGET_LARGE) budget = 5;
|
|
307
|
+
else if (rows >= ROWS_BUDGET_MEDIUM) budget = 4;
|
|
308
|
+
else if (rows >= ROWS_BUDGET_SMALL) budget = 3;
|
|
298
309
|
else if (rows > 0) budget = 2;
|
|
299
310
|
else budget = 5; // rows 감지 불가 → 넉넉하게
|
|
300
311
|
|
|
@@ -437,25 +448,21 @@ function renderAlignedRows(rows) {
|
|
|
437
448
|
function getMicroLine(stdin, claudeUsage, codexBuckets, geminiSession, geminiBucket, combinedSvPct) {
|
|
438
449
|
const ctx = getContextPercent(stdin);
|
|
439
450
|
|
|
440
|
-
// Claude 5h/1w (
|
|
441
|
-
const cF = claudeUsage
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
? (isResetPast(claudeUsage.weeklyResetsAt) ? 0 : clampPercent(claudeUsage.weeklyPercent ?? 0))
|
|
446
|
-
: null;
|
|
447
|
-
const cVal = cF != null
|
|
448
|
-
? `${colorByProvider(cF, `${cF}`, claudeOrange)}${dim("/")}${colorByProvider(cW, `${cW}`, claudeOrange)}`
|
|
451
|
+
// Claude 5h/1w (캐시된 값 그대로 표시, 시간은 advanceToNextCycle이 처리)
|
|
452
|
+
const cF = claudeUsage?.fiveHourPercent != null ? clampPercent(claudeUsage.fiveHourPercent) : null;
|
|
453
|
+
const cW = claudeUsage?.weeklyPercent != null ? clampPercent(claudeUsage.weeklyPercent) : null;
|
|
454
|
+
const cVal = claudeUsage != null
|
|
455
|
+
? `${cF != null ? colorByProvider(cF, `${cF}`, claudeOrange) : dim("--")}${dim("/")}${cW != null ? colorByProvider(cW, `${cW}`, claudeOrange) : dim("--")}`
|
|
449
456
|
: dim("--/--");
|
|
450
457
|
|
|
451
|
-
// Codex 5h/1w
|
|
458
|
+
// Codex 5h/1w (캐시된 값 그대로 표시)
|
|
452
459
|
let xVal = dim("--/--");
|
|
453
460
|
if (codexBuckets) {
|
|
454
461
|
const mb = codexBuckets.codex || codexBuckets[Object.keys(codexBuckets)[0]];
|
|
455
462
|
if (mb) {
|
|
456
|
-
const xF =
|
|
457
|
-
const xW =
|
|
458
|
-
xVal = `${colorByProvider(xF, `${xF}`, codexWhite)}${dim("/")}${colorByProvider(xW, `${xW}`, codexWhite)}`;
|
|
463
|
+
const xF = mb.primary?.used_percent != null ? clampPercent(mb.primary.used_percent) : null;
|
|
464
|
+
const xW = mb.secondary?.used_percent != null ? clampPercent(mb.secondary.used_percent) : null;
|
|
465
|
+
xVal = `${xF != null ? colorByProvider(xF, `${xF}`, codexWhite) : dim("--")}${dim("/")}${xW != null ? colorByProvider(xW, `${xW}`, codexWhite) : dim("--")}`;
|
|
459
466
|
}
|
|
460
467
|
}
|
|
461
468
|
|
|
@@ -667,31 +674,15 @@ function parseClaudeUsageResponse(response) {
|
|
|
667
674
|
// stale 캐시의 과거 resetsAt → 다음 주기로 순환 추정 (null 대신 다음 reset 시간 계산)
|
|
668
675
|
function stripStaleResets(data) {
|
|
669
676
|
if (!data) return data;
|
|
670
|
-
const now = Date.now();
|
|
671
677
|
const copy = { ...data };
|
|
672
|
-
|
|
673
|
-
// 5시간 주기: 과거 reset → 5시간씩 전진하여 미래 시점 추정
|
|
674
678
|
if (copy.fiveHourResetsAt) {
|
|
675
|
-
|
|
676
|
-
if (t
|
|
677
|
-
const cycle = 5 * 60 * 60 * 1000;
|
|
678
|
-
const elapsed = now - t;
|
|
679
|
-
t += Math.ceil(elapsed / cycle) * cycle;
|
|
680
|
-
copy.fiveHourResetsAt = new Date(t).toISOString();
|
|
681
|
-
}
|
|
679
|
+
const t = new Date(copy.fiveHourResetsAt).getTime();
|
|
680
|
+
if (!isNaN(t)) copy.fiveHourResetsAt = new Date(advanceToNextCycle(t, FIVE_HOUR_MS)).toISOString();
|
|
682
681
|
}
|
|
683
|
-
|
|
684
|
-
// 7일 주기: 과거 reset → 7일씩 전진하여 미래 시점 추정
|
|
685
682
|
if (copy.weeklyResetsAt) {
|
|
686
|
-
|
|
687
|
-
if (t
|
|
688
|
-
const cycle = 7 * 24 * 60 * 60 * 1000;
|
|
689
|
-
const elapsed = now - t;
|
|
690
|
-
t += Math.ceil(elapsed / cycle) * cycle;
|
|
691
|
-
copy.weeklyResetsAt = new Date(t).toISOString();
|
|
692
|
-
}
|
|
683
|
+
const t = new Date(copy.weeklyResetsAt).getTime();
|
|
684
|
+
if (!isNaN(t)) copy.weeklyResetsAt = new Date(advanceToNextCycle(t, SEVEN_DAY_MS)).toISOString();
|
|
693
685
|
}
|
|
694
|
-
|
|
695
686
|
return copy;
|
|
696
687
|
}
|
|
697
688
|
|
|
@@ -817,12 +808,15 @@ function scheduleClaudeUsageRefresh() {
|
|
|
817
808
|
|
|
818
809
|
try {
|
|
819
810
|
const child = spawn(process.execPath, [scriptPath, CLAUDE_REFRESH_FLAG], {
|
|
820
|
-
detached:
|
|
811
|
+
detached: true,
|
|
821
812
|
stdio: "ignore",
|
|
822
813
|
windowsHide: true,
|
|
823
814
|
});
|
|
824
815
|
child.unref();
|
|
825
|
-
} catch {
|
|
816
|
+
} catch (spawnErr) {
|
|
817
|
+
// spawn 실패 시 에러 유형을 캐시에 기록 (HUD에서 원인 힌트 표시 가능)
|
|
818
|
+
writeClaudeUsageCache(null, { type: "network", status: 0, hint: String(spawnErr?.message || spawnErr) });
|
|
819
|
+
}
|
|
826
820
|
}
|
|
827
821
|
|
|
828
822
|
function getContextPercent(stdin) {
|
|
@@ -837,11 +831,20 @@ function getContextPercent(stdin) {
|
|
|
837
831
|
return clampPercent((totalTokens / capacity) * 100);
|
|
838
832
|
}
|
|
839
833
|
|
|
840
|
-
|
|
834
|
+
// 과거 리셋 시간 → 다음 주기로 순환하여 미래 시점 반환
|
|
835
|
+
function advanceToNextCycle(epochMs, cycleMs) {
|
|
836
|
+
const now = Date.now();
|
|
837
|
+
if (epochMs >= now || !cycleMs) return epochMs;
|
|
838
|
+
const elapsed = now - epochMs;
|
|
839
|
+
return epochMs + Math.ceil(elapsed / cycleMs) * cycleMs;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function formatResetRemaining(isoOrUnix, cycleMs = 0) {
|
|
841
843
|
if (!isoOrUnix) return "";
|
|
842
844
|
const d = typeof isoOrUnix === "string" ? new Date(isoOrUnix) : new Date(isoOrUnix * 1000);
|
|
843
845
|
if (isNaN(d.getTime())) return "";
|
|
844
|
-
const
|
|
846
|
+
const targetMs = advanceToNextCycle(d.getTime(), cycleMs);
|
|
847
|
+
const diffMs = targetMs - Date.now();
|
|
845
848
|
if (diffMs <= 0) return "";
|
|
846
849
|
const totalMinutes = Math.floor(diffMs / 60000);
|
|
847
850
|
const totalHours = Math.floor(totalMinutes / 60);
|
|
@@ -855,11 +858,12 @@ function isResetPast(isoOrUnix) {
|
|
|
855
858
|
return !isNaN(d.getTime()) && d.getTime() <= Date.now();
|
|
856
859
|
}
|
|
857
860
|
|
|
858
|
-
function formatResetRemainingDayHour(isoOrUnix) {
|
|
861
|
+
function formatResetRemainingDayHour(isoOrUnix, cycleMs = 0) {
|
|
859
862
|
if (!isoOrUnix) return "";
|
|
860
863
|
const d = typeof isoOrUnix === "string" ? new Date(isoOrUnix) : new Date(isoOrUnix * 1000);
|
|
861
864
|
if (isNaN(d.getTime())) return "";
|
|
862
|
-
const
|
|
865
|
+
const targetMs = advanceToNextCycle(d.getTime(), cycleMs);
|
|
866
|
+
const diffMs = targetMs - Date.now();
|
|
863
867
|
if (diffMs <= 0) return "";
|
|
864
868
|
const totalMinutes = Math.floor(diffMs / 60000);
|
|
865
869
|
const days = Math.floor(totalMinutes / (60 * 24));
|
|
@@ -906,20 +910,33 @@ function httpsPost(url, body, accessToken) {
|
|
|
906
910
|
});
|
|
907
911
|
}
|
|
908
912
|
|
|
913
|
+
// ============================================================================
|
|
914
|
+
// JWT base64 디코딩 공통 헬퍼
|
|
915
|
+
// ============================================================================
|
|
916
|
+
/**
|
|
917
|
+
* JWT 파일에서 이메일을 추출하는 공통 헬퍼.
|
|
918
|
+
* @param {string|null} idToken - JWT 문자열
|
|
919
|
+
* @returns {string|null} 이메일 또는 null
|
|
920
|
+
*/
|
|
921
|
+
function decodeJwtEmail(idToken) {
|
|
922
|
+
if (!idToken) return null;
|
|
923
|
+
const parts = idToken.split(".");
|
|
924
|
+
if (parts.length < 2) return null;
|
|
925
|
+
let payload = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
926
|
+
while (payload.length % 4) payload += "=";
|
|
927
|
+
try {
|
|
928
|
+
const decoded = JSON.parse(Buffer.from(payload, "base64").toString("utf-8"));
|
|
929
|
+
return decoded.email || null;
|
|
930
|
+
} catch { return null; }
|
|
931
|
+
}
|
|
932
|
+
|
|
909
933
|
// ============================================================================
|
|
910
934
|
// Codex JWT에서 이메일 추출
|
|
911
935
|
// ============================================================================
|
|
912
936
|
function getCodexEmail() {
|
|
913
937
|
try {
|
|
914
938
|
const auth = JSON.parse(readFileSync(CODEX_AUTH_PATH, "utf-8"));
|
|
915
|
-
|
|
916
|
-
if (!idToken) return null;
|
|
917
|
-
const parts = idToken.split(".");
|
|
918
|
-
if (parts.length < 2) return null;
|
|
919
|
-
let payload = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
920
|
-
while (payload.length % 4) payload += "=";
|
|
921
|
-
const decoded = JSON.parse(Buffer.from(payload, "base64").toString("utf-8"));
|
|
922
|
-
return decoded.email || null;
|
|
939
|
+
return decodeJwtEmail(auth?.tokens?.id_token);
|
|
923
940
|
} catch { return null; }
|
|
924
941
|
}
|
|
925
942
|
|
|
@@ -929,19 +946,15 @@ function getCodexEmail() {
|
|
|
929
946
|
function getGeminiEmail() {
|
|
930
947
|
try {
|
|
931
948
|
const oauth = readJson(GEMINI_OAUTH_PATH, null);
|
|
932
|
-
|
|
933
|
-
if (!idToken) return null;
|
|
934
|
-
const parts = idToken.split(".");
|
|
935
|
-
if (parts.length < 2) return null;
|
|
936
|
-
let payload = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
937
|
-
while (payload.length % 4) payload += "=";
|
|
938
|
-
const decoded = JSON.parse(Buffer.from(payload, "base64").toString("utf-8"));
|
|
939
|
-
return decoded.email || null;
|
|
949
|
+
return decodeJwtEmail(oauth?.id_token);
|
|
940
950
|
} catch { return null; }
|
|
941
951
|
}
|
|
942
952
|
|
|
943
953
|
// ============================================================================
|
|
944
954
|
// Codex 세션 JSONL에서 실제 rate limits 추출
|
|
955
|
+
// 한계: rate_limits는 세션별 스냅샷이므로 여러 세션 간 토큰 합산은 불가.
|
|
956
|
+
// 오늘 날짜의 모든 세션 파일을 스캔해 가장 최신 rate_limits 버킷을 수집한다.
|
|
957
|
+
// (단일 파일 즉시 return 방식에서 → 당일 전체 스캔 후 최신 데이터 우선 병합으로 변경)
|
|
945
958
|
// ============================================================================
|
|
946
959
|
function getCodexRateLimits() {
|
|
947
960
|
const now = new Date();
|
|
@@ -959,17 +972,21 @@ function getCodexRateLimits() {
|
|
|
959
972
|
try { files = readdirSync(sessDir).filter((f) => f.endsWith(".jsonl")).sort().reverse(); }
|
|
960
973
|
catch { continue; }
|
|
961
974
|
if (dayOffset === 0 && files.length > 0) todayHasFiles = true;
|
|
975
|
+
|
|
976
|
+
// 당일 모든 세션 파일을 스캔해 limit_id별 가장 최신 버킷을 병합
|
|
977
|
+
// (파일 목록은 이름 역순 정렬 → 최신 세션 우선)
|
|
978
|
+
const mergedBuckets = {};
|
|
962
979
|
for (const file of files) {
|
|
963
980
|
try {
|
|
964
981
|
const content = readFileSync(join(sessDir, file), "utf-8");
|
|
965
982
|
const lines = content.trim().split("\n").reverse();
|
|
966
|
-
const buckets = {};
|
|
967
983
|
for (const line of lines) {
|
|
968
984
|
try {
|
|
969
985
|
const evt = JSON.parse(line);
|
|
970
986
|
const rl = evt?.payload?.rate_limits;
|
|
971
|
-
if (rl?.limit_id && !
|
|
972
|
-
|
|
987
|
+
if (rl?.limit_id && !mergedBuckets[rl.limit_id]) {
|
|
988
|
+
// limit_id별로 첫 발견(=해당 세션의 가장 최신 이벤트)만 기록
|
|
989
|
+
mergedBuckets[rl.limit_id] = {
|
|
973
990
|
limitId: rl.limit_id, limitName: rl.limit_name,
|
|
974
991
|
primary: rl.primary, secondary: rl.secondary,
|
|
975
992
|
credits: rl.credits,
|
|
@@ -979,11 +996,12 @@ function getCodexRateLimits() {
|
|
|
979
996
|
};
|
|
980
997
|
}
|
|
981
998
|
} catch { /* 라인 파싱 실패 무시 */ }
|
|
982
|
-
if (Object.keys(
|
|
999
|
+
if (Object.keys(mergedBuckets).length >= CODEX_MIN_BUCKETS) break;
|
|
983
1000
|
}
|
|
984
|
-
if (Object.keys(buckets).length > 0) return buckets;
|
|
985
1001
|
} catch { /* 파일 읽기 실패 무시 */ }
|
|
986
1002
|
}
|
|
1003
|
+
if (Object.keys(mergedBuckets).length > 0) return mergedBuckets;
|
|
1004
|
+
|
|
987
1005
|
// 오늘 세션 파일이 존재하지만 rate_limits가 없으면 어제 stale 데이터로 폴백하지 않음
|
|
988
1006
|
if (todayHasFiles && dayOffset === 0) return null;
|
|
989
1007
|
}
|
|
@@ -1007,8 +1025,28 @@ async function fetchGeminiQuota(accountId, options = {}) {
|
|
|
1007
1025
|
return cache;
|
|
1008
1026
|
}
|
|
1009
1027
|
|
|
1010
|
-
if (!oauth?.access_token)
|
|
1011
|
-
|
|
1028
|
+
if (!oauth?.access_token) {
|
|
1029
|
+
// access_token 없음: 에러 힌트를 캐시에 기록하고 stale 캐시 반환
|
|
1030
|
+
writeJsonSafe(GEMINI_QUOTA_CACHE_PATH, {
|
|
1031
|
+
...(cache || {}),
|
|
1032
|
+
timestamp: cache?.timestamp || Date.now(),
|
|
1033
|
+
error: true,
|
|
1034
|
+
errorType: "auth",
|
|
1035
|
+
errorHint: "no access_token in oauth_creds.json",
|
|
1036
|
+
});
|
|
1037
|
+
return cache;
|
|
1038
|
+
}
|
|
1039
|
+
if (oauth.expiry_date && oauth.expiry_date < Date.now()) {
|
|
1040
|
+
// OAuth 토큰 만료: 에러 힌트를 캐시에 기록 (refresh_token 갱신은 Gemini CLI 담당)
|
|
1041
|
+
writeJsonSafe(GEMINI_QUOTA_CACHE_PATH, {
|
|
1042
|
+
...(cache || {}),
|
|
1043
|
+
timestamp: cache?.timestamp || Date.now(),
|
|
1044
|
+
error: true,
|
|
1045
|
+
errorType: "auth",
|
|
1046
|
+
errorHint: `token expired at ${new Date(oauth.expiry_date).toISOString()}`,
|
|
1047
|
+
});
|
|
1048
|
+
return cache;
|
|
1049
|
+
}
|
|
1012
1050
|
|
|
1013
1051
|
// 3. projectId (캐시 or API)
|
|
1014
1052
|
const fetchProjectId = async () => {
|
|
@@ -1045,7 +1083,18 @@ async function fetchGeminiQuota(accountId, options = {}) {
|
|
|
1045
1083
|
);
|
|
1046
1084
|
}
|
|
1047
1085
|
|
|
1048
|
-
if (!quotaRes?.buckets)
|
|
1086
|
+
if (!quotaRes?.buckets) {
|
|
1087
|
+
// API 응답에 buckets 없음: 에러 코드 또는 응답 내용을 캐시에 기록
|
|
1088
|
+
const apiError = quotaRes?.error?.message || quotaRes?.error?.code || quotaRes?.error || "no buckets in response";
|
|
1089
|
+
writeJsonSafe(GEMINI_QUOTA_CACHE_PATH, {
|
|
1090
|
+
...(cache || {}),
|
|
1091
|
+
timestamp: cache?.timestamp || Date.now(),
|
|
1092
|
+
error: true,
|
|
1093
|
+
errorType: "api",
|
|
1094
|
+
errorHint: String(apiError),
|
|
1095
|
+
});
|
|
1096
|
+
return cache;
|
|
1097
|
+
}
|
|
1049
1098
|
|
|
1050
1099
|
// 5. 캐시 저장
|
|
1051
1100
|
const result = {
|
|
@@ -1120,13 +1169,20 @@ function scheduleGeminiQuotaRefresh(accountId) {
|
|
|
1120
1169
|
process.execPath,
|
|
1121
1170
|
[scriptPath, GEMINI_REFRESH_FLAG, "--account", accountId || "gemini-main"],
|
|
1122
1171
|
{
|
|
1123
|
-
detached:
|
|
1172
|
+
detached: true,
|
|
1124
1173
|
stdio: "ignore",
|
|
1125
1174
|
windowsHide: true,
|
|
1126
1175
|
},
|
|
1127
1176
|
);
|
|
1128
1177
|
child.unref();
|
|
1129
|
-
} catch {
|
|
1178
|
+
} catch (spawnErr) {
|
|
1179
|
+
// spawn 실패 시 캐시에 에러 힌트 기록 (다음 HUD 렌더에서 원인 확인 가능)
|
|
1180
|
+
writeJsonSafe(GEMINI_QUOTA_CACHE_PATH, {
|
|
1181
|
+
timestamp: Date.now(),
|
|
1182
|
+
error: true,
|
|
1183
|
+
errorHint: String(spawnErr?.message || spawnErr),
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1130
1186
|
}
|
|
1131
1187
|
|
|
1132
1188
|
function readCodexRateLimitSnapshot() {
|
|
@@ -1152,12 +1208,20 @@ function scheduleCodexRateLimitRefresh() {
|
|
|
1152
1208
|
if (!scriptPath) return;
|
|
1153
1209
|
try {
|
|
1154
1210
|
const child = spawn(process.execPath, [scriptPath, CODEX_REFRESH_FLAG], {
|
|
1155
|
-
detached:
|
|
1211
|
+
detached: true,
|
|
1156
1212
|
stdio: "ignore",
|
|
1157
1213
|
windowsHide: true,
|
|
1158
1214
|
});
|
|
1159
1215
|
child.unref();
|
|
1160
|
-
} catch {
|
|
1216
|
+
} catch (spawnErr) {
|
|
1217
|
+
// spawn 실패 시 캐시에 에러 힌트 기록
|
|
1218
|
+
writeJsonSafe(CODEX_QUOTA_CACHE_PATH, {
|
|
1219
|
+
timestamp: Date.now(),
|
|
1220
|
+
buckets: null,
|
|
1221
|
+
error: true,
|
|
1222
|
+
errorHint: String(spawnErr?.message || spawnErr),
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1161
1225
|
}
|
|
1162
1226
|
|
|
1163
1227
|
function readGeminiSessionSnapshot() {
|
|
@@ -1183,7 +1247,7 @@ function scheduleGeminiSessionRefresh() {
|
|
|
1183
1247
|
if (!scriptPath) return;
|
|
1184
1248
|
try {
|
|
1185
1249
|
const child = spawn(process.execPath, [scriptPath, GEMINI_SESSION_REFRESH_FLAG], {
|
|
1186
|
-
detached:
|
|
1250
|
+
detached: true,
|
|
1187
1251
|
stdio: "ignore",
|
|
1188
1252
|
windowsHide: true,
|
|
1189
1253
|
});
|
|
@@ -1265,13 +1329,14 @@ function getClaudeRows(stdin, claudeUsage, combinedSvPct) {
|
|
|
1265
1329
|
const svSuffix = `${dim("sv:")}${svStr}`;
|
|
1266
1330
|
|
|
1267
1331
|
// API 실측 데이터 사용 (없으면 플레이스홀더)
|
|
1268
|
-
|
|
1269
|
-
const
|
|
1332
|
+
// 캐시된 percent 그대로 사용 (시간 표시는 advanceToNextCycle이 처리)
|
|
1333
|
+
const fiveHourPercent = claudeUsage?.fiveHourPercent ?? null;
|
|
1334
|
+
const weeklyPercent = claudeUsage?.weeklyPercent ?? null;
|
|
1270
1335
|
const fiveHourReset = claudeUsage?.fiveHourResetsAt
|
|
1271
|
-
? formatResetRemaining(claudeUsage.fiveHourResetsAt)
|
|
1336
|
+
? formatResetRemaining(claudeUsage.fiveHourResetsAt, FIVE_HOUR_MS)
|
|
1272
1337
|
: "n/a";
|
|
1273
1338
|
const weeklyReset = claudeUsage?.weeklyResetsAt
|
|
1274
|
-
? formatResetRemainingDayHour(claudeUsage.weeklyResetsAt)
|
|
1339
|
+
? formatResetRemainingDayHour(claudeUsage.weeklyResetsAt, SEVEN_DAY_MS)
|
|
1275
1340
|
: "n/a";
|
|
1276
1341
|
|
|
1277
1342
|
const hasData = claudeUsage != null;
|
|
@@ -1285,14 +1350,23 @@ function getClaudeRows(stdin, claudeUsage, combinedSvPct) {
|
|
|
1285
1350
|
return [{ prefix, left: quotaSection, right: "" }];
|
|
1286
1351
|
}
|
|
1287
1352
|
if (cols < 40) {
|
|
1288
|
-
|
|
1289
|
-
|
|
1353
|
+
// null이면 '--' 플레이스홀더, 아니면 실제 값
|
|
1354
|
+
const fStr = fiveHourPercent != null ? `${colorByProvider(fiveHourPercent, `${fiveHourPercent}%`, claudeOrange)}` : dim("--");
|
|
1355
|
+
const wStr = weeklyPercent != null ? `${colorByProvider(weeklyPercent, `${weeklyPercent}%`, claudeOrange)}` : dim("--");
|
|
1356
|
+
const quotaSection = `${fStr}${dim("/")}${wStr} ` +
|
|
1290
1357
|
`${dim("ctx:")}${colorByPercent(contextPercent, `${contextPercent}%`)}`;
|
|
1291
1358
|
return [{ prefix, left: quotaSection, right: "" }];
|
|
1292
1359
|
}
|
|
1293
1360
|
// nano: c: 5h 12% 1w 95% sv: 191% ctx:90%
|
|
1294
|
-
|
|
1295
|
-
|
|
1361
|
+
// null이면 '--%' 플레이스홀더 표시
|
|
1362
|
+
const fCellNano = fiveHourPercent != null
|
|
1363
|
+
? colorByProvider(fiveHourPercent, formatPercentCell(fiveHourPercent), claudeOrange)
|
|
1364
|
+
: dim(formatPlaceholderPercentCell());
|
|
1365
|
+
const wCellNano = weeklyPercent != null
|
|
1366
|
+
? colorByProvider(weeklyPercent, formatPercentCell(weeklyPercent), claudeOrange)
|
|
1367
|
+
: dim(formatPlaceholderPercentCell());
|
|
1368
|
+
const quotaSection = `${dim("5h")} ${fCellNano} ` +
|
|
1369
|
+
`${dim("1w")} ${wCellNano} ` +
|
|
1296
1370
|
`${dim("sv:")}${svStr} ${dim("ctx:")}${colorByPercent(contextPercent, `${contextPercent}%`)}`;
|
|
1297
1371
|
return [{ prefix, left: quotaSection, right: "" }];
|
|
1298
1372
|
}
|
|
@@ -1305,8 +1379,15 @@ function getClaudeRows(stdin, claudeUsage, combinedSvPct) {
|
|
|
1305
1379
|
return [{ prefix, left: quotaSection, right: "" }];
|
|
1306
1380
|
}
|
|
1307
1381
|
// compact: c: 5h: 14% 1w: 96% | sv: 191% ctx:43%
|
|
1308
|
-
|
|
1309
|
-
|
|
1382
|
+
// null이면 '--%' 플레이스홀더 표시
|
|
1383
|
+
const fCellCmp = fiveHourPercent != null
|
|
1384
|
+
? colorByProvider(fiveHourPercent, formatPercentCell(fiveHourPercent), claudeOrange)
|
|
1385
|
+
: dim(formatPlaceholderPercentCell());
|
|
1386
|
+
const wCellCmp = weeklyPercent != null
|
|
1387
|
+
? colorByProvider(weeklyPercent, formatPercentCell(weeklyPercent), claudeOrange)
|
|
1388
|
+
: dim(formatPlaceholderPercentCell());
|
|
1389
|
+
const quotaSection = `${dim("5h:")}${fCellCmp} ` +
|
|
1390
|
+
`${dim("1w:")}${wCellCmp} ` +
|
|
1310
1391
|
`${dim("|")} ${svSuffix} ${dim("ctx:")}${colorByPercent(contextPercent, `${contextPercent}%`)}`;
|
|
1311
1392
|
return [{ prefix, left: quotaSection, right: "" }];
|
|
1312
1393
|
}
|
|
@@ -1321,13 +1402,20 @@ function getClaudeRows(stdin, claudeUsage, combinedSvPct) {
|
|
|
1321
1402
|
return [{ prefix, left: quotaSection, right: contextSection }];
|
|
1322
1403
|
}
|
|
1323
1404
|
|
|
1324
|
-
|
|
1325
|
-
const
|
|
1405
|
+
// null이면 dim 바 + '--%' 플레이스홀더, 아니면 실제 값 표시
|
|
1406
|
+
const fiveHourPercentCell = fiveHourPercent != null
|
|
1407
|
+
? colorByProvider(fiveHourPercent, formatPercentCell(fiveHourPercent), claudeOrange)
|
|
1408
|
+
: dim(formatPlaceholderPercentCell());
|
|
1409
|
+
const weeklyPercentCell = weeklyPercent != null
|
|
1410
|
+
? colorByProvider(weeklyPercent, formatPercentCell(weeklyPercent), claudeOrange)
|
|
1411
|
+
: dim(formatPlaceholderPercentCell());
|
|
1412
|
+
const fiveHourBar = fiveHourPercent != null ? tierBar(fiveHourPercent, CLAUDE_ORANGE) : tierDimBar();
|
|
1413
|
+
const weeklyBar = weeklyPercent != null ? tierBar(weeklyPercent, CLAUDE_ORANGE) : tierDimBar();
|
|
1326
1414
|
const fiveHourTimeCell = formatTimeCell(fiveHourReset);
|
|
1327
1415
|
const weeklyTimeCell = formatTimeCellDH(weeklyReset);
|
|
1328
|
-
const quotaSection = `${dim("5h:")}${
|
|
1416
|
+
const quotaSection = `${dim("5h:")}${fiveHourBar}${fiveHourPercentCell} ` +
|
|
1329
1417
|
`${dim(fiveHourTimeCell)} ` +
|
|
1330
|
-
`${dim("1w:")}${
|
|
1418
|
+
`${dim("1w:")}${weeklyBar}${weeklyPercentCell} ` +
|
|
1331
1419
|
`${dim(weeklyTimeCell)}`;
|
|
1332
1420
|
const contextSection = `${svSuffix} ${dim("|")} ${dim("ctx:")}${colorByPercent(contextPercent, `${contextPercent}%`)}`;
|
|
1333
1421
|
return [{ prefix, left: quotaSection, right: contextSection }];
|
|
@@ -1368,12 +1456,15 @@ function getProviderRow(provider, marker, markerColor, qosProfile, accountsConfi
|
|
|
1368
1456
|
if (realQuota?.type === "codex") {
|
|
1369
1457
|
const main = realQuota.buckets.codex || realQuota.buckets[Object.keys(realQuota.buckets)[0]];
|
|
1370
1458
|
if (main) {
|
|
1371
|
-
|
|
1372
|
-
const
|
|
1459
|
+
// 캐시된 값 그대로 표시 (시간은 advanceToNextCycle이 처리)
|
|
1460
|
+
const fiveP = main.primary?.used_percent != null ? clampPercent(main.primary.used_percent) : null;
|
|
1461
|
+
const weekP = main.secondary?.used_percent != null ? clampPercent(main.secondary.used_percent) : null;
|
|
1462
|
+
const fCellN = fiveP != null ? colorByProvider(fiveP, formatPercentCell(fiveP), provFn) : dim(formatPlaceholderPercentCell());
|
|
1463
|
+
const wCellN = weekP != null ? colorByProvider(weekP, formatPercentCell(weekP), provFn) : dim(formatPlaceholderPercentCell());
|
|
1373
1464
|
if (cols < 40) {
|
|
1374
|
-
return { prefix: minPrefix, left: `${
|
|
1465
|
+
return { prefix: minPrefix, left: `${fCellN}${dim("/")}${wCellN}${svCompact}`, right: "" };
|
|
1375
1466
|
}
|
|
1376
|
-
return { prefix: minPrefix, left: `${dim("5h")} ${
|
|
1467
|
+
return { prefix: minPrefix, left: `${dim("5h")} ${fCellN} ${dim("1w")} ${wCellN}${svCompact}`, right: "" };
|
|
1377
1468
|
}
|
|
1378
1469
|
}
|
|
1379
1470
|
if (realQuota?.type === "gemini") {
|
|
@@ -1393,10 +1484,13 @@ function getProviderRow(provider, marker, markerColor, qosProfile, accountsConfi
|
|
|
1393
1484
|
if (realQuota?.type === "codex") {
|
|
1394
1485
|
const main = realQuota.buckets.codex || realQuota.buckets[Object.keys(realQuota.buckets)[0]];
|
|
1395
1486
|
if (main) {
|
|
1396
|
-
|
|
1397
|
-
const
|
|
1398
|
-
|
|
1399
|
-
|
|
1487
|
+
// 캐시된 값 그대로 표시 (시간은 advanceToNextCycle이 처리)
|
|
1488
|
+
const fiveP = main.primary?.used_percent != null ? clampPercent(main.primary.used_percent) : null;
|
|
1489
|
+
const weekP = main.secondary?.used_percent != null ? clampPercent(main.secondary.used_percent) : null;
|
|
1490
|
+
const fCell = fiveP != null ? colorByProvider(fiveP, formatPercentCell(fiveP), provFn) : dim(formatPlaceholderPercentCell());
|
|
1491
|
+
const wCell = weekP != null ? colorByProvider(weekP, formatPercentCell(weekP), provFn) : dim(formatPlaceholderPercentCell());
|
|
1492
|
+
quotaSection = `${dim("5h:")}${fCell} ` +
|
|
1493
|
+
`${dim("1w:")}${wCell}`;
|
|
1400
1494
|
}
|
|
1401
1495
|
}
|
|
1402
1496
|
if (realQuota?.type === "gemini") {
|
|
@@ -1409,7 +1503,7 @@ function getProviderRow(provider, marker, markerColor, qosProfile, accountsConfi
|
|
|
1409
1503
|
}
|
|
1410
1504
|
}
|
|
1411
1505
|
if (!quotaSection) {
|
|
1412
|
-
quotaSection = `${dim("5h:")}${
|
|
1506
|
+
quotaSection = `${dim("5h:")}${dim(formatPlaceholderPercentCell())} ${dim("1w:")}${dim(formatPlaceholderPercentCell())}`;
|
|
1413
1507
|
}
|
|
1414
1508
|
const prefix = `${bold(markerColor(`${marker}`))}:`;
|
|
1415
1509
|
// compact: sv + 계정 (모델 라벨 제거)
|
|
@@ -1420,13 +1514,18 @@ function getProviderRow(provider, marker, markerColor, qosProfile, accountsConfi
|
|
|
1420
1514
|
if (realQuota?.type === "codex") {
|
|
1421
1515
|
const main = realQuota.buckets.codex || realQuota.buckets[Object.keys(realQuota.buckets)[0]];
|
|
1422
1516
|
if (main) {
|
|
1423
|
-
|
|
1424
|
-
const
|
|
1425
|
-
const
|
|
1426
|
-
const
|
|
1427
|
-
|
|
1517
|
+
// 캐시된 값 그대로 표시 (시간은 advanceToNextCycle이 처리)
|
|
1518
|
+
const fiveP = main.primary?.used_percent != null ? clampPercent(main.primary.used_percent) : null;
|
|
1519
|
+
const weekP = main.secondary?.used_percent != null ? clampPercent(main.secondary.used_percent) : null;
|
|
1520
|
+
const fiveReset = formatResetRemaining(main.primary?.resets_at, FIVE_HOUR_MS) || "n/a";
|
|
1521
|
+
const weekReset = formatResetRemainingDayHour(main.secondary?.resets_at, SEVEN_DAY_MS) || "n/a";
|
|
1522
|
+
const fCell = fiveP != null ? colorByProvider(fiveP, formatPercentCell(fiveP), provFn) : dim(formatPlaceholderPercentCell());
|
|
1523
|
+
const wCell = weekP != null ? colorByProvider(weekP, formatPercentCell(weekP), provFn) : dim(formatPlaceholderPercentCell());
|
|
1524
|
+
const fBar = fiveP != null ? tierBar(fiveP, provAnsi) : tierDimBar();
|
|
1525
|
+
const wBar = weekP != null ? tierBar(weekP, provAnsi) : tierDimBar();
|
|
1526
|
+
quotaSection = `${dim("5h:")}${fBar}${fCell} ` +
|
|
1428
1527
|
`${dim(formatTimeCell(fiveReset))} ` +
|
|
1429
|
-
`${dim("1w:")}${
|
|
1528
|
+
`${dim("1w:")}${wBar}${wCell} ` +
|
|
1430
1529
|
`${dim(formatTimeCellDH(weekReset))}`;
|
|
1431
1530
|
}
|
|
1432
1531
|
}
|
|
@@ -1435,7 +1534,7 @@ function getProviderRow(provider, marker, markerColor, qosProfile, accountsConfi
|
|
|
1435
1534
|
const bucket = realQuota.quotaBucket;
|
|
1436
1535
|
if (bucket) {
|
|
1437
1536
|
const usedP = clampPercent((1 - (bucket.remainingFraction ?? 1)) * 100);
|
|
1438
|
-
const rstRemaining = formatResetRemaining(bucket.resetTime) || "n/a";
|
|
1537
|
+
const rstRemaining = formatResetRemaining(bucket.resetTime, ONE_DAY_MS) || "n/a";
|
|
1439
1538
|
quotaSection = `${dim("1d:")}${tierBar(usedP, provAnsi)}${colorByProvider(usedP, formatPercentCell(usedP), provFn)} ${dim(formatTimeCell(rstRemaining))} ` +
|
|
1440
1539
|
`${dim("1w:")}${tierInfBar()}${dim("\u221E%".padStart(PERCENT_CELL_WIDTH))} ${dim(formatTimeCellDH("-d--h"))}`;
|
|
1441
1540
|
} else {
|
package/package.json
CHANGED
package/scripts/cli-route.sh
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# cli-route.sh v1.
|
|
2
|
+
# cli-route.sh v1.7 — CLI 라우팅 래퍼 (ai-scaffold 템플릿)
|
|
3
3
|
# v1.0: 기본 라우팅 (Codex/Gemini/Claude 분기)
|
|
4
4
|
# v1.1: stderr 분리, 출력 필터링, 타임아웃, MCP 프로필 지원
|
|
5
5
|
# v1.2: effort 동적 라우팅, bg/fg 모드, Opus 직접 수행, Gemini 모델 분기, 실행 로그
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
# v1.4: TFX_CLI_MODE 지원 (codex-only/gemini-only), CLI 미설치 자동 fallback
|
|
8
8
|
# v1.5: MCP 인벤토리 캐싱 — 실제 서버 가용성 기반 동적 힌트 생성
|
|
9
9
|
# v1.6: 토큰 사용량 추출 + sv-accumulator.json 누적
|
|
10
|
-
|
|
10
|
+
# v1.7: 배치 AIMD 전략 — 성공 시 +1, 실패/타임아웃 시 ×0.5, 수렴 감지
|
|
11
|
+
VERSION="1.7"
|
|
11
12
|
#
|
|
12
13
|
# 설치: cp scripts/cli-route.sh ~/.claude/scripts/cli-route.sh
|
|
13
14
|
#
|
|
@@ -35,8 +36,207 @@ GEMINI_BIN="${GEMINI_BIN:-$(command -v gemini 2>/dev/null || echo gemini)}"
|
|
|
35
36
|
# ── 상수 ──
|
|
36
37
|
MAX_STDOUT_BYTES=51200 # 50KB — Claude 컨텍스트 절약
|
|
37
38
|
TIMESTAMP=$(date +%s)
|
|
38
|
-
STDERR_LOG="/tmp/
|
|
39
|
-
STDOUT_LOG="/tmp/
|
|
39
|
+
STDERR_LOG="/tmp/tfx-route-${AGENT_TYPE}-${TIMESTAMP}-stderr.log"
|
|
40
|
+
STDOUT_LOG="/tmp/tfx-route-${AGENT_TYPE}-${TIMESTAMP}-stdout.log"
|
|
41
|
+
|
|
42
|
+
# fallback 시 원래 에이전트/CLI 인자 보존용 (수정 3: review fallback 프로필 유실 방지)
|
|
43
|
+
ORIGINAL_AGENT=""
|
|
44
|
+
ORIGINAL_CLI_ARGS=""
|
|
45
|
+
|
|
46
|
+
# ── 크로스 세션 활성 에이전트 추적 ──
|
|
47
|
+
# 활성 에이전트 레지스트리 경로
|
|
48
|
+
ACTIVE_AGENTS_FILE="${HOME}/.claude/cache/active-agents.json"
|
|
49
|
+
|
|
50
|
+
# 죽은 PID 및 좀비 정리
|
|
51
|
+
cleanup_stale_agents() {
|
|
52
|
+
[[ ! -f "$ACTIVE_AGENTS_FILE" ]] && return
|
|
53
|
+
local now
|
|
54
|
+
now=$(date +%s)
|
|
55
|
+
local tmp="${ACTIVE_AGENTS_FILE}.tmp"
|
|
56
|
+
# jq가 있으면 사용, 없으면 건너뜀
|
|
57
|
+
if command -v jq &>/dev/null; then
|
|
58
|
+
jq --argjson now "$now" '
|
|
59
|
+
.agents |= map(select(
|
|
60
|
+
# PID 생존 확인은 셸에서 하므로 여기선 타임아웃만 체크
|
|
61
|
+
(.started + 1200) > $now
|
|
62
|
+
))
|
|
63
|
+
' "$ACTIVE_AGENTS_FILE" > "$tmp" 2>/dev/null && mv "$tmp" "$ACTIVE_AGENTS_FILE"
|
|
64
|
+
# 추가로 kill -0으로 죽은 PID 제거
|
|
65
|
+
local pids
|
|
66
|
+
pids=$(jq -r '.agents[].pid' "$ACTIVE_AGENTS_FILE" 2>/dev/null)
|
|
67
|
+
local pid
|
|
68
|
+
for pid in $pids; do
|
|
69
|
+
if ! kill -0 "$pid" 2>/dev/null; then
|
|
70
|
+
jq --argjson pid "$pid" '.agents |= map(select(.pid != $pid))' "$ACTIVE_AGENTS_FILE" > "$tmp" 2>/dev/null && mv "$tmp" "$ACTIVE_AGENTS_FILE"
|
|
71
|
+
fi
|
|
72
|
+
done
|
|
73
|
+
fi
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
# 에이전트 등록
|
|
77
|
+
register_agent() {
|
|
78
|
+
local pid="$1" cli="$2" agent="$3"
|
|
79
|
+
local now
|
|
80
|
+
now=$(date +%s)
|
|
81
|
+
cleanup_stale_agents
|
|
82
|
+
if command -v jq &>/dev/null; then
|
|
83
|
+
# 캐시 디렉토리가 없으면 생성
|
|
84
|
+
mkdir -p "$(dirname "$ACTIVE_AGENTS_FILE")"
|
|
85
|
+
if [[ -f "$ACTIVE_AGENTS_FILE" ]]; then
|
|
86
|
+
jq --argjson pid "$pid" --arg cli "$cli" --arg agent "$agent" --argjson started "$now" \
|
|
87
|
+
'.agents += [{"pid": $pid, "cli": $cli, "agent": $agent, "started": $started}]' \
|
|
88
|
+
"$ACTIVE_AGENTS_FILE" > "${ACTIVE_AGENTS_FILE}.tmp" && mv "${ACTIVE_AGENTS_FILE}.tmp" "$ACTIVE_AGENTS_FILE"
|
|
89
|
+
else
|
|
90
|
+
echo "{\"agents\":[{\"pid\":$pid,\"cli\":\"$cli\",\"agent\":\"$agent\",\"started\":$now}]}" > "$ACTIVE_AGENTS_FILE"
|
|
91
|
+
fi
|
|
92
|
+
fi
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# 에이전트 등록 해제
|
|
96
|
+
deregister_agent() {
|
|
97
|
+
local pid="$1"
|
|
98
|
+
if command -v jq &>/dev/null && [[ -f "$ACTIVE_AGENTS_FILE" ]]; then
|
|
99
|
+
jq --argjson pid "$pid" '.agents |= map(select(.pid != $pid))' \
|
|
100
|
+
"$ACTIVE_AGENTS_FILE" > "${ACTIVE_AGENTS_FILE}.tmp" && mv "${ACTIVE_AGENTS_FILE}.tmp" "$ACTIVE_AGENTS_FILE"
|
|
101
|
+
fi
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# ── 배치 AIMD 전략 ──
|
|
105
|
+
# 배치 설정 파일: ~/.claude/cache/batch-config.json
|
|
106
|
+
# 초기 batch_size=2, 성공→+1 (AI), 실패/타임아웃→×0.5 (MD), 상한=8, 수렴=3연속 동일
|
|
107
|
+
BATCH_CONFIG_FILE="${HOME}/.claude/cache/batch-config.json"
|
|
108
|
+
|
|
109
|
+
# 현재 batch_size 반환 (파일 없으면 기본값 2)
|
|
110
|
+
get_batch_size() {
|
|
111
|
+
if ! command -v jq &>/dev/null; then
|
|
112
|
+
echo "2"
|
|
113
|
+
return
|
|
114
|
+
fi
|
|
115
|
+
if [[ -f "$BATCH_CONFIG_FILE" ]]; then
|
|
116
|
+
local size
|
|
117
|
+
size=$(jq -r '.batch_size // 2' "$BATCH_CONFIG_FILE" 2>/dev/null)
|
|
118
|
+
# 숫자가 아니거나 비어 있으면 기본값
|
|
119
|
+
if [[ "$size" =~ ^[0-9]+$ ]]; then
|
|
120
|
+
echo "$size"
|
|
121
|
+
else
|
|
122
|
+
echo "2"
|
|
123
|
+
fi
|
|
124
|
+
else
|
|
125
|
+
echo "2"
|
|
126
|
+
fi
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# 현재 활성 에이전트 수 반환 (active-agents.json 기반)
|
|
130
|
+
get_active_agent_count() {
|
|
131
|
+
if ! command -v jq &>/dev/null; then
|
|
132
|
+
echo "0"
|
|
133
|
+
return
|
|
134
|
+
fi
|
|
135
|
+
if [[ -f "$ACTIVE_AGENTS_FILE" ]]; then
|
|
136
|
+
local count
|
|
137
|
+
count=$(jq '.agents | length' "$ACTIVE_AGENTS_FILE" 2>/dev/null)
|
|
138
|
+
echo "${count:-0}"
|
|
139
|
+
else
|
|
140
|
+
echo "0"
|
|
141
|
+
fi
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
# AIMD 결과 기록 및 batch_size 업데이트
|
|
145
|
+
# 인자: result (success/failed/timeout), agent
|
|
146
|
+
update_batch_result() {
|
|
147
|
+
local result="$1"
|
|
148
|
+
local agent="${2:-unknown}"
|
|
149
|
+
|
|
150
|
+
if ! command -v jq &>/dev/null; then
|
|
151
|
+
return
|
|
152
|
+
fi
|
|
153
|
+
|
|
154
|
+
mkdir -p "$(dirname "$BATCH_CONFIG_FILE")"
|
|
155
|
+
|
|
156
|
+
# 현재 설정 읽기 (없으면 초기값)
|
|
157
|
+
local current_size consecutive_same converged
|
|
158
|
+
if [[ -f "$BATCH_CONFIG_FILE" ]]; then
|
|
159
|
+
current_size=$(jq -r '.batch_size // 2' "$BATCH_CONFIG_FILE" 2>/dev/null)
|
|
160
|
+
consecutive_same=$(jq -r '.consecutive_same // 0' "$BATCH_CONFIG_FILE" 2>/dev/null)
|
|
161
|
+
converged=$(jq -r '.converged // false' "$BATCH_CONFIG_FILE" 2>/dev/null)
|
|
162
|
+
else
|
|
163
|
+
current_size=2
|
|
164
|
+
consecutive_same=0
|
|
165
|
+
converged="false"
|
|
166
|
+
fi
|
|
167
|
+
|
|
168
|
+
# 숫자 검증
|
|
169
|
+
[[ "$current_size" =~ ^[0-9]+$ ]] || current_size=2
|
|
170
|
+
[[ "$consecutive_same" =~ ^[0-9]+$ ]] || consecutive_same=0
|
|
171
|
+
|
|
172
|
+
# 수렴 상태면 batch_size 고정 (업데이트만 기록)
|
|
173
|
+
local new_size="$current_size"
|
|
174
|
+
if [[ "$converged" != "true" ]]; then
|
|
175
|
+
case "$result" in
|
|
176
|
+
success)
|
|
177
|
+
# Additive Increase: +1, 상한 8
|
|
178
|
+
new_size=$((current_size + 1))
|
|
179
|
+
if [[ $new_size -gt 8 ]]; then new_size=8; fi
|
|
180
|
+
;;
|
|
181
|
+
failed|timeout)
|
|
182
|
+
# Multiplicative Decrease: ×0.5, 하한 1
|
|
183
|
+
new_size=$((current_size / 2))
|
|
184
|
+
if [[ $new_size -lt 1 ]]; then new_size=1; fi
|
|
185
|
+
;;
|
|
186
|
+
esac
|
|
187
|
+
fi
|
|
188
|
+
|
|
189
|
+
# 수렴 판단: 3연속 동일하면 converged=true
|
|
190
|
+
if [[ $new_size -eq $current_size ]]; then
|
|
191
|
+
consecutive_same=$((consecutive_same + 1))
|
|
192
|
+
else
|
|
193
|
+
consecutive_same=0
|
|
194
|
+
fi
|
|
195
|
+
|
|
196
|
+
local new_converged="false"
|
|
197
|
+
if [[ $consecutive_same -ge 3 ]]; then
|
|
198
|
+
new_converged="true"
|
|
199
|
+
fi
|
|
200
|
+
|
|
201
|
+
local now
|
|
202
|
+
now=$(date +%s)
|
|
203
|
+
|
|
204
|
+
# history에 추가 (최대 50건 유지) 후 batch_size 업데이트
|
|
205
|
+
local tmp="${BATCH_CONFIG_FILE}.tmp"
|
|
206
|
+
if [[ -f "$BATCH_CONFIG_FILE" ]]; then
|
|
207
|
+
jq --argjson now "$now" \
|
|
208
|
+
--arg agent "$agent" \
|
|
209
|
+
--arg result "$result" \
|
|
210
|
+
--argjson batch_at_time "$current_size" \
|
|
211
|
+
--argjson new_size "$new_size" \
|
|
212
|
+
--argjson consecutive_same "$consecutive_same" \
|
|
213
|
+
--argjson converged "$new_converged" \
|
|
214
|
+
'
|
|
215
|
+
.history += [{"timestamp": $now, "agent": $agent, "result": $result, "batch_at_time": $batch_at_time}] |
|
|
216
|
+
.history = (.history | if length > 50 then .[-50:] else . end) |
|
|
217
|
+
.batch_size = $new_size |
|
|
218
|
+
.consecutive_same = $consecutive_same |
|
|
219
|
+
.converged = $converged
|
|
220
|
+
' "$BATCH_CONFIG_FILE" > "$tmp" 2>/dev/null && mv "$tmp" "$BATCH_CONFIG_FILE"
|
|
221
|
+
else
|
|
222
|
+
# 파일 신규 생성
|
|
223
|
+
jq -n \
|
|
224
|
+
--argjson now "$now" \
|
|
225
|
+
--arg agent "$agent" \
|
|
226
|
+
--arg result "$result" \
|
|
227
|
+
--argjson new_size "$new_size" \
|
|
228
|
+
--argjson consecutive_same "$consecutive_same" \
|
|
229
|
+
--argjson converged "$new_converged" \
|
|
230
|
+
'{
|
|
231
|
+
batch_size: $new_size,
|
|
232
|
+
history: [{"timestamp": $now, "agent": $agent, "result": $result, "batch_at_time": 2}],
|
|
233
|
+
consecutive_same: $consecutive_same,
|
|
234
|
+
converged: $converged
|
|
235
|
+
}' > "$BATCH_CONFIG_FILE" 2>/dev/null || true
|
|
236
|
+
fi
|
|
237
|
+
|
|
238
|
+
echo "[cli-route] AIMD: $result → batch_size $current_size→$new_size (consecutive_same=$consecutive_same, converged=$new_converged)" >&2
|
|
239
|
+
}
|
|
40
240
|
|
|
41
241
|
# ── 라우팅 테이블 ──
|
|
42
242
|
# 반환: CLI_CMD, CLI_ARGS, CLI_TYPE, CLI_EFFORT, DEFAULT_TIMEOUT, RUN_MODE, OPUS_OVERSIGHT
|
|
@@ -373,10 +573,13 @@ apply_cli_mode() {
|
|
|
373
573
|
apply_cli_mode
|
|
374
574
|
return
|
|
375
575
|
else
|
|
576
|
+
# 원래 에이전트 및 MCP 프로필 정보 보존
|
|
577
|
+
ORIGINAL_AGENT="${AGENT_TYPE}"
|
|
578
|
+
ORIGINAL_CLI_ARGS="$CLI_ARGS"
|
|
376
579
|
CLI_TYPE="claude-native"
|
|
377
580
|
CLI_CMD=""
|
|
378
581
|
CLI_ARGS=""
|
|
379
|
-
echo "[cli-route] codex/gemini 모두 미설치: $AGENT_TYPE → claude-native fallback" >&2
|
|
582
|
+
echo "[cli-route] codex/gemini 모두 미설치: $AGENT_TYPE → claude-native fallback (원래 프로필: $MCP_PROFILE)" >&2
|
|
380
583
|
fi
|
|
381
584
|
elif [[ "$CLI_TYPE" == "gemini" ]] && ! command -v "$GEMINI_BIN" &>/dev/null; then
|
|
382
585
|
if command -v "$CODEX_BIN" &>/dev/null; then
|
|
@@ -384,10 +587,13 @@ apply_cli_mode() {
|
|
|
384
587
|
apply_cli_mode
|
|
385
588
|
return
|
|
386
589
|
else
|
|
590
|
+
# 원래 에이전트 및 MCP 프로필 정보 보존
|
|
591
|
+
ORIGINAL_AGENT="${AGENT_TYPE}"
|
|
592
|
+
ORIGINAL_CLI_ARGS="$CLI_ARGS"
|
|
387
593
|
CLI_TYPE="claude-native"
|
|
388
594
|
CLI_CMD=""
|
|
389
595
|
CLI_ARGS=""
|
|
390
|
-
echo "[cli-route] codex/gemini 모두 미설치: $AGENT_TYPE → claude-native fallback" >&2
|
|
596
|
+
echo "[cli-route] codex/gemini 모두 미설치: $AGENT_TYPE → claude-native fallback (원래 프로필: $MCP_PROFILE)" >&2
|
|
391
597
|
fi
|
|
392
598
|
fi
|
|
393
599
|
;;
|
|
@@ -764,6 +970,9 @@ truncate_output() {
|
|
|
764
970
|
|
|
765
971
|
# ── 메인 실행 ──
|
|
766
972
|
main() {
|
|
973
|
+
# 종료 시 활성 에이전트 레지스트리에서 자동 제거
|
|
974
|
+
trap 'deregister_agent $$' EXIT
|
|
975
|
+
|
|
767
976
|
route_agent "$AGENT_TYPE"
|
|
768
977
|
|
|
769
978
|
# CLI 모드 오버라이드 적용 (tfx-codex/tfx-gemini 또는 auto-fallback)
|
|
@@ -782,12 +991,12 @@ main() {
|
|
|
782
991
|
TIMEOUT_SEC="$DEFAULT_TIMEOUT"
|
|
783
992
|
fi
|
|
784
993
|
|
|
785
|
-
# kteam 안정화: Gemini 에이전트 기본 타임아웃
|
|
994
|
+
# kteam 안정화: Gemini 에이전트 기본 타임아웃 하한 적용 (사용자 미지정 시만)
|
|
786
995
|
if [[ -z "$USER_TIMEOUT" ]]; then
|
|
787
996
|
case "$AGENT_TYPE" in
|
|
788
997
|
designer|writer)
|
|
789
|
-
if [[ "$DEFAULT_TIMEOUT" -gt
|
|
790
|
-
TIMEOUT_SEC=
|
|
998
|
+
if [[ "$DEFAULT_TIMEOUT" -gt 300 ]]; then
|
|
999
|
+
TIMEOUT_SEC=300
|
|
791
1000
|
fi
|
|
792
1001
|
;;
|
|
793
1002
|
esac
|
|
@@ -808,6 +1017,12 @@ main() {
|
|
|
808
1017
|
echo "RUN_MODE=$RUN_MODE"
|
|
809
1018
|
echo "OPUS_OVERSIGHT=$OPUS_OVERSIGHT"
|
|
810
1019
|
echo "TIMEOUT=$TIMEOUT_SEC"
|
|
1020
|
+
echo "MCP_PROFILE=$MCP_PROFILE"
|
|
1021
|
+
# fallback 시 원래 에이전트/MCP 프로필 정보를 함께 출력 (수정 3)
|
|
1022
|
+
if [[ -n "$ORIGINAL_AGENT" ]]; then
|
|
1023
|
+
echo "ORIGINAL_AGENT=$ORIGINAL_AGENT"
|
|
1024
|
+
echo "ORIGINAL_CLI_ARGS=$ORIGINAL_CLI_ARGS"
|
|
1025
|
+
fi
|
|
811
1026
|
echo "PROMPT=$PROMPT"
|
|
812
1027
|
echo "--- Claude Task($model) 에이전트로 위임하세요 ---"
|
|
813
1028
|
exit 0
|
|
@@ -825,6 +1040,9 @@ main() {
|
|
|
825
1040
|
echo "[cli-route] type=$CLI_TYPE agent=$AGENT_TYPE effort=$CLI_EFFORT mode=$RUN_MODE timeout=${TIMEOUT_SEC}s" >&2
|
|
826
1041
|
echo "[cli-route] opus_oversight=$OPUS_OVERSIGHT mcp_profile=$MCP_PROFILE stderr_log=$STDERR_LOG" >&2
|
|
827
1042
|
|
|
1043
|
+
# 크로스 세션 활성 에이전트 레지스트리에 등록 (수정 4)
|
|
1044
|
+
register_agent $$ "$CLI_TYPE" "$AGENT_TYPE"
|
|
1045
|
+
|
|
828
1046
|
# CLI 실행 (stderr 분리 + 타임아웃 + 소요시간 측정)
|
|
829
1047
|
local exit_code=0
|
|
830
1048
|
local raw_output=""
|
|
@@ -906,6 +1124,15 @@ main() {
|
|
|
906
1124
|
accumulate_tokens "$CLI_TYPE" "$input_tokens" "$output_tokens" || true
|
|
907
1125
|
fi
|
|
908
1126
|
|
|
1127
|
+
# AIMD 배치 크기 업데이트 (exit code 기반)
|
|
1128
|
+
if [[ $exit_code -eq 0 ]]; then
|
|
1129
|
+
update_batch_result "success" "$AGENT_TYPE" || true
|
|
1130
|
+
elif [[ $exit_code -eq 124 ]]; then
|
|
1131
|
+
update_batch_result "timeout" "$AGENT_TYPE" || true
|
|
1132
|
+
else
|
|
1133
|
+
update_batch_result "failed" "$AGENT_TYPE" || true
|
|
1134
|
+
fi
|
|
1135
|
+
|
|
909
1136
|
# CLI 이슈 자동 수집
|
|
910
1137
|
local _stderr_for_track=""
|
|
911
1138
|
[[ -f "$STDERR_LOG" ]] && _stderr_for_track=$(cat "$STDERR_LOG" 2>/dev/null || echo "")
|
package/scripts/notion-read.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// notion-read.mjs v1.
|
|
2
|
+
// notion-read.mjs v1.2 — Notion 대형 페이지 리더 (Codex/Gemini/Claude MCP 위임)
|
|
3
3
|
//
|
|
4
4
|
// Codex/Gemini/Claude CLI에 설치된 Notion MCP를 활용하여 대형 페이지를 마크다운으로 추출.
|
|
5
5
|
// 폴백 체인: Codex(무료) → Gemini(무료) → Claude(최후) → 에러
|
|
6
|
+
// 이관 모드(--delegate): Claude(notion-guest 우선) 단독 실행 + 결과 파일 저장
|
|
6
7
|
//
|
|
7
8
|
// 사용법:
|
|
8
9
|
// node notion-read.mjs <notion-url-or-page-id> [옵션]
|
|
@@ -14,27 +15,28 @@
|
|
|
14
15
|
// --cli, -c <codex|gemini> CLI 강제 지정 (기본: 자동 + 폴백)
|
|
15
16
|
// --depth, -d <n> 중첩 블록 최대 깊이 (기본: 3)
|
|
16
17
|
// --guest notion-guest 통합 사용 (기본: notion)
|
|
18
|
+
// --delegate Claude 이관 모드 (notion-guest 우선, 파일 저장)
|
|
17
19
|
|
|
18
|
-
import { execSync } from
|
|
19
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync, unlinkSync } from
|
|
20
|
-
import { join, dirname } from
|
|
21
|
-
import { homedir, tmpdir } from
|
|
20
|
+
import { execSync } from 'child_process';
|
|
21
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync, unlinkSync } from 'fs';
|
|
22
|
+
import { join, dirname, resolve } from 'path';
|
|
23
|
+
import { homedir, tmpdir } from 'os';
|
|
22
24
|
|
|
23
|
-
const VERSION =
|
|
24
|
-
const CLAUDE_DIR = join(homedir(),
|
|
25
|
-
const MCP_CACHE = join(CLAUDE_DIR,
|
|
26
|
-
const LOG_FILE = join(CLAUDE_DIR,
|
|
27
|
-
const ACC_FILE = join(CLAUDE_DIR,
|
|
25
|
+
const VERSION = '1.2';
|
|
26
|
+
const CLAUDE_DIR = join(homedir(), '.claude');
|
|
27
|
+
const MCP_CACHE = join(CLAUDE_DIR, 'cache', 'mcp-inventory.json');
|
|
28
|
+
const LOG_FILE = join(CLAUDE_DIR, 'logs', 'cli-route-stats.jsonl');
|
|
29
|
+
const ACC_FILE = join(CLAUDE_DIR, 'cache', 'sv-accumulator.json');
|
|
28
30
|
|
|
29
31
|
// ── ANSI 색상 ──
|
|
30
|
-
const AMBER =
|
|
31
|
-
const GREEN =
|
|
32
|
-
const RED =
|
|
33
|
-
const YELLOW =
|
|
34
|
-
const DIM =
|
|
35
|
-
const BOLD =
|
|
36
|
-
const RESET =
|
|
37
|
-
const GRAY =
|
|
32
|
+
const AMBER = '\x1b[38;5;214m';
|
|
33
|
+
const GREEN = '\x1b[38;5;82m';
|
|
34
|
+
const RED = '\x1b[38;5;196m';
|
|
35
|
+
const YELLOW = '\x1b[33m';
|
|
36
|
+
const DIM = '\x1b[2m';
|
|
37
|
+
const BOLD = '\x1b[1m';
|
|
38
|
+
const RESET = '\x1b[0m';
|
|
39
|
+
const GRAY = '\x1b[38;5;245m';
|
|
38
40
|
|
|
39
41
|
// ── URL 파싱 ──
|
|
40
42
|
function parseNotionUrl(input) {
|
|
@@ -165,24 +167,24 @@ ${mcpServer} MCP 서버의 도구를 사용하라.
|
|
|
165
167
|
}
|
|
166
168
|
|
|
167
169
|
// ── CLI 실행 (임시 파일 + execSync — Windows .cmd 호환) ──
|
|
168
|
-
function runWithCli(cliType, prompt, timeout) {
|
|
169
|
-
const cliName = cliType ===
|
|
170
|
+
function runWithCli(cliType, prompt, timeout, runMode = 'fg') {
|
|
171
|
+
const cliName = cliType === 'claude' ? 'claude' : cliType === 'codex' ? 'codex' : 'gemini';
|
|
170
172
|
if (!cliExists(cliName)) {
|
|
171
|
-
return { success: false, output:
|
|
173
|
+
return { success: false, output: '', error: `${cliType} CLI 미설치`, cli: cliType };
|
|
172
174
|
}
|
|
173
175
|
|
|
174
176
|
// 프롬프트를 임시 파일에 저장 (shell escaping 회피)
|
|
175
177
|
const promptFile = join(tmpdir(), `notion-prompt-${Date.now()}.md`);
|
|
176
|
-
writeFileSync(promptFile, prompt,
|
|
177
|
-
const promptPath = promptFile.replace(/\\/g,
|
|
178
|
+
writeFileSync(promptFile, prompt, 'utf8');
|
|
179
|
+
const promptPath = promptFile.replace(/\\/g, '/');
|
|
178
180
|
|
|
179
181
|
// CLI에 전달할 짧은 메타 프롬프트
|
|
180
182
|
const metaPrompt = `Read the file at ${promptPath} and execute all instructions in it exactly as described. Output only the final markdown result.`;
|
|
181
183
|
|
|
182
184
|
let cmd;
|
|
183
|
-
if (cliType ===
|
|
185
|
+
if (cliType === 'codex') {
|
|
184
186
|
cmd = `codex exec --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check "${metaPrompt}"`;
|
|
185
|
-
} else if (cliType ===
|
|
187
|
+
} else if (cliType === 'gemini') {
|
|
186
188
|
cmd = `gemini -m gemini-3-flash-preview -y --allowed-mcp-server-names notion,notion-guest --prompt "${metaPrompt}"`;
|
|
187
189
|
} else {
|
|
188
190
|
// Claude CLI — print 모드 (MCP 도구 자동 접근)
|
|
@@ -192,16 +194,16 @@ function runWithCli(cliType, prompt, timeout) {
|
|
|
192
194
|
console.error(`${AMBER}▸${RESET} ${cliType}로 실행 중... (timeout: ${timeout}s)`);
|
|
193
195
|
const startTime = Date.now();
|
|
194
196
|
|
|
195
|
-
let stdout =
|
|
196
|
-
let stderr =
|
|
197
|
+
let stdout = '';
|
|
198
|
+
let stderr = '';
|
|
197
199
|
let exitCode = 0;
|
|
198
200
|
|
|
199
201
|
try {
|
|
200
202
|
stdout = execSync(cmd, {
|
|
201
|
-
encoding:
|
|
203
|
+
encoding: 'utf8',
|
|
202
204
|
timeout: (timeout + 30) * 1000,
|
|
203
205
|
maxBuffer: 10 * 1024 * 1024,
|
|
204
|
-
stdio: [
|
|
206
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
205
207
|
cwd: process.cwd(),
|
|
206
208
|
});
|
|
207
209
|
} catch (e) {
|
|
@@ -216,7 +218,7 @@ function runWithCli(cliType, prompt, timeout) {
|
|
|
216
218
|
try { unlinkSync(promptFile); } catch {}
|
|
217
219
|
|
|
218
220
|
// 실행 로그 기록
|
|
219
|
-
logExecution(cliType, exitCode, elapsed, timeout, stderr);
|
|
221
|
+
logExecution(cliType, exitCode, elapsed, timeout, stderr, runMode);
|
|
220
222
|
|
|
221
223
|
if (exitCode === 0 && stdout) {
|
|
222
224
|
return { success: true, output: stdout, cli: cliType, elapsed };
|
|
@@ -263,30 +265,30 @@ function cleanCodexOutput(raw) {
|
|
|
263
265
|
}
|
|
264
266
|
|
|
265
267
|
// ── 실행 로그 (cli-route.sh 호환) ──
|
|
266
|
-
function logExecution(cliType, exitCode, elapsed, timeout, stderr) {
|
|
268
|
+
function logExecution(cliType, exitCode, elapsed, timeout, stderr, runMode = 'fg') {
|
|
267
269
|
try {
|
|
268
270
|
const logDir = dirname(LOG_FILE);
|
|
269
271
|
if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true });
|
|
270
272
|
|
|
271
273
|
const ts = new Date().toISOString();
|
|
272
|
-
const status = exitCode === 0 ?
|
|
274
|
+
const status = exitCode === 0 ? 'success' : exitCode === 124 ? 'timeout' : 'failed';
|
|
273
275
|
const entry = JSON.stringify({
|
|
274
276
|
ts,
|
|
275
|
-
agent:
|
|
277
|
+
agent: 'notion-read',
|
|
276
278
|
cli: cliType,
|
|
277
|
-
effort: cliType ===
|
|
278
|
-
run_mode:
|
|
279
|
-
opus_oversight:
|
|
279
|
+
effort: cliType === 'codex' ? 'high' : cliType === 'claude' ? 'sonnet' : 'flash',
|
|
280
|
+
run_mode: runMode,
|
|
281
|
+
opus_oversight: 'false',
|
|
280
282
|
status,
|
|
281
283
|
exit_code: exitCode,
|
|
282
284
|
elapsed_sec: elapsed,
|
|
283
285
|
timeout_sec: timeout,
|
|
284
|
-
mcp_profile:
|
|
286
|
+
mcp_profile: runMode === 'delegate' ? 'notion-guest' : 'notion',
|
|
285
287
|
input_tokens: 0,
|
|
286
288
|
output_tokens: 0,
|
|
287
289
|
total_tokens: 0,
|
|
288
290
|
});
|
|
289
|
-
appendFileSync(LOG_FILE, entry +
|
|
291
|
+
appendFileSync(LOG_FILE, entry + '\n');
|
|
290
292
|
} catch {}
|
|
291
293
|
}
|
|
292
294
|
|
|
@@ -309,6 +311,7 @@ function main() {
|
|
|
309
311
|
--depth, -d <n> 중첩 블록 최대 깊이 (기본: 3)
|
|
310
312
|
--comments 블록/페이지 댓글 포함
|
|
311
313
|
--guest notion-guest 통합 사용
|
|
314
|
+
--delegate Claude 이관 모드 (notion-guest 우선, 파일 저장)
|
|
312
315
|
|
|
313
316
|
${BOLD}폴백 체인${RESET}
|
|
314
317
|
Codex(무료) → Gemini(무료) → Claude(최후) → 에러
|
|
@@ -318,6 +321,8 @@ function main() {
|
|
|
318
321
|
tfx notion-read abc123def456... --output page.md --comments
|
|
319
322
|
tfx notion-read abc123def456... --cli gemini --timeout 900
|
|
320
323
|
tfx notion-read abc123def456... --guest --comments
|
|
324
|
+
tfx notion-read abc123def456... --delegate
|
|
325
|
+
tfx notion-read abc123def456... --delegate --output .notion-cache/page.md
|
|
321
326
|
`);
|
|
322
327
|
return;
|
|
323
328
|
}
|
|
@@ -330,6 +335,7 @@ function main() {
|
|
|
330
335
|
let depth = 3;
|
|
331
336
|
let useGuest = false;
|
|
332
337
|
let includeComments = false;
|
|
338
|
+
let delegateMode = false;
|
|
333
339
|
|
|
334
340
|
for (let i = 1; i < args.length; i++) {
|
|
335
341
|
switch (args[i]) {
|
|
@@ -355,6 +361,9 @@ function main() {
|
|
|
355
361
|
case "--comments":
|
|
356
362
|
includeComments = true;
|
|
357
363
|
break;
|
|
364
|
+
case '--delegate':
|
|
365
|
+
delegateMode = true;
|
|
366
|
+
break;
|
|
358
367
|
}
|
|
359
368
|
}
|
|
360
369
|
|
|
@@ -368,7 +377,62 @@ function main() {
|
|
|
368
377
|
console.error(
|
|
369
378
|
`${AMBER}▸${RESET} 페이지: ${parsed.pageId}${parsed.blockId ? ` (블록: ${parsed.blockId})` : ""}`,
|
|
370
379
|
);
|
|
371
|
-
console.error(`${GRAY} 통합: ${useGuest ?
|
|
380
|
+
console.error(`${GRAY} 통합: ${delegateMode ? 'notion-guest(우선)' : useGuest ? 'notion-guest' : 'notion'} | 깊이: ${depth} | 댓글: ${includeComments ? 'O' : 'X'} | 타임아웃: ${timeout}s${RESET}`);
|
|
381
|
+
|
|
382
|
+
// 프롬프트 생성
|
|
383
|
+
const prompt = buildPrompt(parsed.pageId, parsed.blockId, depth, useGuest, includeComments);
|
|
384
|
+
// Claude 폴백용: notion/notion-guest 양쪽 시도 프롬프트
|
|
385
|
+
const claudePrompt = buildPrompt(parsed.pageId, parsed.blockId, depth, false, includeComments)
|
|
386
|
+
.replace(
|
|
387
|
+
'notion MCP 서버의 도구를 사용하라.',
|
|
388
|
+
'가능하면 notion-guest MCP 서버를 먼저 사용하라. 실패하면 notion MCP 서버를 사용하라.',
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
// delegate 모드: Claude 단독 + notion-guest 우선 + 파일 저장
|
|
392
|
+
if (delegateMode) {
|
|
393
|
+
console.error(`${AMBER}▸${RESET} delegate 모드 활성화: Claude로 notion-guest 우선 접근`);
|
|
394
|
+
|
|
395
|
+
const delegatePrompt = `${claudePrompt}
|
|
396
|
+
|
|
397
|
+
### delegate 모드 추가 지시
|
|
398
|
+
- notion-guest MCP 서버를 최우선으로 먼저 시도하라.
|
|
399
|
+
- notion-guest가 실패하거나 미구성일 때만 notion 서버로 폴백하라.
|
|
400
|
+
- 도구 호출 결과를 바탕으로 최종 마크다운만 출력하라.`;
|
|
401
|
+
|
|
402
|
+
const delegateResult = runWithCli('claude', delegatePrompt, timeout, 'delegate');
|
|
403
|
+
if (!delegateResult.success) {
|
|
404
|
+
console.error(`${RED}✗${RESET} delegate 모드 실패: ${delegateResult.error}`);
|
|
405
|
+
if (delegateResult.stderr) {
|
|
406
|
+
console.error(`${GRAY} stderr: ${delegateResult.stderr.slice(0, 250)}${RESET}`);
|
|
407
|
+
}
|
|
408
|
+
console.error(`${GRAY} 대안: --delegate 없이 실행해 기존 폴백 체인을 사용하세요.${RESET}`);
|
|
409
|
+
console.error(`${GRAY} 예: tfx notion-read ${parsed.pageId} --comments${RESET}`);
|
|
410
|
+
process.exit(1);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const delegateOutput = delegateResult.output.trim();
|
|
414
|
+
const isDelegateFailureOutput =
|
|
415
|
+
(delegateOutput.includes('조회 실패') || delegateOutput.includes('읽기 실패') || delegateOutput.includes('not_found')) &&
|
|
416
|
+
delegateOutput.length < 500;
|
|
417
|
+
|
|
418
|
+
if (delegateOutput.length <= 100 || isDelegateFailureOutput) {
|
|
419
|
+
console.error(`${RED}✗${RESET} delegate 모드 실패: Claude 결과가 비정상적입니다.`);
|
|
420
|
+
console.error(`${GRAY} 대안: --delegate 없이 실행해 Codex/Gemini/Claude 폴백 체인을 사용하세요.${RESET}`);
|
|
421
|
+
process.exit(1);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const delegateTarget = outputFile || join('.notion-cache', `${parsed.pageId}.md`);
|
|
425
|
+
const delegateDir = dirname(delegateTarget);
|
|
426
|
+
if (delegateDir && delegateDir !== '.' && !existsSync(delegateDir)) {
|
|
427
|
+
mkdirSync(delegateDir, { recursive: true });
|
|
428
|
+
}
|
|
429
|
+
writeFileSync(delegateTarget, delegateOutput, 'utf8');
|
|
430
|
+
|
|
431
|
+
const savedPath = resolve(delegateTarget);
|
|
432
|
+
console.error(`${GREEN}✓${RESET} delegate 결과 저장: ${savedPath}`);
|
|
433
|
+
console.error(`${GRAY} 후속 작업 참조 경로: ${savedPath}${RESET}`);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
372
436
|
|
|
373
437
|
// MCP 가용성 확인
|
|
374
438
|
const mcpAvail = getNotionMcpClis(useGuest);
|
|
@@ -400,15 +464,6 @@ function main() {
|
|
|
400
464
|
cliOrder.push("claude");
|
|
401
465
|
}
|
|
402
466
|
|
|
403
|
-
// 프롬프트 생성
|
|
404
|
-
const prompt = buildPrompt(parsed.pageId, parsed.blockId, depth, useGuest, includeComments);
|
|
405
|
-
// Claude 폴백용: notion/notion-guest 양쪽 시도 프롬프트
|
|
406
|
-
const claudePrompt = buildPrompt(parsed.pageId, parsed.blockId, depth, false, includeComments)
|
|
407
|
-
.replace(
|
|
408
|
-
"404 에러가 나면 notion-guest 서버로 재시도하라.",
|
|
409
|
-
"404 에러가 나면 반드시 notion-guest 서버로 재시도하라. notion, notion-guest, claude_ai_Notion 등 사용 가능한 모든 Notion MCP 서버를 시도하라.",
|
|
410
|
-
);
|
|
411
|
-
|
|
412
467
|
// 실행 + 폴백
|
|
413
468
|
let lastResult = null;
|
|
414
469
|
let notionAccessFailed = false;
|