triflux 10.18.2 → 10.20.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/.claude-plugin/marketplace.json +34 -0
- package/.claude-plugin/plugin.json +22 -0
- package/config/mcp-registry.json +44 -0
- package/hub/account-broker.mjs +33 -9
- package/hub/cli-adapter-base.mjs +19 -2
- package/hub/team/dashboard-open.mjs +24 -115
- package/hub/team/headless.mjs +1 -0
- package/hub/team/notify.mjs +9 -2
- package/hub/team/runtime-strategy.mjs +75 -17
- package/hub/team/terminal-opener.mjs +178 -0
- package/hub/team/worktree-lifecycle.mjs +16 -6
- package/hub/team/wt-manager.mjs +23 -1
- package/hub/workers/codex-mcp.mjs +49 -4
- package/package.json +67 -23
- package/scripts/mcp-gateway-ensure.mjs +14 -5
- package/scripts/mcp-gateway-integration-test.mjs +27 -11
- package/scripts/mcp-gateway-start.mjs +86 -34
- package/scripts/tfx-route-worker.mjs +14 -4
- package/scripts/tfx-route.sh +2 -2
- package/skills/tfx-research/SKILL.md +1 -1
- package/skills/tfx-wt/SKILL.md +212 -0
- package/tui/codex-profile.mjs +459 -0
- package/tui/core.mjs +266 -0
- package/tui/doctor.mjs +375 -0
- package/tui/gemini-profile.mjs +299 -0
- package/tui/monitor-data.mjs +152 -0
- package/tui/monitor.mjs +317 -0
- package/tui/setup.mjs +599 -0
- package/CLAUDE.md +0 -170
- package/references/cli-parameter-reference.md +0 -240
- package/references/codex-plugin-cc-analysis.md +0 -706
- package/references/codex-plugin-cc-code-patterns.md +0 -468
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { exec as defaultExec } from "node:child_process";
|
|
2
|
+
import { platform as osPlatform } from "node:os";
|
|
3
|
+
import { psmuxExec as defaultPsmuxExec } from "./psmux.mjs";
|
|
4
|
+
import { tmuxExec as defaultTmuxExec, detectMultiplexer } from "./session.mjs";
|
|
5
|
+
import { createWtManager as defaultCreateWtManager } from "./wt-manager.mjs";
|
|
6
|
+
|
|
7
|
+
const TMUX_LIKE_MUXES = new Set(["tmux", "wsl-tmux", "git-bash-tmux"]);
|
|
8
|
+
|
|
9
|
+
export function sanitizeTerminalTitle(value, fallback = "triflux") {
|
|
10
|
+
const title = String(value ?? "")
|
|
11
|
+
.replace(/\s+/gu, " ")
|
|
12
|
+
.trim();
|
|
13
|
+
return title || fallback;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function shellQuote(value) {
|
|
17
|
+
return `'${String(value).replace(/'/g, "'\\''")}'`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function powershellSingleQuote(value) {
|
|
21
|
+
return `'${String(value).replace(/'/g, "''")}'`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function buildCommandString({ cwd, command }) {
|
|
25
|
+
const commandString = String(command ?? "");
|
|
26
|
+
if (!cwd) return commandString;
|
|
27
|
+
return `cd ${shellQuote(cwd)} && ${commandString}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function resolvePlatform(deps) {
|
|
31
|
+
if (typeof deps.platform === "function") return deps.platform();
|
|
32
|
+
return deps.platform || osPlatform();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function resolveMux(deps) {
|
|
36
|
+
if (Object.hasOwn(deps, "mux")) return deps.mux;
|
|
37
|
+
if (typeof deps.detectMultiplexer === "function")
|
|
38
|
+
return deps.detectMultiplexer();
|
|
39
|
+
return detectMultiplexer();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function createTabSpec(spec, title) {
|
|
43
|
+
return {
|
|
44
|
+
title,
|
|
45
|
+
command: String(spec.command ?? ""),
|
|
46
|
+
cwd: spec.cwd,
|
|
47
|
+
profile: spec.profile ?? "triflux",
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function execOpenTerminal(execFn) {
|
|
52
|
+
return new Promise((resolve) => {
|
|
53
|
+
execFn("open -a Terminal", { timeout: 5000 }, (error) => {
|
|
54
|
+
resolve(!error);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// psmux는 Windows에서 wt-manager 경유 (createTab/splitPane 등) — tmux 호환 unix 명령군과 다른 표면.
|
|
60
|
+
// 비-Windows(macOS/Linux) 환경에서는 psmux가 tmux-compatible new-window/select-pane 명령을 받아주므로
|
|
61
|
+
// tmux 어댑터와 동일하게 취급한다.
|
|
62
|
+
function isTmuxLikeMux(mux, platform) {
|
|
63
|
+
return TMUX_LIKE_MUXES.has(mux) || (platform !== "win32" && mux === "psmux");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function shellCommandName(value) {
|
|
67
|
+
const command = String(value);
|
|
68
|
+
return /^[A-Za-z0-9_./:-]+$/u.test(command) ? command : shellQuote(command);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function buildAttachCommand(mux, sessionName) {
|
|
72
|
+
if (mux === "psmux") {
|
|
73
|
+
return `${shellCommandName(process.env.PSMUX_BIN || "psmux")} attach-session -t ${shellQuote(sessionName)}`;
|
|
74
|
+
}
|
|
75
|
+
return `tmux attach-session -t ${shellQuote(sessionName)}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// wt-manager.createTab은 (a) undefined/void 반환 — legacy success 의미 (b) {success:true|false, ...} 객체 반환의 두 형태가 공존.
|
|
79
|
+
// `result?.success !== false`는 undefined/null/{}을 모두 success로 취급하고 명시적 {success:false}만 실패로 본다.
|
|
80
|
+
function wtResultSucceeded(result) {
|
|
81
|
+
return result?.success !== false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function createTerminalOpener(deps = {}) {
|
|
85
|
+
const platform = resolvePlatform(deps);
|
|
86
|
+
const tmuxExec = deps.tmuxExec || defaultTmuxExec;
|
|
87
|
+
const psmuxExec = deps.psmuxExec || defaultPsmuxExec;
|
|
88
|
+
const exec = deps.exec || defaultExec;
|
|
89
|
+
const createWtManager = deps.createWtManager || defaultCreateWtManager;
|
|
90
|
+
|
|
91
|
+
async function openCommand(spec = {}) {
|
|
92
|
+
const title = sanitizeTerminalTitle(spec.title);
|
|
93
|
+
|
|
94
|
+
if (platform === "win32") {
|
|
95
|
+
const wt = createWtManager();
|
|
96
|
+
try {
|
|
97
|
+
return wtResultSucceeded(
|
|
98
|
+
await wt.createTab(createTabSpec(spec, title)),
|
|
99
|
+
);
|
|
100
|
+
} catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const mux = resolveMux(deps);
|
|
106
|
+
if (isTmuxLikeMux(mux, platform)) {
|
|
107
|
+
tmuxExec(
|
|
108
|
+
`new-window -n ${shellQuote(title)} ${shellQuote(
|
|
109
|
+
buildCommandString(spec),
|
|
110
|
+
)}`,
|
|
111
|
+
);
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (platform === "darwin") {
|
|
116
|
+
return execOpenTerminal(exec);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function openSession(sessionName, opts = {}) {
|
|
123
|
+
if (platform === "win32") {
|
|
124
|
+
const wt = createWtManager();
|
|
125
|
+
try {
|
|
126
|
+
return wtResultSucceeded(
|
|
127
|
+
await wt.createTab({
|
|
128
|
+
title: sanitizeTerminalTitle(opts.title ?? sessionName),
|
|
129
|
+
command: `psmux attach-session -t ${powershellSingleQuote(sessionName)}`,
|
|
130
|
+
cwd: opts.cwd,
|
|
131
|
+
profile: opts.profile ?? "triflux",
|
|
132
|
+
}),
|
|
133
|
+
);
|
|
134
|
+
} catch {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const mux = resolveMux(deps);
|
|
140
|
+
if (isTmuxLikeMux(mux, platform)) {
|
|
141
|
+
tmuxExec(
|
|
142
|
+
`new-window -n ${shellQuote(opts.title ?? sessionName)} ${shellQuote(
|
|
143
|
+
buildAttachCommand(mux, sessionName),
|
|
144
|
+
)}`,
|
|
145
|
+
);
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function focusPane(sessionName, workerNumber) {
|
|
153
|
+
const target = `${sessionName}:0.${workerNumber}`;
|
|
154
|
+
const mux = resolveMux(deps);
|
|
155
|
+
|
|
156
|
+
if (mux === "psmux") {
|
|
157
|
+
psmuxExec(["select-pane", "-t", target]);
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (isTmuxLikeMux(mux, platform)) {
|
|
162
|
+
tmuxExec(`select-pane -t ${shellQuote(target)}`);
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return { openCommand, openSession, focusPane };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function openSessionTarget(sessionName, opts = {}) {
|
|
173
|
+
return createTerminalOpener(opts._deps).openSession(sessionName, opts);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function focusSessionPane(sessionName, workerNumber, opts = {}) {
|
|
177
|
+
return createTerminalOpener(opts._deps).focusPane(sessionName, workerNumber);
|
|
178
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// Remote support: host option → SSH-based git operations via remote-session.mjs.
|
|
5
5
|
|
|
6
6
|
import { execFile } from "node:child_process";
|
|
7
|
-
import { access, mkdir, readdir, rm } from "node:fs/promises";
|
|
7
|
+
import { access, mkdir, readdir, realpath, rm } from "node:fs/promises";
|
|
8
8
|
import { join, normalize, relative, resolve } from "node:path";
|
|
9
9
|
import { remoteGit, validateHost } from "./remote-session.mjs";
|
|
10
10
|
|
|
@@ -117,6 +117,14 @@ function normPath(p) {
|
|
|
117
117
|
return normalize(p).replace(/\\/g, "/");
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
async function normExistingPath(p) {
|
|
121
|
+
try {
|
|
122
|
+
return normPath(await realpath(p));
|
|
123
|
+
} catch {
|
|
124
|
+
return normPath(p);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
120
128
|
function resolveCleanupTarget(worktreePath, rootDir) {
|
|
121
129
|
const resolvedRoot = resolve(rootDir);
|
|
122
130
|
const resolvedWorktree = resolve(worktreePath);
|
|
@@ -562,10 +570,12 @@ export async function pruneOrphanWorktrees({ rootDir = process.cwd() } = {}) {
|
|
|
562
570
|
try {
|
|
563
571
|
const raw = await git(["worktree", "list", "--porcelain"], rootDir);
|
|
564
572
|
registeredPaths = new Set(
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
573
|
+
await Promise.all(
|
|
574
|
+
raw
|
|
575
|
+
.split("\n")
|
|
576
|
+
.filter((l) => l.startsWith("worktree "))
|
|
577
|
+
.map((l) => normExistingPath(l.slice("worktree ".length))),
|
|
578
|
+
),
|
|
569
579
|
);
|
|
570
580
|
} catch {
|
|
571
581
|
return removed; // git worktree list failed → don't remove anything
|
|
@@ -573,7 +583,7 @@ export async function pruneOrphanWorktrees({ rootDir = process.cwd() } = {}) {
|
|
|
573
583
|
|
|
574
584
|
for (const dir of wtDirs) {
|
|
575
585
|
const fullPath = resolve(swarmDir, dir);
|
|
576
|
-
const normalized =
|
|
586
|
+
const normalized = await normExistingPath(fullPath);
|
|
577
587
|
if (!registeredPaths.has(normalized)) {
|
|
578
588
|
try {
|
|
579
589
|
await stopFsmonitorDaemon(fullPath).catch(() => null);
|
package/hub/team/wt-manager.mjs
CHANGED
|
@@ -170,12 +170,34 @@ function atomicWriteSync(filePath, data) {
|
|
|
170
170
|
* @param {number} [opts.tabCreateDelayMs=500]
|
|
171
171
|
* @param {object} [opts.deps] — 테스트용 의존성 주입
|
|
172
172
|
*/
|
|
173
|
+
function createNonWindowsStubManager() {
|
|
174
|
+
const asyncFalse = async () => false;
|
|
175
|
+
return Object.freeze({
|
|
176
|
+
ensureWtProfile: () => {},
|
|
177
|
+
createTab: asyncFalse,
|
|
178
|
+
renameTab: asyncFalse,
|
|
179
|
+
closeTab: asyncFalse,
|
|
180
|
+
listTabs: async () => [],
|
|
181
|
+
closeStale: async () => 0,
|
|
182
|
+
createSession: asyncFalse,
|
|
183
|
+
splitPane: asyncFalse,
|
|
184
|
+
applySplitLayout: asyncFalse,
|
|
185
|
+
getEnvironmentInfo: () => ({
|
|
186
|
+
platform: process.platform,
|
|
187
|
+
hasWindowsTerminal: false,
|
|
188
|
+
hasWt: false,
|
|
189
|
+
isWindowsTerminalSession: false,
|
|
190
|
+
}),
|
|
191
|
+
getTabCount: () => 0,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
173
195
|
export function createWtManager(opts = {}) {
|
|
174
196
|
const deps = opts.deps || {};
|
|
175
197
|
const platform = deps.platform || osPlatform;
|
|
176
198
|
|
|
177
199
|
if (platform() !== "win32") {
|
|
178
|
-
|
|
200
|
+
return createNonWindowsStubManager();
|
|
179
201
|
}
|
|
180
202
|
|
|
181
203
|
const now = deps.now || Date.now;
|
|
@@ -16,6 +16,7 @@ const REQUIRED_TOOLS = ["codex", "codex-reply"];
|
|
|
16
16
|
export { CODEX_MCP_EXECUTION_EXIT_CODE, CODEX_MCP_TRANSPORT_EXIT_CODE };
|
|
17
17
|
export const DEFAULT_CODEX_MCP_TIMEOUT_MS = 10 * 60 * 1000;
|
|
18
18
|
export const DEFAULT_CODEX_MCP_BOOTSTRAP_TIMEOUT_MS = 120 * 1000;
|
|
19
|
+
export const DEFAULT_CODEX_MCP_SHUTDOWN_TIMEOUT_MS = 2_000;
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* Codex MCP transport/bootstrap 계층 오류
|
|
@@ -40,6 +41,34 @@ function cloneEnv(env = process.env) {
|
|
|
40
41
|
);
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
async function closeWithin(label, closeFn, timeoutMs) {
|
|
45
|
+
let timer = null;
|
|
46
|
+
try {
|
|
47
|
+
const timeout = new Promise((resolve) => {
|
|
48
|
+
timer = setTimeout(() => {
|
|
49
|
+
resolve({ timedOut: true });
|
|
50
|
+
}, timeoutMs);
|
|
51
|
+
});
|
|
52
|
+
const closed = Promise.resolve()
|
|
53
|
+
.then(closeFn)
|
|
54
|
+
.then(() => ({ timedOut: false }));
|
|
55
|
+
const result = await Promise.race([closed, timeout]);
|
|
56
|
+
if (result.timedOut) {
|
|
57
|
+
console.error(
|
|
58
|
+
`[codex-mcp] WARNING: ${label} did not close within ${timeoutMs}ms; continuing shutdown.`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error(
|
|
64
|
+
`[codex-mcp] WARNING: ${label} close failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
65
|
+
);
|
|
66
|
+
return { timedOut: false };
|
|
67
|
+
} finally {
|
|
68
|
+
if (timer) clearTimeout(timer);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
43
72
|
function collectTextContent(content = []) {
|
|
44
73
|
return content
|
|
45
74
|
.filter((item) => item?.type === "text" && typeof item.text === "string")
|
|
@@ -388,7 +417,7 @@ export class CodexMcpWorker {
|
|
|
388
417
|
this.ready = true;
|
|
389
418
|
}
|
|
390
419
|
|
|
391
|
-
async stop() {
|
|
420
|
+
async stop(options = {}) {
|
|
392
421
|
this.ready = false;
|
|
393
422
|
this.availableTools.clear();
|
|
394
423
|
|
|
@@ -397,10 +426,26 @@ export class CodexMcpWorker {
|
|
|
397
426
|
this.transport = null;
|
|
398
427
|
this.client = null;
|
|
399
428
|
|
|
429
|
+
const shutdownTimeoutMs = Number.isFinite(options.shutdownTimeoutMs)
|
|
430
|
+
? options.shutdownTimeoutMs
|
|
431
|
+
: DEFAULT_CODEX_MCP_SHUTDOWN_TIMEOUT_MS;
|
|
432
|
+
|
|
433
|
+
let clientCloseTimedOut = false;
|
|
400
434
|
if (client) {
|
|
401
|
-
await
|
|
402
|
-
|
|
403
|
-
|
|
435
|
+
const result = await closeWithin(
|
|
436
|
+
"client.close",
|
|
437
|
+
() => client.close(),
|
|
438
|
+
shutdownTimeoutMs,
|
|
439
|
+
);
|
|
440
|
+
clientCloseTimedOut = Boolean(result.timedOut);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (transport && (!client || clientCloseTimedOut)) {
|
|
444
|
+
await closeWithin(
|
|
445
|
+
"transport.close",
|
|
446
|
+
() => transport.close(),
|
|
447
|
+
shutdownTimeoutMs,
|
|
448
|
+
);
|
|
404
449
|
}
|
|
405
450
|
|
|
406
451
|
transport?.stderr?.destroy?.();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "triflux",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.20.0",
|
|
4
4
|
"description": "CLI-first multi-model orchestrator for Claude Code — route tasks to Codex, Gemini, and Claude",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,29 +13,81 @@
|
|
|
13
13
|
"tfx-doctor-tui": "bin/tfx-doctor-tui.mjs",
|
|
14
14
|
"tfx-setup-tui": "bin/tfx-setup-tui.mjs"
|
|
15
15
|
},
|
|
16
|
-
"engines": {
|
|
17
|
-
"node": ">=18.0.0"
|
|
18
|
-
},
|
|
19
|
-
"dependencies": {
|
|
20
|
-
"@triflux/core": "10.0.1",
|
|
21
|
-
"@triflux/remote": "^10.0.0-alpha.1"
|
|
22
|
-
},
|
|
23
16
|
"files": [
|
|
24
17
|
"bin",
|
|
18
|
+
"tui",
|
|
19
|
+
"hub",
|
|
20
|
+
"config",
|
|
25
21
|
"skills",
|
|
26
22
|
"!skills/tfx-workspace",
|
|
23
|
+
"!**/failure-reports",
|
|
24
|
+
"scripts",
|
|
27
25
|
"hooks",
|
|
28
26
|
"hud",
|
|
29
|
-
"scripts",
|
|
30
|
-
"hub",
|
|
31
27
|
"mesh",
|
|
32
|
-
"
|
|
33
|
-
"!references/codex-snapshots",
|
|
34
|
-
"!references/gemini-snapshots",
|
|
35
|
-
"CLAUDE.md",
|
|
28
|
+
".claude-plugin",
|
|
36
29
|
"README.md",
|
|
30
|
+
"README.ko.md",
|
|
37
31
|
"LICENSE"
|
|
38
32
|
],
|
|
33
|
+
"workspaces": [
|
|
34
|
+
"packages/core",
|
|
35
|
+
"packages/remote",
|
|
36
|
+
"packages/triflux"
|
|
37
|
+
],
|
|
38
|
+
"scripts": {
|
|
39
|
+
"pack": "node scripts/pack.mjs all",
|
|
40
|
+
"pack:core": "node scripts/pack.mjs core",
|
|
41
|
+
"pack:remote": "node scripts/pack.mjs remote",
|
|
42
|
+
"setup": "node scripts/setup.mjs",
|
|
43
|
+
"preinstall": "node scripts/preinstall.mjs",
|
|
44
|
+
"postinstall": "node scripts/setup.mjs",
|
|
45
|
+
"snapshot:codex": "node scripts/snapshot-codex-state.mjs",
|
|
46
|
+
"snapshot:gemini": "node scripts/snapshot-gemini-state.mjs",
|
|
47
|
+
"snapshot:all": "npm run snapshot:codex && npm run snapshot:gemini",
|
|
48
|
+
"lint": "biome check bin config hooks hub hud mesh scripts tests .claude-plugin .github package.json package-lock.json biome.json",
|
|
49
|
+
"lint:fix": "biome check --write bin config hooks hub hud mesh scripts tests .claude-plugin .github package.json package-lock.json biome.json",
|
|
50
|
+
"health": "npm test && npm run lint",
|
|
51
|
+
"test": "node scripts/test-lock.mjs --test --test-force-exit --test-concurrency=8 \"tests/**/*.test.mjs\" \"scripts/__tests__/**/*.test.mjs\"",
|
|
52
|
+
"test:guard-codex-config": "node scripts/check-codex-config-stable.mjs npm test",
|
|
53
|
+
"test:unit": "node scripts/test-lock.mjs --test --test-force-exit --test-concurrency=8 tests/unit/**/*.test.mjs",
|
|
54
|
+
"test:integration": "node scripts/test-lock.mjs --test --test-force-exit --test-concurrency=8 tests/integration/**/*.test.mjs",
|
|
55
|
+
"test:route-smoke": "node scripts/test-lock.mjs --test scripts/test-tfx-route-no-claude-native.mjs",
|
|
56
|
+
"test:contract": "node scripts/test-lock.mjs --test --test-force-exit --test-concurrency=8 tests/contract/**/*.test.mjs",
|
|
57
|
+
"test:coverage": "node --experimental-test-coverage --test-coverage-lines=60 --test-coverage-functions=60 --test --test-force-exit --test-concurrency=8 \"tests/**/*.test.mjs\"",
|
|
58
|
+
"gen:skill-docs": "node scripts/gen-skill-docs.mjs",
|
|
59
|
+
"gen:skill-manifest": "node scripts/gen-skill-manifest.mjs",
|
|
60
|
+
"release:check-sync": "node scripts/release/check-sync.mjs",
|
|
61
|
+
"release:check-sync:fix": "node scripts/release/check-sync.mjs --fix",
|
|
62
|
+
"release:check-mirror": "node scripts/release/check-packages-mirror.mjs",
|
|
63
|
+
"release:check-mirror:fix": "node scripts/release/check-packages-mirror.mjs --fix",
|
|
64
|
+
"release:bump": "node scripts/release/bump-version.mjs",
|
|
65
|
+
"release:prepare": "node scripts/release/prepare.mjs",
|
|
66
|
+
"release:publish": "node scripts/release/publish.mjs",
|
|
67
|
+
"release:verify": "node scripts/release/verify.mjs"
|
|
68
|
+
},
|
|
69
|
+
"engines": {
|
|
70
|
+
"node": ">=18.0.0"
|
|
71
|
+
},
|
|
72
|
+
"repository": {
|
|
73
|
+
"type": "git",
|
|
74
|
+
"url": "git+https://github.com/tellang/triflux.git"
|
|
75
|
+
},
|
|
76
|
+
"homepage": "https://github.com/tellang/triflux#readme",
|
|
77
|
+
"author": "tellang",
|
|
78
|
+
"license": "MIT",
|
|
79
|
+
"dependencies": {
|
|
80
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
81
|
+
"better-sqlite3": "^12.6.2",
|
|
82
|
+
"pino": "^10.3.1",
|
|
83
|
+
"pino-pretty": "^13.1.3",
|
|
84
|
+
"systray2": "^2.1.4",
|
|
85
|
+
"zod": "^4.0.0"
|
|
86
|
+
},
|
|
87
|
+
"devDependencies": {
|
|
88
|
+
"@biomejs/biome": "^2.0.0",
|
|
89
|
+
"knip": "^6.3.0"
|
|
90
|
+
},
|
|
39
91
|
"keywords": [
|
|
40
92
|
"claude-code",
|
|
41
93
|
"plugin",
|
|
@@ -46,13 +98,5 @@
|
|
|
46
98
|
"multi-model",
|
|
47
99
|
"triflux",
|
|
48
100
|
"tfx"
|
|
49
|
-
]
|
|
50
|
-
"author": "tellang",
|
|
51
|
-
"license": "MIT",
|
|
52
|
-
"homepage": "https://github.com/tellang/triflux#readme",
|
|
53
|
-
"repository": {
|
|
54
|
-
"type": "git",
|
|
55
|
-
"url": "git+https://github.com/tellang/triflux.git",
|
|
56
|
-
"directory": "packages/triflux"
|
|
57
|
-
}
|
|
101
|
+
]
|
|
58
102
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// mcp-gateway-ensure.mjs — SessionStart 훅에서 supergateway MCP 서비스 보장
|
|
4
4
|
// hub-ensure.mjs 패턴을 따름. 가볍게 헬스체크만 수행하고 필요시 기동.
|
|
5
5
|
|
|
6
|
-
import { execSync } from "node:child_process";
|
|
6
|
+
import { execSync, spawn } from "node:child_process";
|
|
7
7
|
import { existsSync } from "node:fs";
|
|
8
8
|
import { tmpdir } from "node:os";
|
|
9
9
|
import { dirname, join } from "node:path";
|
|
@@ -36,10 +36,19 @@ function startGateway() {
|
|
|
36
36
|
if (!existsSync(scriptPath)) return false;
|
|
37
37
|
|
|
38
38
|
try {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
if (process.platform === "win32") {
|
|
40
|
+
execSync(
|
|
41
|
+
`powershell -NoProfile -Command "Start-Process -WindowStyle Hidden -FilePath '${process.execPath}' -ArgumentList '${scriptPath.replaceAll("'", "''")}'"`,
|
|
42
|
+
{ stdio: "ignore", timeout: 10000 },
|
|
43
|
+
);
|
|
44
|
+
} else {
|
|
45
|
+
// POSIX: detached node child + stdio:'ignore' so SessionStart 훅이 block 되지 않음
|
|
46
|
+
const child = spawn(process.execPath, [scriptPath], {
|
|
47
|
+
detached: true,
|
|
48
|
+
stdio: "ignore",
|
|
49
|
+
});
|
|
50
|
+
child.unref();
|
|
51
|
+
}
|
|
43
52
|
return true;
|
|
44
53
|
} catch {
|
|
45
54
|
return false;
|
|
@@ -51,18 +51,34 @@ function runScript(scriptPath, ...args) {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
function countSupergateways() {
|
|
54
|
+
if (process.platform === "win32") {
|
|
55
|
+
try {
|
|
56
|
+
// Write the query as a PS1 file to avoid shell quoting issues
|
|
57
|
+
const ps1 = [
|
|
58
|
+
`$procs = Get-CimInstance Win32_Process -Filter "Name='node.exe' OR Name='cmd.exe'"`,
|
|
59
|
+
`$hits = $procs | Where-Object { $_.CommandLine -match 'supergateway' }`,
|
|
60
|
+
`Write-Output $hits.Count`,
|
|
61
|
+
].join("\n");
|
|
62
|
+
const out = execSync(
|
|
63
|
+
`powershell -NoProfile -Command "${ps1.replace(/\n/g, "; ")}"`,
|
|
64
|
+
{ encoding: "utf8", timeout: 10000, stdio: ["pipe", "pipe", "ignore"] },
|
|
65
|
+
);
|
|
66
|
+
return parseInt(out.trim(), 10) || 0;
|
|
67
|
+
} catch {
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// POSIX: pgrep -f → 줄별 PID. macOS BSD pgrep 은 `-c` 미지원이라
|
|
73
|
+
// PID 목록을 받아 줄 수로 카운트한다. 0 match 시 exit 1.
|
|
54
74
|
try {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
`powershell -NoProfile -Command "${ps1.replace(/\n/g, "; ")}"`,
|
|
63
|
-
{ encoding: "utf8", timeout: 10000, stdio: ["pipe", "pipe", "ignore"] },
|
|
64
|
-
);
|
|
65
|
-
return parseInt(out.trim(), 10) || 0;
|
|
75
|
+
const out = execSync("pgrep -f supergateway", {
|
|
76
|
+
encoding: "utf8",
|
|
77
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
78
|
+
timeout: 5000,
|
|
79
|
+
});
|
|
80
|
+
const trimmed = out.trim();
|
|
81
|
+
return trimmed ? trimmed.split("\n").filter(Boolean).length : 0;
|
|
66
82
|
} catch {
|
|
67
83
|
return 0;
|
|
68
84
|
}
|