triflux 10.24.0 → 10.25.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/bin/triflux.mjs +56 -4
- package/hub/team/backend.mjs +44 -6
- package/hub/team/claude-daemon-control.mjs +160 -0
- package/hub/team/claude-native-bridge.mjs +574 -0
- package/hub/team/claude-session-projection.mjs +65 -0
- package/hub/team/cli/commands/start/index.mjs +4 -0
- package/hub/team/cli/commands/start/parse-args.mjs +20 -0
- package/hub/team/cli/commands/start/start-headless.mjs +12 -0
- package/hub/team/headless-bridge-session.mjs +17 -0
- package/hub/team/headless.mjs +400 -50
- package/hud/constants.mjs +6 -0
- package/hud/hud-qos-status.mjs +11 -3
- package/hud/renderers.mjs +5 -11
- package/package.json +4 -2
- package/scripts/__tests__/install-mcp-gateway-startup.test.mjs +343 -0
- package/scripts/__tests__/mcp-guard-engine-http-headers.test.mjs +49 -3
- package/scripts/__tests__/mcp-guard-engine-policy-sync.test.mjs +203 -0
- package/scripts/__tests__/mcp-guard-engine-stdio-sync.test.mjs +171 -0
- package/scripts/__tests__/mcp-guard-engine-sync-http-headers.test.mjs +47 -1
- package/scripts/__tests__/mcp-guard-engine-watch-http-headers.test.mjs +3 -4
- package/scripts/__tests__/mcp-guard-engine.test.mjs +87 -2
- package/scripts/__tests__/tfx-route-bash-node-parity.test.mjs +169 -0
- package/scripts/__tests__/tfx-route-node-entry.test.mjs +403 -0
- package/scripts/__tests__/tfx-route-phase1-modules.test.mjs +268 -0
- package/scripts/codex-mcp-gateway-sync.mjs +1 -1
- package/scripts/headless-guard.mjs +9 -4
- package/scripts/install-mcp-gateway-startup.mjs +501 -0
- package/scripts/lib/agent-json.mjs +27 -0
- package/scripts/lib/async.mjs +174 -0
- package/scripts/lib/cli-agy.mjs +62 -0
- package/scripts/lib/cli-claude.mjs +78 -0
- package/scripts/lib/cli-codex.mjs +199 -0
- package/scripts/lib/cli-gemini.mjs +67 -0
- package/scripts/lib/env.mjs +14 -0
- package/scripts/lib/hub.mjs +48 -0
- package/scripts/lib/mcp-gateway-servers.mjs +70 -0
- package/scripts/lib/mcp-guard-engine.mjs +524 -25
- package/scripts/lib/mcp-manifest.mjs +2 -2
- package/scripts/lib/pid.mjs +63 -0
- package/scripts/lib/quota.mjs +47 -0
- package/scripts/lib/team.mjs +75 -0
- package/scripts/lib/timeout.mjs +66 -0
- package/scripts/lib/tmp.mjs +26 -0
- package/scripts/lib/toml.mjs +113 -0
- package/scripts/mcp-gateway-config.mjs +2 -31
- package/scripts/mcp-gateway-ensure.mjs +24 -6
- package/scripts/mcp-gateway-start.mjs +16 -59
- package/scripts/mcp-safety-guard.mjs +3 -1
- package/scripts/preflight-cache.mjs +79 -9
- package/scripts/setup.mjs +40 -7
- package/scripts/sync-hub-mcp-settings.mjs +353 -28
- package/scripts/tfx-route.mjs +289 -0
- package/scripts/tfx-route.sh +124 -19
package/bin/triflux.mjs
CHANGED
|
@@ -61,6 +61,7 @@ import { serializeHandoff } from "../scripts/lib/handoff.mjs";
|
|
|
61
61
|
import {
|
|
62
62
|
addRegistryServer,
|
|
63
63
|
createDefaultRegistry,
|
|
64
|
+
discoverProjectMcpTargets,
|
|
64
65
|
inspectRegistry,
|
|
65
66
|
inspectRegistryStatus,
|
|
66
67
|
removeRegistryServer,
|
|
@@ -333,7 +334,8 @@ const CLI_COMMAND_SCHEMAS = Object.freeze({
|
|
|
333
334
|
],
|
|
334
335
|
},
|
|
335
336
|
sync: {
|
|
336
|
-
usage:
|
|
337
|
+
usage:
|
|
338
|
+
"tfx mcp sync [--json] [--all-projects [root]] [--dry-run] [--exclude <glob>]",
|
|
337
339
|
options: [
|
|
338
340
|
{
|
|
339
341
|
name: "--json",
|
|
@@ -2515,11 +2517,12 @@ function buildMcpStatusRows(statusInfo) {
|
|
|
2515
2517
|
.filter((row) => row.type === "registry")
|
|
2516
2518
|
.map((row) => {
|
|
2517
2519
|
let detail = "";
|
|
2518
|
-
if (row.status === "present")
|
|
2520
|
+
if (row.status === "present")
|
|
2521
|
+
detail = row.actualUrl || row.actualCommand || row.expectedUrl;
|
|
2519
2522
|
else if (row.status === "missing") detail = "registry only";
|
|
2520
2523
|
else if (row.status === "missing-file") detail = "config missing";
|
|
2521
2524
|
else if (row.status === "mismatch")
|
|
2522
|
-
detail = `expected ${row.expectedUrl}`;
|
|
2525
|
+
detail = `expected ${row.expectedUrl || row.expectedCommand}`;
|
|
2523
2526
|
else if (row.status === "invalid-config") detail = "parse error";
|
|
2524
2527
|
else if (row.status === "stdio") detail = "configured as stdio";
|
|
2525
2528
|
return [
|
|
@@ -5776,10 +5779,45 @@ function cmdMcp(args = [], options = {}) {
|
|
|
5776
5779
|
|
|
5777
5780
|
case "sync": {
|
|
5778
5781
|
const registryState = ensureValidRegistryState();
|
|
5779
|
-
const
|
|
5782
|
+
const allProjectsIndex = args.indexOf("--all-projects");
|
|
5783
|
+
const allProjectsRoot =
|
|
5784
|
+
allProjectsIndex >= 0 &&
|
|
5785
|
+
args[allProjectsIndex + 1] &&
|
|
5786
|
+
!String(args[allProjectsIndex + 1]).startsWith("--")
|
|
5787
|
+
? args[allProjectsIndex + 1]
|
|
5788
|
+
: null;
|
|
5789
|
+
const dryRun = args.includes("--dry-run");
|
|
5790
|
+
const excludes = args.flatMap((arg, index) =>
|
|
5791
|
+
arg === "--exclude" && args[index + 1] ? [args[index + 1]] : [],
|
|
5792
|
+
);
|
|
5793
|
+
const allProjects =
|
|
5794
|
+
allProjectsIndex >= 0
|
|
5795
|
+
? discoverProjectMcpTargets({
|
|
5796
|
+
root: allProjectsRoot || undefined,
|
|
5797
|
+
exclude: excludes,
|
|
5798
|
+
})
|
|
5799
|
+
: null;
|
|
5800
|
+
const result = dryRun
|
|
5801
|
+
? { actions: [] }
|
|
5802
|
+
: syncRegistryTargets({
|
|
5803
|
+
registry: registryState.registry,
|
|
5804
|
+
...(allProjects ? { targets: allProjects.targets } : {}),
|
|
5805
|
+
});
|
|
5780
5806
|
if (json) {
|
|
5781
5807
|
printJson({
|
|
5782
5808
|
registry_path: registryState.path,
|
|
5809
|
+
...(allProjects
|
|
5810
|
+
? {
|
|
5811
|
+
dry_run: dryRun,
|
|
5812
|
+
all_projects: {
|
|
5813
|
+
root: allProjects.root,
|
|
5814
|
+
maxDepth: allProjects.maxDepth,
|
|
5815
|
+
exclude: allProjects.exclude,
|
|
5816
|
+
count: allProjects.targets.length,
|
|
5817
|
+
},
|
|
5818
|
+
targets: allProjects.targets,
|
|
5819
|
+
}
|
|
5820
|
+
: {}),
|
|
5783
5821
|
actions: result.actions,
|
|
5784
5822
|
});
|
|
5785
5823
|
return;
|
|
@@ -5787,8 +5825,22 @@ function cmdMcp(args = [], options = {}) {
|
|
|
5787
5825
|
|
|
5788
5826
|
console.log(`\n ${AMBER}${BOLD}⬡ triflux mcp sync${RESET} ${VER}\n`);
|
|
5789
5827
|
console.log(` ${LINE}`);
|
|
5828
|
+
if (allProjects) {
|
|
5829
|
+
section("Project Targets");
|
|
5830
|
+
info(`${allProjects.targets.length}개 파일 (${allProjects.root})`);
|
|
5831
|
+
for (const target of allProjects.targets) {
|
|
5832
|
+
info(formatPathForDisplay(target.filePath));
|
|
5833
|
+
}
|
|
5834
|
+
if (dryRun) {
|
|
5835
|
+
console.log("");
|
|
5836
|
+
return;
|
|
5837
|
+
}
|
|
5838
|
+
}
|
|
5790
5839
|
section("Actions");
|
|
5791
5840
|
for (const action of result.actions) {
|
|
5841
|
+
for (const warning of action.warnings || []) {
|
|
5842
|
+
process.stderr.write(`${warning}\n`);
|
|
5843
|
+
}
|
|
5792
5844
|
const label = `${action.label} ${DIM}(${formatPathForDisplay(action.filePath)})${RESET}`;
|
|
5793
5845
|
if (action.status === "updated") ok(`${label} → updated`);
|
|
5794
5846
|
else if (action.status === "warning") warn(`${label} → warning`);
|
package/hub/team/backend.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// hub/team/backend.mjs — CLI 백엔드 추상화 레이어
|
|
2
|
-
// 각 CLI(codex/gemini/claude)의 명령 빌드 로직을 클래스로 캡슐화한다.
|
|
2
|
+
// 각 CLI(codex/gemini/claude/antigravity)의 명령 빌드 로직을 클래스로 캡슐화한다.
|
|
3
3
|
// v7.2.2
|
|
4
|
+
import { writeFileSync } from "node:fs";
|
|
4
5
|
import { createRequire } from "node:module";
|
|
5
6
|
|
|
6
7
|
import { buildExecArgs } from "../codex-adapter.mjs";
|
|
@@ -16,6 +17,22 @@ export function buildGeminiCommand(prompt, resultFile, { isWindows } = {}) {
|
|
|
16
17
|
return `gemini --yolo --prompt ${prompt} --output-format text > '${resultFile}' 2>'${resultFile}.err' < /dev/null`;
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
export function buildAntigravityCommand(
|
|
21
|
+
prompt,
|
|
22
|
+
resultFile,
|
|
23
|
+
{ isWindows } = {},
|
|
24
|
+
) {
|
|
25
|
+
// Persist prompt to a sibling file so the shell never interpolates raw
|
|
26
|
+
// prompt content. Without this the Unix branch let `$()`, backticks, `\\`,
|
|
27
|
+
// and `"` reach the shell verbatim (P1: shell injection).
|
|
28
|
+
const promptFile = `${resultFile}.prompt`;
|
|
29
|
+
writeFileSync(promptFile, prompt);
|
|
30
|
+
if (isWindows) {
|
|
31
|
+
return `Get-Content -Raw '${promptFile}' | agy --print --dangerously-skip-permissions > '${resultFile}' 2>'${resultFile}.err'`;
|
|
32
|
+
}
|
|
33
|
+
return `agy --print --dangerously-skip-permissions < '${promptFile}' > '${resultFile}' 2>'${resultFile}.err'`;
|
|
34
|
+
}
|
|
35
|
+
|
|
19
36
|
const _require = createRequire(import.meta.url);
|
|
20
37
|
|
|
21
38
|
// ── 백엔드 클래스 ──────────────────────────────────────────────────────────
|
|
@@ -77,19 +94,40 @@ export class ClaudeBackend {
|
|
|
77
94
|
}
|
|
78
95
|
}
|
|
79
96
|
|
|
97
|
+
export class AntigravityBackend {
|
|
98
|
+
name() {
|
|
99
|
+
return "antigravity";
|
|
100
|
+
}
|
|
101
|
+
command() {
|
|
102
|
+
return "agy";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
buildArgs(prompt, resultFile, opts = {}) {
|
|
106
|
+
return buildAntigravityCommand(prompt, resultFile, {
|
|
107
|
+
isWindows: IS_WINDOWS,
|
|
108
|
+
...opts,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
env() {
|
|
113
|
+
return {};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
80
117
|
// ── 레지스트리 ─────────────────────────────────────────────────────────────
|
|
81
118
|
|
|
82
|
-
/** @type {Map<string, CodexBackend|GeminiBackend|ClaudeBackend>} */
|
|
119
|
+
/** @type {Map<string, CodexBackend|GeminiBackend|ClaudeBackend|AntigravityBackend>} */
|
|
83
120
|
const backends = new Map([
|
|
84
121
|
["codex", new CodexBackend()],
|
|
85
122
|
["gemini", new GeminiBackend()],
|
|
86
123
|
["claude", new ClaudeBackend()],
|
|
124
|
+
["antigravity", new AntigravityBackend()],
|
|
87
125
|
]);
|
|
88
126
|
|
|
89
127
|
/**
|
|
90
128
|
* 백엔드 이름으로 조회한다.
|
|
91
|
-
* @param {string} name — "codex" | "gemini" | "claude"
|
|
92
|
-
* @returns {CodexBackend|GeminiBackend|ClaudeBackend}
|
|
129
|
+
* @param {string} name — "codex" | "gemini" | "claude" | "antigravity"
|
|
130
|
+
* @returns {CodexBackend|GeminiBackend|ClaudeBackend|AntigravityBackend}
|
|
93
131
|
* @throws {Error} 등록되지 않은 이름
|
|
94
132
|
*/
|
|
95
133
|
export function getBackend(name) {
|
|
@@ -102,7 +140,7 @@ export function getBackend(name) {
|
|
|
102
140
|
* 에이전트명 또는 CLI명을 Backend로 해석한다.
|
|
103
141
|
* agent-map.json을 통해 에이전트명 → CLI명으로 변환 후 레지스트리에서 조회한다.
|
|
104
142
|
* @param {string} agentOrCli — "executor", "codex", "designer" 등
|
|
105
|
-
* @returns {CodexBackend|GeminiBackend|ClaudeBackend}
|
|
143
|
+
* @returns {CodexBackend|GeminiBackend|ClaudeBackend|AntigravityBackend}
|
|
106
144
|
*/
|
|
107
145
|
export function getBackendForAgent(agentOrCli) {
|
|
108
146
|
const agentMap = _require("./agent-map.json");
|
|
@@ -112,7 +150,7 @@ export function getBackendForAgent(agentOrCli) {
|
|
|
112
150
|
|
|
113
151
|
/**
|
|
114
152
|
* 등록된 모든 백엔드를 반환한다.
|
|
115
|
-
* @returns {Array<CodexBackend|GeminiBackend|ClaudeBackend>}
|
|
153
|
+
* @returns {Array<CodexBackend|GeminiBackend|ClaudeBackend|AntigravityBackend>}
|
|
116
154
|
*/
|
|
117
155
|
export function listBackends() {
|
|
118
156
|
return Array.from(backends.values());
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import net from "node:net";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
export function resolveClaudeConfigDir(env = process.env) {
|
|
7
|
+
if (env.CLAUDE_CONFIG_DIR) return path.resolve(env.CLAUDE_CONFIG_DIR);
|
|
8
|
+
return path.join(os.homedir(), ".claude");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function deriveClaudeDaemonPaths({
|
|
12
|
+
configDir = resolveClaudeConfigDir(),
|
|
13
|
+
uid = typeof process.getuid === "function" ? process.getuid() : 0,
|
|
14
|
+
tmpRoot = "/tmp",
|
|
15
|
+
} = {}) {
|
|
16
|
+
const resolvedConfigDir = path.resolve(configDir);
|
|
17
|
+
const hash = crypto
|
|
18
|
+
.createHash("sha256")
|
|
19
|
+
.update(resolvedConfigDir)
|
|
20
|
+
.digest("hex")
|
|
21
|
+
.slice(0, 8);
|
|
22
|
+
const daemonDir = path.join(tmpRoot, `cc-daemon-${uid}`, hash);
|
|
23
|
+
return {
|
|
24
|
+
configDir: resolvedConfigDir,
|
|
25
|
+
hash,
|
|
26
|
+
daemonDir,
|
|
27
|
+
controlSock: path.join(daemonDir, "control.sock"),
|
|
28
|
+
rosterPath: path.join(resolvedConfigDir, "daemon", "roster.json"),
|
|
29
|
+
sessionsDir: path.join(resolvedConfigDir, "sessions"),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function readFirstJsonLine(text) {
|
|
34
|
+
const line = String(text)
|
|
35
|
+
.split("\n")
|
|
36
|
+
.find((entry) => entry.trim().length > 0);
|
|
37
|
+
if (!line) throw new Error("empty daemon response");
|
|
38
|
+
return JSON.parse(line);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function sendClaudeControlRequest(
|
|
42
|
+
sockPath,
|
|
43
|
+
request,
|
|
44
|
+
{ timeoutMs = 6000 } = {},
|
|
45
|
+
) {
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
const socket = net.connect(sockPath);
|
|
48
|
+
let settled = false;
|
|
49
|
+
let data = "";
|
|
50
|
+
const finish = (error, value) => {
|
|
51
|
+
if (settled) return;
|
|
52
|
+
settled = true;
|
|
53
|
+
socket.destroy();
|
|
54
|
+
if (error) reject(error);
|
|
55
|
+
else resolve(value);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
socket.setTimeout(timeoutMs, () => {
|
|
59
|
+
finish(
|
|
60
|
+
new Error(`Timed out waiting for Claude daemon response: ${sockPath}`),
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
socket.on("error", finish);
|
|
64
|
+
socket.on("connect", () => {
|
|
65
|
+
socket.write(`${JSON.stringify(request)}\n`);
|
|
66
|
+
});
|
|
67
|
+
socket.on("data", (chunk) => {
|
|
68
|
+
data += chunk.toString("utf8");
|
|
69
|
+
if (!data.includes("\n")) return;
|
|
70
|
+
try {
|
|
71
|
+
finish(null, readFirstJsonLine(data));
|
|
72
|
+
} catch (error) {
|
|
73
|
+
finish(error);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
socket.on("close", () => {
|
|
77
|
+
if (settled) return;
|
|
78
|
+
if (data.length === 0) {
|
|
79
|
+
finish(
|
|
80
|
+
new Error("Claude daemon closed control socket without a response"),
|
|
81
|
+
);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
finish(null, readFirstJsonLine(data));
|
|
86
|
+
} catch (error) {
|
|
87
|
+
finish(error);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function buildDaemonExecDispatchPayload({
|
|
94
|
+
short = crypto.randomBytes(4).toString("hex"),
|
|
95
|
+
sessionId,
|
|
96
|
+
cwd = process.cwd(),
|
|
97
|
+
command,
|
|
98
|
+
name,
|
|
99
|
+
createdAt = Date.now(),
|
|
100
|
+
cols = 120,
|
|
101
|
+
rows = 40,
|
|
102
|
+
} = {}) {
|
|
103
|
+
if (!command) throw new Error("command is required");
|
|
104
|
+
if (!name) throw new Error("name is required");
|
|
105
|
+
const uuid = crypto.randomUUID();
|
|
106
|
+
const resolvedSessionId = sessionId || `${short}${uuid.slice(8)}`;
|
|
107
|
+
return {
|
|
108
|
+
proto: 1,
|
|
109
|
+
short,
|
|
110
|
+
sessionId: resolvedSessionId,
|
|
111
|
+
createdAt,
|
|
112
|
+
source: "shell",
|
|
113
|
+
cwd,
|
|
114
|
+
launch: {
|
|
115
|
+
mode: "exec",
|
|
116
|
+
cmd: "/bin/zsh",
|
|
117
|
+
args: ["-lc", command],
|
|
118
|
+
},
|
|
119
|
+
env: {},
|
|
120
|
+
isolation: "none",
|
|
121
|
+
respawnFlags: [],
|
|
122
|
+
seed: {
|
|
123
|
+
intent: name,
|
|
124
|
+
name,
|
|
125
|
+
},
|
|
126
|
+
cols,
|
|
127
|
+
rows,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function findDaemonJobByShort(listResponse, short) {
|
|
132
|
+
if (!Array.isArray(listResponse?.jobs)) return null;
|
|
133
|
+
return listResponse.jobs.find((job) => job?.short === short) || null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function waitForDaemonJobPid(
|
|
137
|
+
controlSock,
|
|
138
|
+
short,
|
|
139
|
+
{ timeoutMs = 5000 } = {},
|
|
140
|
+
) {
|
|
141
|
+
const deadline = Date.now() + timeoutMs;
|
|
142
|
+
while (Date.now() < deadline) {
|
|
143
|
+
const list = await sendClaudeControlRequest(controlSock, {
|
|
144
|
+
proto: 1,
|
|
145
|
+
op: "list",
|
|
146
|
+
});
|
|
147
|
+
const job = findDaemonJobByShort(list, short);
|
|
148
|
+
if (Number.isInteger(job?.pid) && job.pid > 0) return job;
|
|
149
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
150
|
+
}
|
|
151
|
+
throw new Error(`Claude daemon dispatch did not expose a pid for ${short}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function killDaemonJob(controlSock, short) {
|
|
155
|
+
return await sendClaudeControlRequest(controlSock, {
|
|
156
|
+
proto: 1,
|
|
157
|
+
op: "kill",
|
|
158
|
+
short,
|
|
159
|
+
});
|
|
160
|
+
}
|