triflux 10.16.0 → 10.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/.claude-plugin/marketplace.json +34 -0
  2. package/.claude-plugin/plugin.json +22 -0
  3. package/bin/triflux.mjs +78 -27
  4. package/config/mcp-registry.json +29 -0
  5. package/hooks/hook-registry.json +1 -1
  6. package/hooks/keyword-rules.json +12 -0
  7. package/hooks/safety-guard.mjs +3 -0
  8. package/hub/cli-adapter-base.mjs +10 -5
  9. package/hub/lib/hosts-compat.mjs +19 -4
  10. package/hub/lib/process-utils.mjs +676 -86
  11. package/hub/lib/ssh-command.mjs +4 -10
  12. package/hub/router.mjs +20 -3
  13. package/hub/server.mjs +70 -6
  14. package/hub/team/agent-map.json +2 -0
  15. package/hub/team/codex-review.mjs +14 -9
  16. package/hub/team/conductor.mjs +8 -4
  17. package/hub/team/psmux.mjs +1 -1
  18. package/hub/team/swarm-hypervisor.mjs +59 -10
  19. package/hub/team/worktree-lifecycle.mjs +51 -9
  20. package/hub/workers/codex-app-server-worker.mjs +4 -6
  21. package/hub/workers/lib/jsonrpc-stdio.mjs +0 -1
  22. package/hub/workers/worker-utils.mjs +1 -2
  23. package/package.json +67 -23
  24. package/scripts/codex-mcp-gateway-sync.mjs +22 -0
  25. package/scripts/doctor-diagnose.mjs +24 -11
  26. package/scripts/lib/mcp-guard-engine.mjs +20 -0
  27. package/scripts/mcp-cleanup.ps1 +9 -0
  28. package/scripts/session-stale-cleanup.mjs +102 -1
  29. package/scripts/setup.mjs +29 -1
  30. package/scripts/sync-hub-mcp-settings.mjs +38 -1
  31. package/scripts/test-lock.mjs +134 -36
  32. package/scripts/tfx-route-post.mjs +5 -1
  33. package/tui/codex-profile.mjs +459 -0
  34. package/tui/core.mjs +266 -0
  35. package/tui/doctor.mjs +375 -0
  36. package/tui/gemini-profile.mjs +299 -0
  37. package/tui/monitor-data.mjs +152 -0
  38. package/tui/monitor.mjs +333 -0
  39. package/tui/setup.mjs +599 -0
  40. package/CLAUDE.md +0 -169
  41. package/references/cli-parameter-reference.md +0 -240
  42. package/references/codex-plugin-cc-analysis.md +0 -706
  43. package/references/codex-plugin-cc-code-patterns.md +0 -468
  44. package/references/hosts.json +0 -46
@@ -0,0 +1,34 @@
1
+ {
2
+ "$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
3
+ "name": "triflux",
4
+ "description": "CLI-first multi-model orchestrator — Codex/Gemini/Claude routing with DAG execution, auto-triage, and cost optimization",
5
+ "owner": {
6
+ "name": "tellang"
7
+ },
8
+ "plugins": [
9
+ {
10
+ "name": "triflux",
11
+ "description": "Tri-CLI orchestrator for Claude Code. Routes tasks across Claude + Codex + Gemini with consensus intelligence, natural language routing, 42 skills, and cross-model review.",
12
+ "version": "10.17.1",
13
+ "author": {
14
+ "name": "tellang"
15
+ },
16
+ "source": {
17
+ "source": "npm",
18
+ "package": "triflux"
19
+ },
20
+ "category": "productivity",
21
+ "homepage": "https://github.com/tellang/triflux",
22
+ "tags": [
23
+ "multi-model",
24
+ "codex",
25
+ "gemini",
26
+ "cli-routing",
27
+ "orchestration",
28
+ "cost-optimization",
29
+ "dag-execution"
30
+ ]
31
+ }
32
+ ],
33
+ "version": "10.17.1"
34
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "triflux",
3
+ "version": "10.17.1",
4
+ "description": "CLI-first multi-model orchestrator for Claude Code — route tasks to Codex, Gemini, and Claude",
5
+ "author": {
6
+ "name": "tellang"
7
+ },
8
+ "repository": "https://github.com/tellang/triflux",
9
+ "homepage": "https://github.com/tellang/triflux",
10
+ "license": "MIT",
11
+ "keywords": [
12
+ "claude-code",
13
+ "plugin",
14
+ "codex",
15
+ "gemini",
16
+ "cli-routing",
17
+ "orchestration",
18
+ "multi-model"
19
+ ],
20
+ "skills": "./skills/",
21
+ "hooks": "./hooks/hooks.json"
22
+ }
package/bin/triflux.mjs CHANGED
@@ -462,14 +462,14 @@ function printJson(payload) {
462
462
  process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
463
463
  }
464
464
 
465
- function withConsoleSilenced(enabled, fn) {
465
+ async function withConsoleSilenced(enabled, fn) {
466
466
  if (!enabled) return fn();
467
467
  const originalLog = console.log;
468
468
  const originalError = console.error;
469
469
  console.log = () => {};
470
470
  console.error = () => {};
471
471
  try {
472
- return fn();
472
+ return await fn();
473
473
  } finally {
474
474
  console.log = originalLog;
475
475
  console.error = originalError;
@@ -1846,6 +1846,7 @@ async function cmdDoctor(options = {}) {
1846
1846
  checks: [],
1847
1847
  actions: [],
1848
1848
  hook_coverage: { total: 0, registered: 0, missing: [] },
1849
+ fsmonitorDaemons: { stale: 0, killed: 0 },
1849
1850
  issue_count: 0,
1850
1851
  };
1851
1852
 
@@ -3126,9 +3127,11 @@ async function cmdDoctor(options = {}) {
3126
3127
  section("Orphan Processes");
3127
3128
  if (process.platform === "win32") {
3128
3129
  try {
3129
- const { cleanupOrphanNodeProcesses } = await import(
3130
- "../hub/lib/process-utils.mjs"
3131
- );
3130
+ const {
3131
+ cleanupOrphanNodeProcesses,
3132
+ cleanupStaleFsmonitorDaemons,
3133
+ findFsmonitorDaemons,
3134
+ } = await import("../hub/lib/process-utils.mjs");
3132
3135
  if (fix) {
3133
3136
  const { killed, remaining } = cleanupOrphanNodeProcesses();
3134
3137
  if (killed > 0) {
@@ -3157,6 +3160,55 @@ async function cmdDoctor(options = {}) {
3157
3160
  ok(`node.exe ${count}개 (정상 범위)`);
3158
3161
  }
3159
3162
  }
3163
+
3164
+ const fsmonitorStale = findFsmonitorDaemons({
3165
+ minAgeMs: 24 * 60 * 60 * 1000,
3166
+ });
3167
+ let fsmonitorKilled = 0;
3168
+ if (fix && fsmonitorStale.length > 0) {
3169
+ const cleanupResult = cleanupStaleFsmonitorDaemons({
3170
+ minAgeMs: 24 * 60 * 60 * 1000,
3171
+ });
3172
+ fsmonitorKilled = cleanupResult.killed;
3173
+ report.actions.push({
3174
+ type: "git-fsmonitor-cleanup",
3175
+ status:
3176
+ fsmonitorKilled === fsmonitorStale.length ? "ok" : "partial",
3177
+ stale: fsmonitorStale.length,
3178
+ killed: fsmonitorKilled,
3179
+ });
3180
+ warn(
3181
+ `stale git fsmonitor daemon ${fsmonitorKilled}/${fsmonitorStale.length}개 정리`,
3182
+ );
3183
+ } else if (fsmonitorStale.length > 0) {
3184
+ warn(
3185
+ `stale git fsmonitor daemon ${fsmonitorStale.length}개 발견 (24h+). 정리: tfx doctor --fix`,
3186
+ );
3187
+ } else {
3188
+ ok("stale git fsmonitor daemon 없음");
3189
+ }
3190
+
3191
+ report.fsmonitorDaemons = {
3192
+ stale: fsmonitorStale.length,
3193
+ killed: fsmonitorKilled,
3194
+ };
3195
+ addDoctorCheck(report, {
3196
+ name: "fsmonitor-daemons",
3197
+ status: fsmonitorStale.length > 0 ? "warning" : "ok",
3198
+ stale: fsmonitorStale.length,
3199
+ killed: fsmonitorKilled,
3200
+ detail: fsmonitorStale.map((p) => ({
3201
+ pid: p.pid,
3202
+ parentPid: p.parentPid,
3203
+ ageHours: Number((p.ageMs / (60 * 60 * 1000)).toFixed(1)),
3204
+ })),
3205
+ });
3206
+ if (
3207
+ fsmonitorStale.length > 0 &&
3208
+ (!fix || fsmonitorKilled < fsmonitorStale.length)
3209
+ ) {
3210
+ issues++;
3211
+ }
3160
3212
  } catch (e) {
3161
3213
  info(`고아 프로세스 검사 실패: ${e.message}`);
3162
3214
  }
@@ -5073,29 +5125,28 @@ function startHubAfterUpdate(info) {
5073
5125
  function autoRegisterMcp(mcpUrl, { codexEnabled = false } = {}) {
5074
5126
  section("MCP 자동 등록");
5075
5127
 
5076
- // Codex — config.json에 기본 disabled 엔트리로 등록
5077
- if (which("codex")) {
5078
- try {
5079
- const result = ensureCodexHubServerConfig({
5080
- mcpUrl,
5081
- createIfMissing: true,
5082
- enabled: codexEnabled,
5083
- });
5084
- if (!result.ok) throw new Error(result.reason || "unknown");
5085
- if (result.changed) {
5086
- ok(
5087
- `Codex: config.json에 등록 완료 (${codexEnabled ? "enabled" : "기본 disabled"})`,
5088
- );
5089
- } else {
5090
- ok(
5091
- `Codex: 이미 등록됨 (${codexEnabled ? "enabled" : "기본 disabled"})`,
5092
- );
5093
- }
5094
- } catch (e) {
5095
- warn(`Codex 등록 실패: ${e.message}`);
5128
+ // Codex — config.json에 기본 disabled 엔트리로 등록.
5129
+ // Hub startup must keep the MCP config fresh even on CI/dev machines where
5130
+ // the Codex CLI binary itself is not installed.
5131
+ try {
5132
+ const result = ensureCodexHubServerConfig({
5133
+ mcpUrl,
5134
+ createIfMissing: true,
5135
+ enabled: codexEnabled,
5136
+ });
5137
+ if (!result.ok) throw new Error(result.reason || "unknown");
5138
+ const suffix = which("codex") ? "" : " (CLI 미설치)";
5139
+ if (result.changed) {
5140
+ ok(
5141
+ `Codex: config.json에 등록 완료 (${codexEnabled ? "enabled" : "기본 disabled"})${suffix}`,
5142
+ );
5143
+ } else {
5144
+ ok(
5145
+ `Codex: 이미 등록됨 (${codexEnabled ? "enabled" : "기본 disabled"})${suffix}`,
5146
+ );
5096
5147
  }
5097
- } else {
5098
- info("Codex: 미설치 (건너뜀)");
5148
+ } catch (e) {
5149
+ warn(`Codex 등록 실패: ${e.message}`);
5099
5150
  }
5100
5151
 
5101
5152
  // Gemini — settings.json 직접 수정
@@ -0,0 +1,29 @@
1
+ {
2
+ "$schema": "mcp-registry-schema",
3
+ "version": 1,
4
+ "description": "MCP 서버 중앙 레지스트리 — 진실의 원천",
5
+ "defaults": {
6
+ "transport": "hub-url",
7
+ "hub_base": "http://127.0.0.1:27888"
8
+ },
9
+ "servers": {
10
+ "tfx-hub": {
11
+ "transport": "hub-url",
12
+ "url": "http://127.0.0.1:27888/mcp",
13
+ "safe": true,
14
+ "targets": ["claude", "gemini", "codex"],
15
+ "description": "triflux Hub MCP 서버"
16
+ }
17
+ },
18
+ "policies": {
19
+ "stdio_action": "replace-with-hub",
20
+ "unknown_server_action": "warn",
21
+ "watched_paths": [
22
+ "~/.gemini/settings.json",
23
+ "~/.codex/config.toml",
24
+ "~/.claude/settings.json",
25
+ "~/.claude/settings.local.json",
26
+ ".mcp.json"
27
+ ]
28
+ }
29
+ }
@@ -244,7 +244,7 @@
244
244
  "matcher": "*",
245
245
  "command": "powershell -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File \"${HOME}/.claude/scripts/mcp-cleanup.ps1\"",
246
246
  "priority": 100,
247
- "enabled": true,
247
+ "enabled": false,
248
248
  "timeout": 8,
249
249
  "blocking": false,
250
250
  "description": "MCP 고아 프로세스 정리"
@@ -97,6 +97,18 @@
97
97
  "source": "(?:정리해|슬롭|클린업)",
98
98
  "flags": "i"
99
99
  },
100
+ {
101
+ "source": "(?:병렬|동시에|parallel|concurrent)",
102
+ "flags": "i"
103
+ },
104
+ {
105
+ "source": "(?:점검|진단|확인해)",
106
+ "flags": "i"
107
+ },
108
+ {
109
+ "source": "(?:계속해|이어서|진행해)",
110
+ "flags": "i"
111
+ },
100
112
  {
101
113
  "source": "\\b(?:implement|build|fix|review|test|plan|analyze)\\b",
102
114
  "flags": "i"
@@ -123,9 +123,12 @@ function getWindowsHostIds() {
123
123
  ids.add(name);
124
124
  if (cfg.tailscale?.ip) ids.add(cfg.tailscale.ip);
125
125
  if (cfg.tailscale?.dns) ids.add(cfg.tailscale.dns);
126
+ if (cfg.ssh?.host) ids.add(cfg.ssh.host);
126
127
  if (cfg.ssh?.user) {
127
128
  ids.add(`${cfg.ssh.user}@${name}`);
128
129
  if (cfg.tailscale?.ip) ids.add(`${cfg.ssh.user}@${cfg.tailscale.ip}`);
130
+ if (cfg.tailscale?.dns) ids.add(`${cfg.ssh.user}@${cfg.tailscale.dns}`);
131
+ if (cfg.ssh?.host) ids.add(`${cfg.ssh.user}@${cfg.ssh.host}`);
129
132
  }
130
133
  }
131
134
  } catch {
@@ -72,6 +72,11 @@ let _cachedVersion = null;
72
72
  */
73
73
  export function getCodexVersion() {
74
74
  if (_cachedVersion !== null) return _cachedVersion;
75
+ const override = Number(process.env.TFX_CODEX_VERSION_MINOR);
76
+ if (Number.isFinite(override) && override > 0) {
77
+ _cachedVersion = override;
78
+ return _cachedVersion;
79
+ }
75
80
  try {
76
81
  const out = execSync("codex --version", {
77
82
  encoding: "utf8",
@@ -80,7 +85,10 @@ export function getCodexVersion() {
80
85
  const match = out.match(/(\d+)\.(\d+)\.(\d+)/);
81
86
  _cachedVersion = match ? Number.parseInt(match[2], 10) : 0;
82
87
  } catch {
83
- _cachedVersion = 0;
88
+ // Command builders should remain stable in CI even when the real Codex
89
+ // CLI is absent. Runtime preflight still reports/install-gates Codex
90
+ // separately; this fallback only selects the modern argv shape.
91
+ _cachedVersion = 117;
84
92
  }
85
93
  return _cachedVersion;
86
94
  }
@@ -182,10 +190,7 @@ export function buildExecCommand(prompt, resultFile = null, opts = {}) {
182
190
  // ── Sleep ───────────────────────────────────────────────────────
183
191
 
184
192
  export function sleep(ms) {
185
- return new Promise((resolve) => {
186
- const timer = setTimeout(resolve, ms);
187
- timer.unref?.();
188
- });
193
+ return new Promise((resolve) => setTimeout(resolve, ms));
189
194
  }
190
195
 
191
196
  // ── Result factory ──────────────────────────────────────────────
@@ -133,8 +133,20 @@ function normalizeLastProbe(rawProbe) {
133
133
  return Object.keys(probe).length > 0 ? probe : null;
134
134
  }
135
135
 
136
+ function normalizeResources(rawHost) {
137
+ const rawResources =
138
+ rawHost.resources && typeof rawHost.resources === "object"
139
+ ? rawHost.resources
140
+ : {};
141
+ const rawSpecs =
142
+ rawHost.specs && typeof rawHost.specs === "object" ? rawHost.specs : {};
143
+ return { ...rawResources, ...rawSpecs };
144
+ }
145
+
136
146
  export function normalizeHost(rawHost = {}, name = "") {
137
147
  const sshUser = rawHost.ssh_user || rawHost.ssh?.user || rawHost.user || null;
148
+ const sshHost = rawHost.ssh?.host || rawHost.host || null;
149
+ const resources = normalizeResources(rawHost);
138
150
  const tailscale = {
139
151
  ip: rawHost.tailscale?.ip || null,
140
152
  dns: rawHost.tailscale?.dns || null,
@@ -167,15 +179,14 @@ export function normalizeHost(rawHost = {}, name = "") {
167
179
  ssh: {
168
180
  ...(rawHost.ssh && typeof rawHost.ssh === "object" ? rawHost.ssh : {}),
169
181
  user: sshUser,
182
+ host: sshHost,
170
183
  },
171
184
  tailscale,
172
185
  capabilities,
173
186
  capabilities_v2,
174
187
  last_probe: normalizeLastProbe(rawHost.last_probe),
175
- specs:
176
- rawHost.specs && typeof rawHost.specs === "object"
177
- ? { ...rawHost.specs }
178
- : {},
188
+ resources,
189
+ specs: { ...resources },
179
190
  raw: { ...rawHost },
180
191
  };
181
192
  }
@@ -234,6 +245,7 @@ export function resolveHost(nameOrAlias, repoRoot) {
234
245
  ...host.aliases,
235
246
  host.tailscale.ip,
236
247
  host.tailscale.dns,
248
+ host.ssh.host,
237
249
  host.ssh_user ? `${host.ssh_user}@${name}` : null,
238
250
  host.ssh_user && host.tailscale.ip
239
251
  ? `${host.ssh_user}@${host.tailscale.ip}`
@@ -241,6 +253,9 @@ export function resolveHost(nameOrAlias, repoRoot) {
241
253
  host.ssh_user && host.tailscale.dns
242
254
  ? `${host.ssh_user}@${host.tailscale.dns}`
243
255
  : null,
256
+ host.ssh_user && host.ssh.host
257
+ ? `${host.ssh_user}@${host.ssh.host}`
258
+ : null,
244
259
  ]);
245
260
  for (const alias of aliases) {
246
261
  if (alias && String(alias).toLowerCase() === lowered) {