triflux 10.3.4 → 10.7.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/LICENSE +21 -21
- package/bin/tfx-doctor-tui.mjs +1 -1
- package/bin/tfx-doctor.mjs +6 -1
- package/bin/tfx-profile.mjs +1 -1
- package/bin/tfx-setup-tui.mjs +1 -1
- package/bin/tfx-setup.mjs +6 -1
- package/bin/triflux.mjs +2396 -1140
- package/hooks/agent-route-guard.mjs +12 -8
- package/hooks/cross-review-tracker.mjs +21 -8
- package/hooks/error-context.mjs +19 -7
- package/hooks/hook-adaptive-collector.mjs +18 -16
- package/hooks/hook-manager.mjs +93 -32
- package/hooks/hook-orchestrator.mjs +108 -24
- package/hooks/hook-registry.json +11 -0
- package/hooks/keyword-rules.json +6 -10
- package/hooks/lib/resolve-root.mjs +1 -1
- package/hooks/mcp-config-watcher.mjs +6 -2
- package/hooks/pipeline-stop.mjs +3 -6
- package/hooks/safety-guard.mjs +99 -28
- package/hooks/session-start-fast.mjs +143 -0
- package/hooks/subagent-verifier.mjs +5 -4
- package/hub/account-broker.mjs +256 -60
- package/hub/adaptive-diagnostic.mjs +75 -48
- package/hub/adaptive-inject.mjs +95 -57
- package/hub/adaptive-memory.mjs +156 -42
- package/hub/adaptive.mjs +60 -31
- package/hub/assign-callbacks.mjs +67 -30
- package/hub/bridge.mjs +0 -1
- package/hub/cli-adapter-base.mjs +200 -48
- package/hub/codex-adapter.mjs +76 -96
- package/hub/codex-compat.mjs +3 -3
- package/hub/codex-preflight.mjs +63 -37
- package/hub/delegator/contracts.mjs +19 -23
- package/hub/delegator/index.mjs +3 -3
- package/hub/delegator/service.mjs +88 -64
- package/hub/delegator/tool-definitions.mjs +5 -5
- package/hub/fullcycle.mjs +33 -17
- package/hub/gemini-adapter.mjs +69 -94
- package/hub/hitl.mjs +89 -30
- package/hub/intent.mjs +161 -38
- package/hub/lib/cache-guard.mjs +43 -17
- package/hub/lib/mcp-response-cache.mjs +66 -32
- package/hub/lib/memory-store.mjs +285 -111
- package/hub/lib/path-utils.mjs +35 -37
- package/hub/lib/process-utils.mjs +106 -37
- package/hub/lib/spawn-trace.mjs +527 -0
- package/hub/lib/ssh-command.mjs +34 -4
- package/hub/lib/ssh-retry.mjs +5 -1
- package/hub/lib/uuidv7.mjs +4 -3
- package/hub/memory-doctor.mjs +266 -106
- 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 +57 -48
- package/hub/promote-penalties.mjs +25 -7
- package/hub/quality/deslop.mjs +70 -49
- package/hub/research.mjs +32 -25
- package/hub/router.mjs +240 -107
- package/hub/routing/complexity.mjs +132 -29
- package/hub/routing/index.mjs +17 -12
- package/hub/routing/q-learning.mjs +76 -28
- package/hub/server.mjs +4 -4
- package/hub/session-fingerprint.mjs +126 -60
- package/hub/state.mjs +84 -43
- package/hub/store-adapter.mjs +59 -26
- package/hub/store.mjs +356 -153
- package/hub/team/agent-map.json +22 -7
- package/hub/team/ansi.mjs +186 -122
- package/hub/team/backend.mjs +28 -10
- package/hub/team/cli/commands/attach.mjs +29 -9
- package/hub/team/cli/commands/control.mjs +29 -8
- package/hub/team/cli/commands/debug.mjs +32 -11
- package/hub/team/cli/commands/focus.mjs +38 -11
- package/hub/team/cli/commands/interrupt.mjs +18 -6
- package/hub/team/cli/commands/kill.mjs +16 -5
- package/hub/team/cli/commands/list.mjs +11 -4
- package/hub/team/cli/commands/send.mjs +19 -6
- package/hub/team/cli/commands/start/index.mjs +154 -31
- package/hub/team/cli/commands/start/parse-args.mjs +38 -11
- package/hub/team/cli/commands/start/start-headless.mjs +112 -36
- package/hub/team/cli/commands/start/start-in-process.mjs +12 -2
- package/hub/team/cli/commands/start/start-mux.mjs +70 -21
- package/hub/team/cli/commands/start/start-wt.mjs +29 -12
- package/hub/team/cli/commands/status.mjs +43 -14
- package/hub/team/cli/commands/stop.mjs +11 -4
- package/hub/team/cli/commands/task.mjs +8 -3
- package/hub/team/cli/commands/tasks.mjs +1 -1
- package/hub/team/cli/index.mjs +2 -2
- package/hub/team/cli/manifest.mjs +38 -8
- package/hub/team/cli/render.mjs +30 -8
- package/hub/team/cli/services/attach-fallback.mjs +31 -11
- package/hub/team/cli/services/hub-client.mjs +42 -14
- package/hub/team/cli/services/member-selector.mjs +11 -4
- package/hub/team/cli/services/native-control.mjs +48 -21
- package/hub/team/cli/services/runtime-mode.mjs +2 -1
- package/hub/team/cli/services/state-store.mjs +25 -8
- package/hub/team/cli/services/task-model.mjs +16 -6
- package/hub/team/conductor-mesh-bridge.mjs +24 -23
- package/hub/team/conductor.mjs +8 -4
- package/hub/team/dashboard-anchor.mjs +4 -5
- package/hub/team/dashboard-layout.mjs +3 -1
- package/hub/team/dashboard-open.mjs +41 -21
- package/hub/team/dashboard.mjs +76 -28
- package/hub/team/event-log.mjs +18 -10
- package/hub/team/handoff.mjs +31 -15
- package/hub/team/headless.mjs +2 -1
- package/hub/team/health-probe.mjs +69 -54
- package/hub/team/launcher-template.mjs +16 -13
- package/hub/team/native-supervisor.mjs +65 -21
- package/hub/team/native.mjs +74 -35
- package/hub/team/nativeProxy.mjs +184 -113
- package/hub/team/notify.mjs +119 -76
- package/hub/team/orchestrator.mjs +9 -4
- package/hub/team/pane.mjs +12 -7
- package/hub/team/process-cleanup.mjs +25 -16
- package/hub/team/psmux.mjs +491 -201
- package/hub/team/remote-probe.mjs +68 -52
- package/hub/team/remote-session.mjs +117 -59
- package/hub/team/remote-watcher.mjs +61 -33
- package/hub/team/routing.mjs +51 -25
- package/hub/team/runtime-strategy.mjs +3 -1
- package/hub/team/session.mjs +98 -34
- package/hub/team/staleState.mjs +72 -30
- package/hub/team/swarm-locks.mjs +15 -13
- package/hub/team/swarm-planner.mjs +32 -21
- package/hub/team/swarm-reconciler.mjs +48 -23
- package/hub/team/tui-lite.mjs +266 -68
- package/hub/team/tui-remote-adapter.mjs +14 -10
- package/hub/team/tui-viewer.mjs +99 -43
- package/hub/team/tui.mjs +708 -271
- package/hub/team/worktree-lifecycle.mjs +152 -58
- package/hub/team/wt-manager.mjs +24 -14
- package/hub/token-mode.mjs +71 -71
- package/hub/tray.mjs +66 -23
- package/hub/workers/claude-worker.mjs +162 -118
- package/hub/workers/codex-mcp.mjs +192 -141
- package/hub/workers/delegator-mcp.mjs +507 -333
- package/hub/workers/factory.mjs +8 -8
- package/hub/workers/gemini-worker.mjs +115 -84
- package/hub/workers/interface.mjs +6 -1
- package/hub/workers/worker-utils.mjs +21 -14
- package/hud/colors.mjs +27 -9
- package/hud/constants.mjs +162 -26
- package/hud/context-monitor.mjs +82 -41
- package/hud/hud-qos-status.mjs +129 -49
- package/hud/mission-board.mjs +6 -3
- package/hud/providers/claude.mjs +226 -115
- package/hud/providers/codex.mjs +62 -22
- package/hud/providers/gemini.mjs +168 -56
- package/hud/renderers.mjs +384 -119
- package/hud/terminal.mjs +101 -31
- package/hud/utils.mjs +78 -38
- package/mesh/index.mjs +11 -5
- package/mesh/mesh-budget.mjs +18 -9
- package/mesh/mesh-heartbeat.mjs +1 -1
- package/mesh/mesh-queue.mjs +3 -5
- package/mesh/mesh-router.mjs +5 -4
- package/package.json +2 -1
- package/scripts/__tests__/gen-skill-docs.test.mjs +36 -7
- package/scripts/__tests__/keyword-detector.test.mjs +77 -28
- package/scripts/__tests__/mcp-guard-engine.test.mjs +58 -20
- package/scripts/__tests__/remote-spawn-transfer.test.mjs +30 -19
- package/scripts/__tests__/remote-spawn.test.mjs +10 -4
- package/scripts/__tests__/session-start-fast.test.mjs +36 -0
- package/scripts/__tests__/skill-template.test.mjs +98 -50
- package/scripts/__tests__/smoke.test.mjs +1 -1
- package/scripts/__tests__/spawn-trace.test.mjs +102 -0
- package/scripts/__tests__/tfx-doctor-diagnose.test.mjs +48 -0
- package/scripts/cache-doctor.mjs +11 -4
- package/scripts/cache-warmup.mjs +96 -37
- package/scripts/claudemd-sync.mjs +27 -17
- package/scripts/codex-gateway-preflight.mjs +52 -37
- package/scripts/codex-mcp-gateway-sync.mjs +59 -39
- package/scripts/completions/tfx.bash +47 -47
- package/scripts/completions/tfx.fish +44 -44
- package/scripts/completions/tfx.zsh +83 -83
- package/scripts/config-audit.mjs +232 -0
- package/scripts/convert-to-tmpl.mjs +54 -0
- package/scripts/cross-review-gate.mjs +35 -12
- package/scripts/cross-review-tracker.mjs +21 -8
- package/scripts/demo.mjs +35 -17
- package/scripts/doctor-diagnose.mjs +284 -0
- package/scripts/gen-skill-docs.mjs +7 -2
- package/scripts/gen-skill-manifest.mjs +2 -1
- package/scripts/headless-guard.mjs +86 -48
- package/scripts/hub-ensure.mjs +45 -26
- package/scripts/keyword-detector.mjs +41 -20
- package/scripts/keyword-rules-expander.mjs +47 -30
- package/scripts/lib/claudemd-scanner.mjs +6 -1
- package/scripts/lib/context.mjs +3 -3
- package/scripts/lib/cross-review-utils.mjs +6 -3
- package/scripts/lib/env-probe.mjs +47 -28
- 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/keyword-rules.mjs +43 -19
- package/scripts/lib/logger.mjs +24 -24
- package/scripts/lib/mcp-filter.mjs +377 -239
- package/scripts/lib/mcp-guard-engine.mjs +194 -79
- package/scripts/lib/mcp-manifest.mjs +23 -13
- package/scripts/lib/mcp-server-catalog.mjs +300 -63
- package/scripts/lib/psmux-info.mjs +11 -6
- package/scripts/lib/remote-spawn-transfer.mjs +44 -14
- package/scripts/lib/skill-template.mjs +30 -7
- package/scripts/mcp-check.mjs +58 -39
- package/scripts/mcp-gateway-config.mjs +83 -39
- package/scripts/mcp-gateway-ensure.mjs +43 -35
- package/scripts/mcp-gateway-integration-test.mjs +70 -58
- package/scripts/mcp-gateway-start.mjs +126 -60
- package/scripts/mcp-gateway-verify.mjs +24 -22
- package/scripts/mcp-safety-guard.mjs +44 -11
- package/scripts/notion-read.mjs +199 -84
- package/scripts/pack.mjs +94 -89
- package/scripts/preflight-cache.mjs +27 -10
- package/scripts/preinstall.mjs +42 -13
- package/scripts/remote-spawn.mjs +309 -94
- package/scripts/run.cjs +8 -5
- package/scripts/session-spawn-helper.mjs +130 -39
- package/scripts/session-stale-cleanup.mjs +123 -0
- package/scripts/setup.mjs +941 -492
- package/scripts/test-lock.mjs +20 -7
- package/scripts/test-tfx-route-no-claude-native.mjs +16 -12
- package/scripts/tfx-batch-stats.mjs +32 -11
- package/scripts/tfx-gate-activate.mjs +11 -4
- package/scripts/tfx-route-post.mjs +87 -20
- package/scripts/tfx-route-worker.mjs +57 -51
- package/scripts/tfx-route.sh +41 -124
- package/scripts/tmp-cleanup.mjs +21 -7
- package/scripts/token-snapshot.mjs +204 -85
- package/skills/.omc/state/agent-replay-8f0e10a9-9693-4410-96f5-a6b07e8ed995.jsonl +1 -0
- package/skills/.omc/state/idle-notif-cooldown.json +3 -0
- package/skills/.omc/state/last-tool-error.json +7 -0
- package/skills/.omc/state/subagent-tracking.json +7 -0
- package/skills/_templates/base.md +1 -6
- package/skills/merge-worktree/SKILL.md.tmpl +144 -0
- package/skills/shared/telemetry-segment.md +6 -0
- package/skills/star-prompt/SKILL.md.tmpl +222 -0
- package/skills/tfx-analysis/SKILL.md.tmpl +107 -0
- package/skills/tfx-analysis/skill.json +1 -6
- package/skills/tfx-auto/SKILL.md +1 -0
- package/skills/tfx-auto-codex/SKILL.md.tmpl +106 -0
- package/skills/tfx-auto-codex/skill.json +1 -3
- package/skills/tfx-autopilot/SKILL.md.tmpl +116 -0
- package/skills/tfx-autopilot/skill.json +1 -5
- package/skills/tfx-autoresearch/SKILL.md.tmpl +136 -0
- package/skills/tfx-autoroute/SKILL.md.tmpl +189 -0
- package/skills/tfx-autoroute/skill.json +1 -7
- package/skills/tfx-codex/SKILL.md +1 -0
- package/skills/tfx-codex/skill.json +1 -3
- package/skills/tfx-codex-swarm/SKILL.md.tmpl +16 -0
- package/skills/tfx-codex-swarm/evals/evals.json +1 -1
- package/skills/tfx-codex-swarm/skill.json +1 -4
- package/skills/tfx-codex-swarm-workspace/iteration-1/benchmark.json +54 -12
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/grading.json +35 -7
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/grading.json +35 -7
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/grading.json +25 -5
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/grading.json +25 -5
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/grading.json +20 -4
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/grading.json +16 -4
- package/skills/tfx-consensus/SKILL.md.tmpl +146 -0
- package/skills/tfx-debate/SKILL.md.tmpl +192 -0
- package/skills/tfx-debate/skill.json +1 -7
- package/skills/tfx-deep-analysis/SKILL.md.tmpl +228 -0
- package/skills/tfx-deep-analysis/skill.json +1 -5
- package/skills/tfx-deep-interview/SKILL.md.tmpl +203 -0
- package/skills/tfx-deep-plan/SKILL.md.tmpl +282 -0
- package/skills/tfx-deep-qa/SKILL.md.tmpl +165 -0
- package/skills/tfx-deep-qa/skill.json +1 -6
- package/skills/tfx-deep-research/SKILL.md.tmpl +217 -0
- package/skills/tfx-deep-review/SKILL.md.tmpl +179 -0
- package/skills/tfx-doctor/SKILL.md +21 -0
- package/skills/tfx-doctor/SKILL.md.tmpl +172 -0
- package/skills/tfx-doctor/skill.json +1 -3
- package/skills/tfx-find/SKILL.md +1 -0
- package/skills/tfx-forge/SKILL.md.tmpl +187 -0
- package/skills/tfx-fullcycle/SKILL.md.tmpl +286 -0
- package/skills/tfx-fullcycle/skill.json +1 -6
- package/skills/tfx-gemini/SKILL.md.tmpl +91 -0
- package/skills/tfx-gemini/skill.json +1 -3
- package/skills/tfx-hooks/SKILL.md.tmpl +216 -0
- package/skills/tfx-hooks/skill.json +1 -3
- package/skills/tfx-hub/SKILL.md.tmpl +212 -0
- package/skills/tfx-hub/skill.json +1 -3
- package/skills/tfx-index/SKILL.md +1 -0
- package/skills/tfx-index/skill.json +1 -6
- package/skills/tfx-interview/SKILL.md.tmpl +285 -0
- package/skills/tfx-multi/SKILL.md.tmpl +183 -0
- package/skills/tfx-multi/skill.json +1 -3
- package/skills/tfx-panel/SKILL.md.tmpl +189 -0
- package/skills/tfx-panel/skill.json +1 -7
- package/skills/tfx-persist/SKILL.md.tmpl +270 -0
- package/skills/tfx-persist/skill.json +1 -7
- package/skills/tfx-plan/SKILL.md +1 -0
- package/skills/tfx-plan/skill.json +1 -6
- package/skills/tfx-profile/SKILL.md.tmpl +239 -0
- package/skills/tfx-profile/skill.json +1 -3
- package/skills/tfx-prune/SKILL.md.tmpl +200 -0
- package/skills/tfx-prune/skill.json +1 -7
- package/skills/tfx-psmux-rules/SKILL.md.tmpl +326 -0
- package/skills/tfx-psmux-rules/skill.json +1 -4
- package/skills/tfx-qa/SKILL.md +1 -0
- package/skills/tfx-qa/skill.json +1 -6
- package/skills/tfx-ralph/SKILL.md.tmpl +28 -0
- package/skills/tfx-ralph/skill.json +1 -4
- package/skills/tfx-remote-setup/SKILL.md.tmpl +576 -0
- package/skills/tfx-remote-setup/skill.json +1 -3
- package/skills/tfx-remote-spawn/SKILL.md.tmpl +263 -0
- package/skills/tfx-remote-spawn/references/hosts.json +16 -0
- package/skills/tfx-remote-spawn/skill.json +1 -4
- package/skills/tfx-research/SKILL.md +1 -0
- package/skills/tfx-review/SKILL.md +1 -0
- package/skills/tfx-review/skill.json +1 -6
- package/skills/tfx-setup/SKILL.md.tmpl +504 -0
- package/skills/tfx-setup/skill.json +1 -3
- package/skills/tfx-swarm/SKILL.md +22 -0
- package/skills/tfx-swarm/SKILL.md.tmpl +218 -0
- package/tui/codex-profile.mjs +88 -33
- package/tui/core.mjs +45 -15
- package/tui/doctor.mjs +75 -28
- package/tui/gemini-profile.mjs +74 -29
- package/tui/monitor-data.mjs +8 -4
- package/tui/monitor.mjs +71 -27
- package/tui/setup.mjs +133 -42
|
@@ -29,7 +29,8 @@ function main() {
|
|
|
29
29
|
|
|
30
30
|
const agentType = input.agent_type || input.subagent_type || "unknown";
|
|
31
31
|
const result = input.tool_output || input.result || "";
|
|
32
|
-
const resultStr =
|
|
32
|
+
const resultStr =
|
|
33
|
+
typeof result === "string" ? result : JSON.stringify(result);
|
|
33
34
|
|
|
34
35
|
const issues = [];
|
|
35
36
|
|
|
@@ -37,7 +38,7 @@ function main() {
|
|
|
37
38
|
if (!resultStr.trim() || resultStr.trim().length < 20) {
|
|
38
39
|
issues.push(
|
|
39
40
|
`서브에이전트(${agentType})가 거의 빈 결과를 반환했습니다. ` +
|
|
40
|
-
"프롬프트를 더 구체적으로 작성하거나, 다른 subagent_type을 시도하세요."
|
|
41
|
+
"프롬프트를 더 구체적으로 작성하거나, 다른 subagent_type을 시도하세요.",
|
|
41
42
|
);
|
|
42
43
|
}
|
|
43
44
|
|
|
@@ -50,7 +51,7 @@ function main() {
|
|
|
50
51
|
if (hasError && resultStr.length > 50) {
|
|
51
52
|
issues.push(
|
|
52
53
|
`서브에이전트(${agentType}) 결과에 에러 신호가 감지되었습니다. ` +
|
|
53
|
-
"결과를 검토하고, 필요 시 다른 접근 방식을 사용하세요."
|
|
54
|
+
"결과를 검토하고, 필요 시 다른 접근 방식을 사용하세요.",
|
|
54
55
|
);
|
|
55
56
|
}
|
|
56
57
|
|
|
@@ -58,7 +59,7 @@ function main() {
|
|
|
58
59
|
if (resultStr.length > 15000) {
|
|
59
60
|
issues.push(
|
|
60
61
|
`서브에이전트(${agentType}) 결과가 ${Math.round(resultStr.length / 1000)}K 자입니다. ` +
|
|
61
|
-
"핵심만 추출하여 컨텍스트 윈도우를 절약하세요."
|
|
62
|
+
"핵심만 추출하여 컨텍스트 윈도우를 절약하세요.",
|
|
62
63
|
);
|
|
63
64
|
}
|
|
64
65
|
|
package/hub/account-broker.mjs
CHANGED
|
@@ -1,35 +1,45 @@
|
|
|
1
1
|
// hub/account-broker.mjs — Multi-account CLI pool broker
|
|
2
|
-
// Manages lease/release/cooldown for Codex and Gemini accounts.
|
|
2
|
+
// Manages lease/release/cooldown/circuit-breaker for Codex and Gemini accounts.
|
|
3
|
+
// Per-account circuit breaker: one bad account does not block others.
|
|
3
4
|
// Singleton export. All state changes create new objects (immutable pattern).
|
|
4
5
|
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { homedir } from
|
|
8
|
-
import
|
|
6
|
+
import { EventEmitter } from "node:events";
|
|
7
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { join, sep } from "node:path";
|
|
10
|
+
import * as z from "zod";
|
|
9
11
|
|
|
10
12
|
// ── Zod schema ───────────────────────────────────────────────────
|
|
11
13
|
|
|
12
|
-
const AccountSchema = z
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
14
|
+
const AccountSchema = z
|
|
15
|
+
.object({
|
|
16
|
+
id: z.string().min(1),
|
|
17
|
+
mode: z.enum(["profile", "env", "auth"]),
|
|
18
|
+
profile: z.string().optional(),
|
|
19
|
+
env: z.record(z.string(), z.string()).optional(),
|
|
20
|
+
authFile: z.string().optional(),
|
|
21
|
+
host: z.string().min(1).optional(),
|
|
22
|
+
tier: z
|
|
23
|
+
.enum(["pro", "plus", "free", "unknown"])
|
|
24
|
+
.optional()
|
|
25
|
+
.default("unknown"),
|
|
26
|
+
})
|
|
27
|
+
.superRefine((val, ctx) => {
|
|
28
|
+
if (val.mode === "auth" && !val.authFile) {
|
|
29
|
+
ctx.addIssue({
|
|
30
|
+
code: z.ZodIssueCode.custom,
|
|
31
|
+
message: 'authFile is required when mode is "auth"',
|
|
32
|
+
path: ["authFile"],
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
});
|
|
28
36
|
|
|
29
37
|
const ConfigSchema = z.object({
|
|
30
|
-
defaults: z
|
|
31
|
-
|
|
32
|
-
|
|
38
|
+
defaults: z
|
|
39
|
+
.object({
|
|
40
|
+
cooldownMs: z.number().int().positive().optional(),
|
|
41
|
+
})
|
|
42
|
+
.optional(),
|
|
33
43
|
codex: z.array(AccountSchema).optional(),
|
|
34
44
|
gemini: z.array(AccountSchema).optional(),
|
|
35
45
|
});
|
|
@@ -37,7 +47,9 @@ const ConfigSchema = z.object({
|
|
|
37
47
|
const DEFAULT_COOLDOWN_MS = 300_000; // 5 minutes
|
|
38
48
|
const TIER_PRIORITY = { pro: 0, plus: 1, unknown: 2, free: 3 };
|
|
39
49
|
const LEASE_TTL_MS = 30 * 60 * 1000; // 30 minutes
|
|
40
|
-
const
|
|
50
|
+
const CIRCUIT_WINDOW_MS = 10 * 60_000; // 10 minutes
|
|
51
|
+
const CIRCUIT_MAX_FAILURES = 3;
|
|
52
|
+
const AUTH_BASE_PATH = join(homedir(), ".claude", "cache", "tfx-hub");
|
|
41
53
|
|
|
42
54
|
// ── env var resolution ───────────────────────────────────────────
|
|
43
55
|
|
|
@@ -45,9 +57,9 @@ function resolveEnvValues(env) {
|
|
|
45
57
|
if (!env) return undefined;
|
|
46
58
|
const resolved = {};
|
|
47
59
|
for (const [key, value] of Object.entries(env)) {
|
|
48
|
-
if (typeof value ===
|
|
60
|
+
if (typeof value === "string" && value.startsWith("$")) {
|
|
49
61
|
const varName = value.slice(1);
|
|
50
|
-
resolved[key] = process.env[varName] ??
|
|
62
|
+
resolved[key] = process.env[varName] ?? "";
|
|
51
63
|
} else {
|
|
52
64
|
resolved[key] = value;
|
|
53
65
|
}
|
|
@@ -55,14 +67,24 @@ function resolveEnvValues(env) {
|
|
|
55
67
|
return resolved;
|
|
56
68
|
}
|
|
57
69
|
|
|
70
|
+
function isRemoteAccount(account) {
|
|
71
|
+
return Boolean(account.host);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function getRemainingLeaseMs(account, now) {
|
|
75
|
+
if (!account.busy || account.leasedAt === null) return 0;
|
|
76
|
+
return Math.max(0, LEASE_TTL_MS - (now - account.leasedAt));
|
|
77
|
+
}
|
|
78
|
+
|
|
58
79
|
// ── AccountBroker ────────────────────────────────────────────────
|
|
59
80
|
|
|
60
|
-
class AccountBroker {
|
|
81
|
+
class AccountBroker extends EventEmitter {
|
|
61
82
|
#config;
|
|
62
83
|
#state; // Map<accountId, accountState>
|
|
63
84
|
#roundRobinIndex; // Map<provider, number>
|
|
64
85
|
|
|
65
86
|
constructor(config) {
|
|
87
|
+
super();
|
|
66
88
|
const parsed = ConfigSchema.parse(config);
|
|
67
89
|
this.#config = parsed;
|
|
68
90
|
|
|
@@ -70,8 +92,8 @@ class AccountBroker {
|
|
|
70
92
|
this.#roundRobinIndex = new Map();
|
|
71
93
|
|
|
72
94
|
const allAccounts = [
|
|
73
|
-
...(parsed.codex || []).map((a) => ({ ...a, provider:
|
|
74
|
-
...(parsed.gemini || []).map((a) => ({ ...a, provider:
|
|
95
|
+
...(parsed.codex || []).map((a) => ({ ...a, provider: "codex" })),
|
|
96
|
+
...(parsed.gemini || []).map((a) => ({ ...a, provider: "gemini" })),
|
|
75
97
|
];
|
|
76
98
|
|
|
77
99
|
for (const account of allAccounts) {
|
|
@@ -82,22 +104,73 @@ class AccountBroker {
|
|
|
82
104
|
profile: account.profile,
|
|
83
105
|
env: account.env,
|
|
84
106
|
authFile: account.authFile,
|
|
85
|
-
|
|
107
|
+
host: account.host,
|
|
108
|
+
tier: account.tier ?? "unknown",
|
|
86
109
|
busy: false,
|
|
87
110
|
leasedAt: null,
|
|
88
111
|
cooldownUntil: 0,
|
|
89
|
-
|
|
112
|
+
// per-account circuit breaker state
|
|
113
|
+
failureTimestamps: [], // timestamp array for window-based decay
|
|
114
|
+
circuitOpenedAt: 0,
|
|
115
|
+
circuitTrialInFlight: false,
|
|
90
116
|
lastUsedAt: 0,
|
|
91
117
|
totalSessions: 0,
|
|
92
118
|
});
|
|
93
119
|
}
|
|
94
120
|
}
|
|
95
121
|
|
|
122
|
+
// ── per-account circuit breaker ─────────────────────────────────
|
|
123
|
+
|
|
124
|
+
#getCircuitState(acct, now) {
|
|
125
|
+
const validFailures = acct.failureTimestamps.filter(
|
|
126
|
+
(ts) => now - ts < CIRCUIT_WINDOW_MS,
|
|
127
|
+
);
|
|
128
|
+
const withinWindow =
|
|
129
|
+
acct.circuitOpenedAt && now - acct.circuitOpenedAt < CIRCUIT_WINDOW_MS;
|
|
130
|
+
if (withinWindow) return { state: "open", failures: validFailures };
|
|
131
|
+
if (acct.circuitOpenedAt)
|
|
132
|
+
return { state: "half-open", failures: validFailures };
|
|
133
|
+
return { state: "closed", failures: validFailures };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
#isCircuitBlocked(acct, now) {
|
|
137
|
+
const circuit = this.#getCircuitState(acct, now);
|
|
138
|
+
if (circuit.state === "open") return true;
|
|
139
|
+
if (circuit.state === "half-open" && acct.circuitTrialInFlight) return true;
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
#recordCircuitFailure(acct, isHalfOpen, now) {
|
|
144
|
+
const validFailures = [
|
|
145
|
+
...acct.failureTimestamps.filter((ts) => now - ts < CIRCUIT_WINDOW_MS),
|
|
146
|
+
now,
|
|
147
|
+
];
|
|
148
|
+
const shouldOpen =
|
|
149
|
+
isHalfOpen || validFailures.length >= CIRCUIT_MAX_FAILURES;
|
|
150
|
+
return {
|
|
151
|
+
failureTimestamps: validFailures,
|
|
152
|
+
circuitOpenedAt: shouldOpen ? now : acct.circuitOpenedAt,
|
|
153
|
+
circuitTrialInFlight: false,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
#resetCircuit() {
|
|
158
|
+
return {
|
|
159
|
+
failureTimestamps: [],
|
|
160
|
+
circuitOpenedAt: 0,
|
|
161
|
+
circuitTrialInFlight: false,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
96
165
|
// ── lease TTL pruning ──────────────────────────────────────────
|
|
97
166
|
|
|
98
167
|
#pruneExpiredLeases(now) {
|
|
99
168
|
for (const [id, acct] of this.#state) {
|
|
100
|
-
if (
|
|
169
|
+
if (
|
|
170
|
+
acct.busy &&
|
|
171
|
+
acct.leasedAt !== null &&
|
|
172
|
+
now - acct.leasedAt > LEASE_TTL_MS
|
|
173
|
+
) {
|
|
101
174
|
this.#state.set(id, { ...acct, busy: false, leasedAt: null });
|
|
102
175
|
}
|
|
103
176
|
}
|
|
@@ -105,16 +178,36 @@ class AccountBroker {
|
|
|
105
178
|
|
|
106
179
|
// ── lease ─────────────────────────────────────────────────────
|
|
107
180
|
|
|
108
|
-
lease({ provider }) {
|
|
181
|
+
lease({ provider, remote = false } = {}) {
|
|
109
182
|
const now = Date.now();
|
|
110
183
|
this.#pruneExpiredLeases(now);
|
|
111
184
|
|
|
112
|
-
const
|
|
185
|
+
const wantsRemote = remote === true;
|
|
186
|
+
const accounts = [...this.#state.values()].filter(
|
|
187
|
+
(a) => a.provider === provider && isRemoteAccount(a) === wantsRemote,
|
|
188
|
+
);
|
|
113
189
|
if (!accounts.length) return null;
|
|
114
190
|
|
|
115
|
-
//
|
|
116
|
-
const available = accounts.filter(
|
|
117
|
-
|
|
191
|
+
// filter: not busy, not in cooldown, circuit not blocked
|
|
192
|
+
const available = accounts.filter(
|
|
193
|
+
(a) =>
|
|
194
|
+
!a.busy && a.cooldownUntil <= now && !this.#isCircuitBlocked(a, now),
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
if (!available.length) {
|
|
198
|
+
// check if any accounts exist but all are blocked by circuit
|
|
199
|
+
const circuitBlocked = accounts.filter(
|
|
200
|
+
(a) =>
|
|
201
|
+
!a.busy && a.cooldownUntil <= now && this.#isCircuitBlocked(a, now),
|
|
202
|
+
);
|
|
203
|
+
if (circuitBlocked.length) {
|
|
204
|
+
this.emit("noAvailableAccounts", {
|
|
205
|
+
provider,
|
|
206
|
+
count: circuitBlocked.length,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
118
211
|
|
|
119
212
|
// sort by tier priority; stable sort preserves original order within same priority
|
|
120
213
|
const sorted = [...available].sort(
|
|
@@ -125,8 +218,23 @@ class AccountBroker {
|
|
|
125
218
|
const bestTier = sorted[0].tier;
|
|
126
219
|
const sameTierAccounts = sorted.filter((a) => a.tier === bestTier);
|
|
127
220
|
|
|
221
|
+
// detect tier fallback
|
|
222
|
+
const highestTier = accounts.reduce(
|
|
223
|
+
(best, a) => Math.min(best, TIER_PRIORITY[a.tier] ?? 2),
|
|
224
|
+
Infinity,
|
|
225
|
+
);
|
|
226
|
+
if ((TIER_PRIORITY[bestTier] ?? 2) > highestTier) {
|
|
227
|
+
this.emit("tierFallback", {
|
|
228
|
+
provider,
|
|
229
|
+
from: Object.entries(TIER_PRIORITY).find(
|
|
230
|
+
([, v]) => v === highestTier,
|
|
231
|
+
)?.[0],
|
|
232
|
+
to: bestTier,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
128
236
|
// use a per-provider+tier round-robin key to distribute within the tier
|
|
129
|
-
const rrKey = `${provider}:${bestTier}`;
|
|
237
|
+
const rrKey = `${provider}:${bestTier}:${wantsRemote ? "remote" : "local"}`;
|
|
130
238
|
const rrCurrent = this.#roundRobinIndex.get(rrKey) ?? 0;
|
|
131
239
|
const tierCount = sameTierAccounts.length;
|
|
132
240
|
const idx = rrCurrent % tierCount;
|
|
@@ -135,6 +243,10 @@ class AccountBroker {
|
|
|
135
243
|
// advance round-robin index for this tier
|
|
136
244
|
this.#roundRobinIndex.set(rrKey, (idx + 1) % tierCount);
|
|
137
245
|
|
|
246
|
+
// mark half-open trial if applicable
|
|
247
|
+
const circuit = this.#getCircuitState(acct, now);
|
|
248
|
+
const isHalfOpen = circuit.state === "half-open";
|
|
249
|
+
|
|
138
250
|
// update state (immutable)
|
|
139
251
|
this.#state.set(acct.id, {
|
|
140
252
|
...acct,
|
|
@@ -142,14 +254,45 @@ class AccountBroker {
|
|
|
142
254
|
leasedAt: now,
|
|
143
255
|
lastUsedAt: now,
|
|
144
256
|
totalSessions: acct.totalSessions + 1,
|
|
257
|
+
circuitTrialInFlight: isHalfOpen ? true : acct.circuitTrialInFlight,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
this.emit("lease", {
|
|
261
|
+
id: acct.id,
|
|
262
|
+
provider,
|
|
263
|
+
tier: acct.tier,
|
|
264
|
+
halfOpen: isHalfOpen,
|
|
145
265
|
});
|
|
146
266
|
|
|
267
|
+
// path traversal guard for authFile
|
|
268
|
+
let authFile;
|
|
269
|
+
if (acct.mode === "auth") {
|
|
270
|
+
const resolved = join(AUTH_BASE_PATH, acct.authFile);
|
|
271
|
+
if (!resolved.startsWith(AUTH_BASE_PATH + sep)) {
|
|
272
|
+
this.emit("securityViolation", {
|
|
273
|
+
id: acct.id,
|
|
274
|
+
authFile: acct.authFile,
|
|
275
|
+
});
|
|
276
|
+
// undo the lease — path traversal blocked
|
|
277
|
+
this.#state.set(acct.id, {
|
|
278
|
+
...this.#state.get(acct.id),
|
|
279
|
+
busy: false,
|
|
280
|
+
leasedAt: null,
|
|
281
|
+
});
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
authFile = resolved;
|
|
285
|
+
}
|
|
286
|
+
|
|
147
287
|
return {
|
|
148
288
|
id: acct.id,
|
|
149
289
|
mode: acct.mode,
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
290
|
+
remote: isRemoteAccount(acct),
|
|
291
|
+
host: acct.host,
|
|
292
|
+
halfOpen: isHalfOpen,
|
|
293
|
+
profile: acct.mode === "profile" ? acct.profile : undefined,
|
|
294
|
+
env: acct.mode === "env" ? resolveEnvValues(acct.env) : undefined,
|
|
295
|
+
authFile,
|
|
153
296
|
};
|
|
154
297
|
}
|
|
155
298
|
|
|
@@ -157,26 +300,45 @@ class AccountBroker {
|
|
|
157
300
|
|
|
158
301
|
release(accountId, result) {
|
|
159
302
|
const acct = this.#state.get(accountId);
|
|
160
|
-
if (!acct) return;
|
|
303
|
+
if (!acct?.busy) return;
|
|
161
304
|
|
|
305
|
+
const now = Date.now();
|
|
162
306
|
const ok = result?.ok === true;
|
|
163
|
-
const
|
|
307
|
+
const circuit = this.#getCircuitState(acct, now);
|
|
308
|
+
const isHalfOpen = circuit.state === "half-open";
|
|
309
|
+
|
|
310
|
+
let circuitUpdate;
|
|
311
|
+
if (ok) {
|
|
312
|
+
circuitUpdate = this.#resetCircuit();
|
|
313
|
+
if (isHalfOpen) {
|
|
314
|
+
this.emit("circuitClose", { id: accountId });
|
|
315
|
+
}
|
|
316
|
+
} else {
|
|
317
|
+
circuitUpdate = this.#recordCircuitFailure(acct, isHalfOpen, now);
|
|
318
|
+
if (circuitUpdate.circuitOpenedAt !== acct.circuitOpenedAt) {
|
|
319
|
+
this.emit("circuitOpen", {
|
|
320
|
+
id: accountId,
|
|
321
|
+
failures: circuitUpdate.failureTimestamps.length,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
164
326
|
const cooldownMs = this.#config.defaults?.cooldownMs ?? DEFAULT_COOLDOWN_MS;
|
|
165
327
|
|
|
328
|
+
// rate-limit style cooldown: if circuit just opened, also set cooldown
|
|
329
|
+
const shouldCooldown =
|
|
330
|
+
!ok && circuitUpdate.circuitOpenedAt !== acct.circuitOpenedAt;
|
|
331
|
+
|
|
166
332
|
const updated = {
|
|
167
333
|
...acct,
|
|
168
334
|
busy: false,
|
|
169
335
|
leasedAt: null,
|
|
170
|
-
|
|
336
|
+
...circuitUpdate,
|
|
337
|
+
cooldownUntil: shouldCooldown ? now + cooldownMs : acct.cooldownUntil,
|
|
171
338
|
};
|
|
172
339
|
|
|
173
|
-
// consecutive failure guard: 3+ failures → auto-cooldown
|
|
174
|
-
if (newFailures >= 3) {
|
|
175
|
-
updated.cooldownUntil = Date.now() + cooldownMs;
|
|
176
|
-
updated.failures = 0; // reset after cooldown triggered
|
|
177
|
-
}
|
|
178
|
-
|
|
179
340
|
this.#state.set(accountId, updated);
|
|
341
|
+
this.emit("release", { id: accountId, ok });
|
|
180
342
|
}
|
|
181
343
|
|
|
182
344
|
// ── markRateLimited ───────────────────────────────────────────
|
|
@@ -195,7 +357,14 @@ class AccountBroker {
|
|
|
195
357
|
// ── snapshot ──────────────────────────────────────────────────
|
|
196
358
|
|
|
197
359
|
snapshot() {
|
|
198
|
-
|
|
360
|
+
const now = Date.now();
|
|
361
|
+
this.#pruneExpiredLeases(now);
|
|
362
|
+
return [...this.#state.values()].map((acct) => ({
|
|
363
|
+
...acct,
|
|
364
|
+
failureTimestamps: [...acct.failureTimestamps],
|
|
365
|
+
remainingMs: getRemainingLeaseMs(acct, now),
|
|
366
|
+
circuitState: this.#getCircuitState(acct, now).state,
|
|
367
|
+
}));
|
|
199
368
|
}
|
|
200
369
|
|
|
201
370
|
// ── nextAvailableEta ──────────────────────────────────────────
|
|
@@ -204,7 +373,9 @@ class AccountBroker {
|
|
|
204
373
|
const now = Date.now();
|
|
205
374
|
this.#pruneExpiredLeases(now);
|
|
206
375
|
|
|
207
|
-
const accounts = [...this.#state.values()].filter(
|
|
376
|
+
const accounts = [...this.#state.values()].filter(
|
|
377
|
+
(a) => a.provider === provider,
|
|
378
|
+
);
|
|
208
379
|
if (!accounts.length) return null;
|
|
209
380
|
|
|
210
381
|
// find minimum cooldownUntil among accounts that are in cooldown or busy
|
|
@@ -214,7 +385,9 @@ class AccountBroker {
|
|
|
214
385
|
// this account is available now — no ETA needed
|
|
215
386
|
return null;
|
|
216
387
|
}
|
|
217
|
-
const eta = acct.busy
|
|
388
|
+
const eta = acct.busy
|
|
389
|
+
? (acct.leasedAt ?? now) + LEASE_TTL_MS
|
|
390
|
+
: acct.cooldownUntil;
|
|
218
391
|
if (earliest === null || eta < earliest) {
|
|
219
392
|
earliest = eta;
|
|
220
393
|
}
|
|
@@ -226,11 +399,21 @@ class AccountBroker {
|
|
|
226
399
|
// ── Config loader ────────────────────────────────────────────────
|
|
227
400
|
|
|
228
401
|
function loadConfig() {
|
|
229
|
-
const configPath = join(
|
|
402
|
+
const configPath = join(
|
|
403
|
+
homedir(),
|
|
404
|
+
".claude",
|
|
405
|
+
"cache",
|
|
406
|
+
"tfx-hub",
|
|
407
|
+
"accounts.json",
|
|
408
|
+
);
|
|
230
409
|
if (!existsSync(configPath)) return null;
|
|
231
410
|
try {
|
|
232
|
-
return JSON.parse(readFileSync(configPath,
|
|
233
|
-
} catch {
|
|
411
|
+
return JSON.parse(readFileSync(configPath, "utf8"));
|
|
412
|
+
} catch (err) {
|
|
413
|
+
console.error(
|
|
414
|
+
"[account-broker] Failed to parse accounts.json:",
|
|
415
|
+
err.message,
|
|
416
|
+
);
|
|
234
417
|
return null;
|
|
235
418
|
}
|
|
236
419
|
}
|
|
@@ -242,10 +425,23 @@ function createBroker() {
|
|
|
242
425
|
if (!config) return null;
|
|
243
426
|
try {
|
|
244
427
|
return new AccountBroker(config);
|
|
245
|
-
} catch {
|
|
428
|
+
} catch (err) {
|
|
429
|
+
console.error("[account-broker] Failed to create broker:", err.message);
|
|
246
430
|
return null;
|
|
247
431
|
}
|
|
248
432
|
}
|
|
249
433
|
|
|
250
|
-
|
|
251
|
-
|
|
434
|
+
/** Re-read config and replace the module-level singleton. ESM live binding propagates to all importers. */
|
|
435
|
+
function reloadBroker() {
|
|
436
|
+
const config = loadConfig();
|
|
437
|
+
if (!config) return { ok: false, error: "Config not found or invalid" };
|
|
438
|
+
try {
|
|
439
|
+
broker = new AccountBroker(config);
|
|
440
|
+
return { ok: true, broker };
|
|
441
|
+
} catch (err) {
|
|
442
|
+
return { ok: false, error: err.message };
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export let broker = createBroker();
|
|
447
|
+
export { AccountBroker, reloadBroker };
|