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.
Files changed (53) hide show
  1. package/bin/triflux.mjs +56 -4
  2. package/hub/team/backend.mjs +44 -6
  3. package/hub/team/claude-daemon-control.mjs +160 -0
  4. package/hub/team/claude-native-bridge.mjs +574 -0
  5. package/hub/team/claude-session-projection.mjs +65 -0
  6. package/hub/team/cli/commands/start/index.mjs +4 -0
  7. package/hub/team/cli/commands/start/parse-args.mjs +20 -0
  8. package/hub/team/cli/commands/start/start-headless.mjs +12 -0
  9. package/hub/team/headless-bridge-session.mjs +17 -0
  10. package/hub/team/headless.mjs +400 -50
  11. package/hud/constants.mjs +6 -0
  12. package/hud/hud-qos-status.mjs +11 -3
  13. package/hud/renderers.mjs +5 -11
  14. package/package.json +4 -2
  15. package/scripts/__tests__/install-mcp-gateway-startup.test.mjs +343 -0
  16. package/scripts/__tests__/mcp-guard-engine-http-headers.test.mjs +49 -3
  17. package/scripts/__tests__/mcp-guard-engine-policy-sync.test.mjs +203 -0
  18. package/scripts/__tests__/mcp-guard-engine-stdio-sync.test.mjs +171 -0
  19. package/scripts/__tests__/mcp-guard-engine-sync-http-headers.test.mjs +47 -1
  20. package/scripts/__tests__/mcp-guard-engine-watch-http-headers.test.mjs +3 -4
  21. package/scripts/__tests__/mcp-guard-engine.test.mjs +87 -2
  22. package/scripts/__tests__/tfx-route-bash-node-parity.test.mjs +169 -0
  23. package/scripts/__tests__/tfx-route-node-entry.test.mjs +403 -0
  24. package/scripts/__tests__/tfx-route-phase1-modules.test.mjs +268 -0
  25. package/scripts/codex-mcp-gateway-sync.mjs +1 -1
  26. package/scripts/headless-guard.mjs +9 -4
  27. package/scripts/install-mcp-gateway-startup.mjs +501 -0
  28. package/scripts/lib/agent-json.mjs +27 -0
  29. package/scripts/lib/async.mjs +174 -0
  30. package/scripts/lib/cli-agy.mjs +62 -0
  31. package/scripts/lib/cli-claude.mjs +78 -0
  32. package/scripts/lib/cli-codex.mjs +199 -0
  33. package/scripts/lib/cli-gemini.mjs +67 -0
  34. package/scripts/lib/env.mjs +14 -0
  35. package/scripts/lib/hub.mjs +48 -0
  36. package/scripts/lib/mcp-gateway-servers.mjs +70 -0
  37. package/scripts/lib/mcp-guard-engine.mjs +524 -25
  38. package/scripts/lib/mcp-manifest.mjs +2 -2
  39. package/scripts/lib/pid.mjs +63 -0
  40. package/scripts/lib/quota.mjs +47 -0
  41. package/scripts/lib/team.mjs +75 -0
  42. package/scripts/lib/timeout.mjs +66 -0
  43. package/scripts/lib/tmp.mjs +26 -0
  44. package/scripts/lib/toml.mjs +113 -0
  45. package/scripts/mcp-gateway-config.mjs +2 -31
  46. package/scripts/mcp-gateway-ensure.mjs +24 -6
  47. package/scripts/mcp-gateway-start.mjs +16 -59
  48. package/scripts/mcp-safety-guard.mjs +3 -1
  49. package/scripts/preflight-cache.mjs +79 -9
  50. package/scripts/setup.mjs +40 -7
  51. package/scripts/sync-hub-mcp-settings.mjs +353 -28
  52. package/scripts/tfx-route.mjs +289 -0
  53. 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: "tfx mcp sync [--json]",
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") detail = row.actualUrl || row.expectedUrl;
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 result = syncRegistryTargets({ registry: registryState.registry });
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`);
@@ -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
+ }