triflux 8.5.1 → 8.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/bin/triflux.mjs CHANGED
@@ -735,7 +735,7 @@ function previewMcpRegistrationActions(mcpUrl) {
735
735
  type: "mcp-register",
736
736
  cli: "claude",
737
737
  target: "tfx-hub",
738
- path: join(PKG_ROOT, ".mcp.json"),
738
+ path: join(process.cwd(), ".claude", "mcp.json"),
739
739
  url: mcpUrl,
740
740
  change: "check",
741
741
  });
@@ -2161,16 +2161,18 @@ function autoRegisterMcp(mcpUrl) {
2161
2161
  info("Gemini: 미설치 (건너뜀)");
2162
2162
  }
2163
2163
 
2164
- // Claude — 프로젝트 .mcp.json에 등록 (오케스트레이터용)
2164
+ // Claude — .claude/mcp.json에 등록 (Claude Code 공식 경로)
2165
2165
  try {
2166
- const mcpJsonPath = join(PKG_ROOT, ".mcp.json");
2166
+ const claudeDir = join(process.cwd(), ".claude");
2167
+ if (!existsSync(claudeDir)) mkdirSync(claudeDir, { recursive: true });
2168
+ const mcpJsonPath = join(claudeDir, "mcp.json");
2167
2169
  let mcpJson = {};
2168
2170
  if (existsSync(mcpJsonPath)) mcpJson = JSON.parse(readFileSync(mcpJsonPath, "utf8"));
2169
2171
  if (!mcpJson.mcpServers) mcpJson.mcpServers = {};
2170
2172
  if (!mcpJson.mcpServers["tfx-hub"]) {
2171
2173
  mcpJson.mcpServers["tfx-hub"] = { type: "url", url: mcpUrl };
2172
2174
  writeFileSync(mcpJsonPath, JSON.stringify(mcpJson, null, 2) + "\n");
2173
- ok("Claude: .mcp.json에 등록 완료");
2175
+ ok("Claude: .claude/mcp.json에 등록 완료");
2174
2176
  } else {
2175
2177
  ok("Claude: 이미 등록됨");
2176
2178
  }
package/hub/team/ansi.mjs CHANGED
@@ -65,16 +65,92 @@ export function color(text, fg, bg) {
65
65
  export function bold(text) { return `${BOLD}${text}${RESET}`; }
66
66
  export function dim(text) { return `${DIM}${text}${RESET}`; }
67
67
 
68
+ export function lerpRgb(a, b, t) {
69
+ return {
70
+ r: Math.round(a.r + (b.r - a.r) * t),
71
+ g: Math.round(a.g + (b.g - a.g) * t),
72
+ b: Math.round(a.b + (b.b - a.b) * t),
73
+ };
74
+ }
75
+
76
+ function rgbSeq(rgb, mode = 38) {
77
+ return `${ESC}[${mode};2;${rgb.r};${rgb.g};${rgb.b}m`;
78
+ }
79
+
80
+ function brightenRgb(rgb, amount = 0.3) {
81
+ return lerpRgb(rgb, { r: 255, g: 255, b: 255 }, amount);
82
+ }
83
+
84
+ function parseRgbSeq(seq) {
85
+ const match = typeof seq === "string"
86
+ ? seq.match(/\x1b\[(?:38|48);2;(\d+);(\d+);(\d+)m/)
87
+ : null;
88
+ if (!match) return null;
89
+ return {
90
+ r: Number.parseInt(match[1], 10),
91
+ g: Number.parseInt(match[2], 10),
92
+ b: Number.parseInt(match[3], 10),
93
+ };
94
+ }
95
+
96
+ function reapplyBackground(text, bgSeq) {
97
+ if (!bgSeq) return text;
98
+ return `${bgSeq}${String(text).replaceAll(RESET, `${RESET}${bgSeq}`)}${RESET}`;
99
+ }
100
+
68
101
  // ── 박스 그리기 (유니코드 테두리) ──
69
102
  const BOX = { tl: "┌", tr: "┐", bl: "└", br: "┘", h: "─", v: "│", ml: "├", mr: "┤" };
70
103
 
71
- export function box(lines, width, borderColor = "") {
72
- const bc = borderColor;
73
- const rst = bc ? RESET : "";
74
- const top = `${bc}${BOX.tl}${BOX.h.repeat(width - 2)}${BOX.tr}${rst}`;
75
- const bot = `${bc}${BOX.bl}${BOX.h.repeat(width - 2)}${BOX.br}${rst}`;
76
- const mid = `${bc}${BOX.ml}${BOX.h.repeat(width - 2)}${BOX.mr}${rst}`;
77
- const body = lines.map((l) => `${bc}${BOX.v}${rst} ${padRight(l, width - 4)} ${bc}${BOX.v}${rst}`);
104
+ function borderHighlightCell(width, totalRows, highlightPos) {
105
+ if (!Number.isFinite(highlightPos)) return null;
106
+ const perimeter = 2 * (width - 2) + 2 * totalRows;
107
+ if (perimeter <= 0) return null;
108
+ let pos = Math.floor(highlightPos) % perimeter;
109
+ if (pos < 0) pos += perimeter;
110
+
111
+ if (pos < width - 2) return { row: 0, col: pos + 1 };
112
+ pos -= width - 2;
113
+ if (pos < totalRows) return { row: pos, col: width - 1 };
114
+ pos -= totalRows;
115
+ if (pos < width - 2) return { row: totalRows - 1, col: width - 2 - pos };
116
+ pos -= width - 2;
117
+ return { row: totalRows - 1 - pos, col: 0 };
118
+ }
119
+
120
+ function renderBorderChar(glyph, row, col, highlightCell, borderSeq, highlightSeq) {
121
+ if (highlightCell && highlightCell.row === row && highlightCell.col === col) {
122
+ return `${highlightSeq}${glyph}${RESET}`;
123
+ }
124
+ return borderSeq ? `${borderSeq}${glyph}${RESET}` : glyph;
125
+ }
126
+
127
+ export function box(lines, width, borderColor = "", options = {}) {
128
+ const isFn = typeof borderColor === "function";
129
+ const totalRows = lines.length + 2;
130
+ const bc = isFn ? (row) => borderColor(row, totalRows) : () => borderColor;
131
+ const rst = (isFn || borderColor) ? RESET : "";
132
+ const highlightCell = borderHighlightCell(width, totalRows, options.highlightPos);
133
+ const highlightSeq = options.highlightColor
134
+ || (() => {
135
+ const parsed = parseRgbSeq(typeof borderColor === "string" ? borderColor : "");
136
+ return parsed ? rgbSeq(brightenRgb(parsed, 0.45)) : `${BOLD}${FG.white}`;
137
+ })();
138
+ const topChars = [BOX.tl, ...Array.from({ length: width - 2 }, () => BOX.h), BOX.tr];
139
+ const botChars = [BOX.bl, ...Array.from({ length: width - 2 }, () => BOX.h), BOX.br];
140
+ const top = topChars
141
+ .map((glyph, col) => renderBorderChar(glyph, 0, col, highlightCell, bc(0), highlightSeq))
142
+ .join("");
143
+ const bot = botChars
144
+ .map((glyph, col) => renderBorderChar(glyph, totalRows - 1, col, highlightCell, bc(totalRows - 1), highlightSeq))
145
+ .join("");
146
+ const mid = `${bc(Math.floor(totalRows / 2))}${BOX.ml}${BOX.h.repeat(width - 2)}${BOX.mr}${rst}`;
147
+ const body = lines.map((l, i) => {
148
+ const row = i + 1;
149
+ const content = options.titleFlashBg && i === 0
150
+ ? reapplyBackground(padRight(l, width - 4), options.titleFlashBg)
151
+ : padRight(l, width - 4);
152
+ return `${renderBorderChar(BOX.v, row, 0, highlightCell, bc(row), highlightSeq)} ${content} ${renderBorderChar(BOX.v, row, width - 1, highlightCell, bc(row), highlightSeq)}`;
153
+ });
78
154
  return { top, body, bot, mid };
79
155
  }
80
156
 
@@ -205,6 +281,12 @@ export const MOCHA = {
205
281
  surface0: `${ESC}[38;2;49;50;68m`, // #313244 surface0
206
282
  };
207
283
 
284
+ const MOCHA_RGB = {
285
+ ok: { r: 166, g: 227, b: 161 },
286
+ partial: { r: 250, g: 179, b: 135 },
287
+ fail: { r: 243, g: 139, b: 168 },
288
+ };
289
+
208
290
  // ── badge 헬퍼 ──
209
291
  // statusBadge(status) → ANSI 색상 문자열
210
292
  export function statusBadge(status) {
@@ -231,13 +313,48 @@ export function statusBadge(status) {
231
313
  }
232
314
 
233
315
  // ── 진행률 바 ──
234
- // progressBar(percent, width) — percent: 0~100, ANSI colored bar string 반환
235
- export function progressBar(percent, width = 20) {
316
+ // progressBar(percent, width, time) — percent: 0~100, time 전달 shimmer sweep
317
+ export function progressBar(percent, width = 20, time) {
318
+ const ratio = Math.max(0, Math.min(100, percent)) / 100;
319
+ const filled = Math.round(ratio * width);
320
+ const empty = width - filled;
321
+ const fillRgb = percent >= 100 ? MOCHA_RGB.ok : percent >= 50 ? MOCHA_RGB.partial : MOCHA_RGB.fail;
322
+ const fillColor = rgbSeq(fillRgb);
323
+ let fillText = "█".repeat(filled);
324
+
325
+ if (filled > 0 && Number.isFinite(time)) {
326
+ const shinePos = Math.min(
327
+ filled - 1,
328
+ Math.floor(((((time % 2000) + 2000) % 2000) / 2000) * filled),
329
+ );
330
+ const shineColor = rgbSeq(brightenRgb(fillRgb, 0.3));
331
+ fillText = Array.from({ length: filled }, (_, idx) =>
332
+ idx === shinePos ? `${shineColor}█${RESET}` : `${fillColor}█${RESET}`
333
+ ).join("");
334
+ } else if (filled > 0) {
335
+ fillText = `${fillColor}${fillText}${RESET}`;
336
+ }
337
+
338
+ const emptyText = empty > 0 ? `${MOCHA.border}${"░".repeat(empty)}${RESET}` : "";
339
+ return `${fillText}${emptyText}`;
340
+ }
341
+
342
+ // ── 애니메이션 진행률 바 (shimmer sweep) ──
343
+ export function animatedProgressBar(percent, width = 20, tick = 0) {
236
344
  const ratio = Math.max(0, Math.min(100, percent)) / 100;
237
345
  const filled = Math.round(ratio * width);
238
346
  const empty = width - filled;
239
- const fillColor = percent >= 100 ? MOCHA.ok : percent >= 50 ? MOCHA.partial : MOCHA.fail;
240
- return `${fillColor}${"█".repeat(filled)}${MOCHA.border}${"░".repeat(empty)}${RESET}`;
347
+ if (filled === 0 || percent >= 100) return progressBar(percent, width);
348
+ const baseClr = percent >= 50 ? MOCHA.partial : MOCHA.fail;
349
+ const pos = tick % (filled + 3);
350
+ let bar = "";
351
+ for (let i = 0; i < filled; i++) {
352
+ const d = Math.abs(i - pos);
353
+ if (d === 0) bar += `${ESC}[97m█`;
354
+ else if (d === 1) bar += `${baseClr}▓`;
355
+ else bar += `${baseClr}█`;
356
+ }
357
+ return `${bar}${MOCHA.border}${"░".repeat(empty)}${RESET}`;
241
358
  }
242
359
 
243
360
  // ── 상태 아이콘 ──
@@ -253,3 +370,9 @@ export const CLI_ICON = {
253
370
  gemini: `${FG.gemini}🔵${RESET}`,
254
371
  claude: `${FG.claude}🟠${RESET}`,
255
372
  };
373
+
374
+ // ── 로딩 도트 (braille spinner) ──
375
+ const BRAILLE_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
376
+ export function loadingDots(tick = 0, clr = MOCHA.thinking) {
377
+ return `${clr}${BRAILLE_FRAMES[tick % BRAILLE_FRAMES.length]}${RESET}`;
378
+ }
@@ -32,7 +32,7 @@ export class GeminiBackend {
32
32
  command() { return "gemini"; }
33
33
 
34
34
  buildArgs(prompt, resultFile, opts = {}) {
35
- return `gemini -p ${prompt} --output-format text > '${resultFile}' 2>'${resultFile}.err'`;
35
+ return `gemini --prompt ${prompt} --output-format text > '${resultFile}' 2>'${resultFile}.err'`;
36
36
  }
37
37
 
38
38
  env() { return {}; }
@@ -758,7 +758,7 @@ export function startCapture(sessionName, paneNameOrTarget) {
758
758
  * CLI 명령(codex/gemini)이 psmux pane의 PowerShell 환경에서 단축 플래그 충돌을
759
759
  * 일으키는 문제를 방지하기 위해 bash -c '...' 로 감싼다.
760
760
  * - codex -o flag → PS -OutVariable/OutBuffer 충돌
761
- * - gemini -p flag → PS -ProgressAction/PipelineVariable 충돌
761
+ * - gemini --prompt flag (v8.6.0: -p → --prompt, PS 충돌 해소)
762
762
  * @param {string} cmd
763
763
  * @returns {string}
764
764
  */