triflux 2.4.7 → 3.0.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.
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  // MCP 인벤토리 — 백그라운드 비동기 실행
3
- // Codex/Gemini의 MCP 서버 상태를 캐싱하여 cli-route.sh가 동적 힌트 생성에 사용
3
+ // Codex/Gemini의 MCP 서버 상태를 캐싱하여 tfx-route.sh가 동적 힌트 생성에 사용
4
4
  //
5
5
  // 출력: ~/.claude/cache/mcp-inventory.json
6
- // 사용: cli-route.sh의 get_mcp_hint()에서 읽음
6
+ // 사용: tfx-route.sh의 get_mcp_hint()에서 읽음
7
7
 
8
8
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
9
9
  import { join } from "path";
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
- // notion-read.mjs v1.1 — Notion 대형 페이지 리더 (Codex/Gemini/Claude MCP 위임)
2
+ // notion-read.mjs v1.2 — Notion 대형 페이지 리더 (Codex/Gemini/Claude MCP 위임)
3
3
  //
4
4
  // Codex/Gemini/Claude CLI에 설치된 Notion MCP를 활용하여 대형 페이지를 마크다운으로 추출.
5
5
  // 폴백 체인: Codex(무료) → Gemini(무료) → Claude(최후) → 에러
6
+ // 이관 모드(--delegate): Claude(notion-guest 우선) 단독 실행 + 결과 파일 저장
6
7
  //
7
8
  // 사용법:
8
9
  // node notion-read.mjs <notion-url-or-page-id> [옵션]
@@ -14,27 +15,28 @@
14
15
  // --cli, -c <codex|gemini> CLI 강제 지정 (기본: 자동 + 폴백)
15
16
  // --depth, -d <n> 중첩 블록 최대 깊이 (기본: 3)
16
17
  // --guest notion-guest 통합 사용 (기본: notion)
18
+ // --delegate Claude 이관 모드 (notion-guest 우선, 파일 저장)
17
19
 
18
- import { execSync } from "child_process";
19
- import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync, unlinkSync } from "fs";
20
- import { join, dirname } from "path";
21
- import { homedir, tmpdir } from "os";
20
+ import { execSync } from 'child_process';
21
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync, unlinkSync } from 'fs';
22
+ import { join, dirname, resolve } from 'path';
23
+ import { homedir, tmpdir } from 'os';
22
24
 
23
- const VERSION = "1.1";
24
- const CLAUDE_DIR = join(homedir(), ".claude");
25
- const MCP_CACHE = join(CLAUDE_DIR, "cache", "mcp-inventory.json");
26
- const LOG_FILE = join(CLAUDE_DIR, "logs", "cli-route-stats.jsonl");
27
- const ACC_FILE = join(CLAUDE_DIR, "cache", "sv-accumulator.json");
25
+ const VERSION = '1.2';
26
+ const CLAUDE_DIR = join(homedir(), '.claude');
27
+ const MCP_CACHE = join(CLAUDE_DIR, 'cache', 'mcp-inventory.json');
28
+ const LOG_FILE = join(CLAUDE_DIR, 'logs', 'tfx-route-stats.jsonl');
29
+ const ACC_FILE = join(CLAUDE_DIR, 'cache', 'sv-accumulator.json');
28
30
 
29
31
  // ── ANSI 색상 ──
30
- const AMBER = "\x1b[38;5;214m";
31
- const GREEN = "\x1b[38;5;82m";
32
- const RED = "\x1b[38;5;196m";
33
- const YELLOW = "\x1b[33m";
34
- const DIM = "\x1b[2m";
35
- const BOLD = "\x1b[1m";
36
- const RESET = "\x1b[0m";
37
- const GRAY = "\x1b[38;5;245m";
32
+ const AMBER = '\x1b[38;5;214m';
33
+ const GREEN = '\x1b[38;5;82m';
34
+ const RED = '\x1b[38;5;196m';
35
+ const YELLOW = '\x1b[33m';
36
+ const DIM = '\x1b[2m';
37
+ const BOLD = '\x1b[1m';
38
+ const RESET = '\x1b[0m';
39
+ const GRAY = '\x1b[38;5;245m';
38
40
 
39
41
  // ── URL 파싱 ──
40
42
  function parseNotionUrl(input) {
@@ -165,24 +167,24 @@ ${mcpServer} MCP 서버의 도구를 사용하라.
165
167
  }
166
168
 
167
169
  // ── CLI 실행 (임시 파일 + execSync — Windows .cmd 호환) ──
168
- function runWithCli(cliType, prompt, timeout) {
169
- const cliName = cliType === "claude" ? "claude" : cliType === "codex" ? "codex" : "gemini";
170
+ function runWithCli(cliType, prompt, timeout, runMode = 'fg') {
171
+ const cliName = cliType === 'claude' ? 'claude' : cliType === 'codex' ? 'codex' : 'gemini';
170
172
  if (!cliExists(cliName)) {
171
- return { success: false, output: "", error: `${cliType} CLI 미설치`, cli: cliType };
173
+ return { success: false, output: '', error: `${cliType} CLI 미설치`, cli: cliType };
172
174
  }
173
175
 
174
176
  // 프롬프트를 임시 파일에 저장 (shell escaping 회피)
175
177
  const promptFile = join(tmpdir(), `notion-prompt-${Date.now()}.md`);
176
- writeFileSync(promptFile, prompt, "utf8");
177
- const promptPath = promptFile.replace(/\\/g, "/");
178
+ writeFileSync(promptFile, prompt, 'utf8');
179
+ const promptPath = promptFile.replace(/\\/g, '/');
178
180
 
179
181
  // CLI에 전달할 짧은 메타 프롬프트
180
182
  const metaPrompt = `Read the file at ${promptPath} and execute all instructions in it exactly as described. Output only the final markdown result.`;
181
183
 
182
184
  let cmd;
183
- if (cliType === "codex") {
185
+ if (cliType === 'codex') {
184
186
  cmd = `codex exec --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check "${metaPrompt}"`;
185
- } else if (cliType === "gemini") {
187
+ } else if (cliType === 'gemini') {
186
188
  cmd = `gemini -m gemini-3-flash-preview -y --allowed-mcp-server-names notion,notion-guest --prompt "${metaPrompt}"`;
187
189
  } else {
188
190
  // Claude CLI — print 모드 (MCP 도구 자동 접근)
@@ -192,16 +194,16 @@ function runWithCli(cliType, prompt, timeout) {
192
194
  console.error(`${AMBER}▸${RESET} ${cliType}로 실행 중... (timeout: ${timeout}s)`);
193
195
  const startTime = Date.now();
194
196
 
195
- let stdout = "";
196
- let stderr = "";
197
+ let stdout = '';
198
+ let stderr = '';
197
199
  let exitCode = 0;
198
200
 
199
201
  try {
200
202
  stdout = execSync(cmd, {
201
- encoding: "utf8",
203
+ encoding: 'utf8',
202
204
  timeout: (timeout + 30) * 1000,
203
205
  maxBuffer: 10 * 1024 * 1024,
204
- stdio: ["pipe", "pipe", "pipe"],
206
+ stdio: ['pipe', 'pipe', 'pipe'],
205
207
  cwd: process.cwd(),
206
208
  });
207
209
  } catch (e) {
@@ -216,7 +218,7 @@ function runWithCli(cliType, prompt, timeout) {
216
218
  try { unlinkSync(promptFile); } catch {}
217
219
 
218
220
  // 실행 로그 기록
219
- logExecution(cliType, exitCode, elapsed, timeout, stderr);
221
+ logExecution(cliType, exitCode, elapsed, timeout, stderr, runMode);
220
222
 
221
223
  if (exitCode === 0 && stdout) {
222
224
  return { success: true, output: stdout, cli: cliType, elapsed };
@@ -262,31 +264,31 @@ function cleanCodexOutput(raw) {
262
264
  return texts.join("\n");
263
265
  }
264
266
 
265
- // ── 실행 로그 (cli-route.sh 호환) ──
266
- function logExecution(cliType, exitCode, elapsed, timeout, stderr) {
267
+ // ── 실행 로그 (tfx-route.sh 호환) ──
268
+ function logExecution(cliType, exitCode, elapsed, timeout, stderr, runMode = 'fg') {
267
269
  try {
268
270
  const logDir = dirname(LOG_FILE);
269
271
  if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true });
270
272
 
271
273
  const ts = new Date().toISOString();
272
- const status = exitCode === 0 ? "success" : exitCode === 124 ? "timeout" : "failed";
274
+ const status = exitCode === 0 ? 'success' : exitCode === 124 ? 'timeout' : 'failed';
273
275
  const entry = JSON.stringify({
274
276
  ts,
275
- agent: "notion-read",
277
+ agent: 'notion-read',
276
278
  cli: cliType,
277
- effort: cliType === "codex" ? "high" : cliType === "claude" ? "sonnet" : "flash",
278
- run_mode: "fg",
279
- opus_oversight: "false",
279
+ effort: cliType === 'codex' ? 'high' : cliType === 'claude' ? 'sonnet' : 'flash',
280
+ run_mode: runMode,
281
+ opus_oversight: 'false',
280
282
  status,
281
283
  exit_code: exitCode,
282
284
  elapsed_sec: elapsed,
283
285
  timeout_sec: timeout,
284
- mcp_profile: "notion",
286
+ mcp_profile: runMode === 'delegate' ? 'notion-guest' : 'notion',
285
287
  input_tokens: 0,
286
288
  output_tokens: 0,
287
289
  total_tokens: 0,
288
290
  });
289
- appendFileSync(LOG_FILE, entry + "\n");
291
+ appendFileSync(LOG_FILE, entry + '\n');
290
292
  } catch {}
291
293
  }
292
294
 
@@ -309,6 +311,7 @@ function main() {
309
311
  --depth, -d <n> 중첩 블록 최대 깊이 (기본: 3)
310
312
  --comments 블록/페이지 댓글 포함
311
313
  --guest notion-guest 통합 사용
314
+ --delegate Claude 이관 모드 (notion-guest 우선, 파일 저장)
312
315
 
313
316
  ${BOLD}폴백 체인${RESET}
314
317
  Codex(무료) → Gemini(무료) → Claude(최후) → 에러
@@ -318,6 +321,8 @@ function main() {
318
321
  tfx notion-read abc123def456... --output page.md --comments
319
322
  tfx notion-read abc123def456... --cli gemini --timeout 900
320
323
  tfx notion-read abc123def456... --guest --comments
324
+ tfx notion-read abc123def456... --delegate
325
+ tfx notion-read abc123def456... --delegate --output .notion-cache/page.md
321
326
  `);
322
327
  return;
323
328
  }
@@ -330,6 +335,7 @@ function main() {
330
335
  let depth = 3;
331
336
  let useGuest = false;
332
337
  let includeComments = false;
338
+ let delegateMode = false;
333
339
 
334
340
  for (let i = 1; i < args.length; i++) {
335
341
  switch (args[i]) {
@@ -355,6 +361,9 @@ function main() {
355
361
  case "--comments":
356
362
  includeComments = true;
357
363
  break;
364
+ case '--delegate':
365
+ delegateMode = true;
366
+ break;
358
367
  }
359
368
  }
360
369
 
@@ -368,7 +377,62 @@ function main() {
368
377
  console.error(
369
378
  `${AMBER}▸${RESET} 페이지: ${parsed.pageId}${parsed.blockId ? ` (블록: ${parsed.blockId})` : ""}`,
370
379
  );
371
- console.error(`${GRAY} 통합: ${useGuest ? "notion-guest" : "notion"} | 깊이: ${depth} | 댓글: ${includeComments ? "O" : "X"} | 타임아웃: ${timeout}s${RESET}`);
380
+ console.error(`${GRAY} 통합: ${delegateMode ? 'notion-guest(우선)' : useGuest ? 'notion-guest' : 'notion'} | 깊이: ${depth} | 댓글: ${includeComments ? 'O' : 'X'} | 타임아웃: ${timeout}s${RESET}`);
381
+
382
+ // 프롬프트 생성
383
+ const prompt = buildPrompt(parsed.pageId, parsed.blockId, depth, useGuest, includeComments);
384
+ // Claude 폴백용: notion/notion-guest 양쪽 시도 프롬프트
385
+ const claudePrompt = buildPrompt(parsed.pageId, parsed.blockId, depth, false, includeComments)
386
+ .replace(
387
+ 'notion MCP 서버의 도구를 사용하라.',
388
+ '가능하면 notion-guest MCP 서버를 먼저 사용하라. 실패하면 notion MCP 서버를 사용하라.',
389
+ );
390
+
391
+ // delegate 모드: Claude 단독 + notion-guest 우선 + 파일 저장
392
+ if (delegateMode) {
393
+ console.error(`${AMBER}▸${RESET} delegate 모드 활성화: Claude로 notion-guest 우선 접근`);
394
+
395
+ const delegatePrompt = `${claudePrompt}
396
+
397
+ ### delegate 모드 추가 지시
398
+ - notion-guest MCP 서버를 최우선으로 먼저 시도하라.
399
+ - notion-guest가 실패하거나 미구성일 때만 notion 서버로 폴백하라.
400
+ - 도구 호출 결과를 바탕으로 최종 마크다운만 출력하라.`;
401
+
402
+ const delegateResult = runWithCli('claude', delegatePrompt, timeout, 'delegate');
403
+ if (!delegateResult.success) {
404
+ console.error(`${RED}✗${RESET} delegate 모드 실패: ${delegateResult.error}`);
405
+ if (delegateResult.stderr) {
406
+ console.error(`${GRAY} stderr: ${delegateResult.stderr.slice(0, 250)}${RESET}`);
407
+ }
408
+ console.error(`${GRAY} 대안: --delegate 없이 실행해 기존 폴백 체인을 사용하세요.${RESET}`);
409
+ console.error(`${GRAY} 예: tfx notion-read ${parsed.pageId} --comments${RESET}`);
410
+ process.exit(1);
411
+ }
412
+
413
+ const delegateOutput = delegateResult.output.trim();
414
+ const isDelegateFailureOutput =
415
+ (delegateOutput.includes('조회 실패') || delegateOutput.includes('읽기 실패') || delegateOutput.includes('not_found')) &&
416
+ delegateOutput.length < 500;
417
+
418
+ if (delegateOutput.length <= 100 || isDelegateFailureOutput) {
419
+ console.error(`${RED}✗${RESET} delegate 모드 실패: Claude 결과가 비정상적입니다.`);
420
+ console.error(`${GRAY} 대안: --delegate 없이 실행해 Codex/Gemini/Claude 폴백 체인을 사용하세요.${RESET}`);
421
+ process.exit(1);
422
+ }
423
+
424
+ const delegateTarget = outputFile || join('.notion-cache', `${parsed.pageId}.md`);
425
+ const delegateDir = dirname(delegateTarget);
426
+ if (delegateDir && delegateDir !== '.' && !existsSync(delegateDir)) {
427
+ mkdirSync(delegateDir, { recursive: true });
428
+ }
429
+ writeFileSync(delegateTarget, delegateOutput, 'utf8');
430
+
431
+ const savedPath = resolve(delegateTarget);
432
+ console.error(`${GREEN}✓${RESET} delegate 결과 저장: ${savedPath}`);
433
+ console.error(`${GRAY} 후속 작업 참조 경로: ${savedPath}${RESET}`);
434
+ return;
435
+ }
372
436
 
373
437
  // MCP 가용성 확인
374
438
  const mcpAvail = getNotionMcpClis(useGuest);
@@ -400,15 +464,6 @@ function main() {
400
464
  cliOrder.push("claude");
401
465
  }
402
466
 
403
- // 프롬프트 생성
404
- const prompt = buildPrompt(parsed.pageId, parsed.blockId, depth, useGuest, includeComments);
405
- // Claude 폴백용: notion/notion-guest 양쪽 시도 프롬프트
406
- const claudePrompt = buildPrompt(parsed.pageId, parsed.blockId, depth, false, includeComments)
407
- .replace(
408
- "404 에러가 나면 notion-guest 서버로 재시도하라.",
409
- "404 에러가 나면 반드시 notion-guest 서버로 재시도하라. notion, notion-guest, claude_ai_Notion 등 사용 가능한 모든 Notion MCP 서버를 시도하라.",
410
- );
411
-
412
467
  // 실행 + 폴백
413
468
  let lastResult = null;
414
469
  let notionAccessFailed = false;
package/scripts/setup.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // triflux 세션 시작 시 자동 설정 스크립트
3
- // - cli-route.sh를 ~/.claude/scripts/에 동기화
3
+ // - tfx-route.sh를 ~/.claude/scripts/에 동기화
4
4
  // - hud-qos-status.mjs를 ~/.claude/hud/에 동기화
5
5
  // - skills/를 ~/.claude/skills/에 동기화
6
6
 
@@ -15,9 +15,14 @@ const CLAUDE_DIR = join(homedir(), ".claude");
15
15
 
16
16
  const SYNC_MAP = [
17
17
  {
18
- src: join(PLUGIN_ROOT, "scripts", "cli-route.sh"),
19
- dst: join(CLAUDE_DIR, "scripts", "cli-route.sh"),
20
- label: "cli-route.sh",
18
+ src: join(PLUGIN_ROOT, "scripts", "tfx-route.sh"),
19
+ dst: join(CLAUDE_DIR, "scripts", "tfx-route.sh"),
20
+ label: "tfx-route.sh",
21
+ },
22
+ {
23
+ src: join(PLUGIN_ROOT, "scripts", "tfx-route-post.mjs"),
24
+ dst: join(CLAUDE_DIR, "scripts", "tfx-route-post.mjs"),
25
+ label: "tfx-route-post.mjs",
21
26
  },
22
27
  {
23
28
  src: join(PLUGIN_ROOT, "hud", "hud-qos-status.mjs"),
@@ -212,7 +217,7 @@ ${B}╔════════════════════════
212
217
  ${B}║${R} ${C}triflux${R} ${D}v${ver}${R} ${B}— Setup Complete${R} ${B}║${R}
213
218
  ${B}╚═══════════════════════════════════════════════╝${R}
214
219
 
215
- ${G}✓${R} cli-route.sh → ~/.claude/scripts/
220
+ ${G}✓${R} tfx-route.sh → ~/.claude/scripts/
216
221
  ${G}✓${R} hud-qos-status → ~/.claude/hud/
217
222
  ${G}✓${R} ${synced > 0 ? synced + " files synced" : "all files up to date"}
218
223
  ${G}✓${R} HUD statusLine → settings.json
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env node
2
+ // tfx-batch-stats.mjs v1.0 — batch-events.jsonl 소비자
3
+ //
4
+ // tfx-route-post.mjs가 기록한 AIMD 이벤트를 읽어서:
5
+ // 1. 에이전트별 성공/실패/타임아웃 통계 집계
6
+ // 2. AIMD(Additive Increase / Multiplicative Decrease) batch_size 계산
7
+ //
8
+ // 사용법:
9
+ // node tfx-batch-stats.mjs stats [--recent] 에이전트별 통계 (--recent: 30분)
10
+ // node tfx-batch-stats.mjs batch 현재 권장 batch_size
11
+ // node tfx-batch-stats.mjs agent <name> 특정 에이전트 통계
12
+
13
+ import { readFileSync } from "node:fs";
14
+ import { join } from "node:path";
15
+ import { homedir } from "node:os";
16
+
17
+ const CACHE_DIR = join(homedir(), ".claude", "cache");
18
+ const EVENTS_FILE = join(CACHE_DIR, "batch-events.jsonl");
19
+
20
+ // AIMD 파라미터
21
+ const AIMD_INITIAL = 3;
22
+ const AIMD_MIN = 1;
23
+ const AIMD_MAX = 10;
24
+ const AIMD_INC = 1; // 성공 시 +1
25
+ const AIMD_DEC = 0.5; // 실패 시 ×0.5
26
+ const WINDOW_MS = 30 * 60 * 1000; // 30분 윈도우
27
+
28
+ // ── 이벤트 읽기 ──
29
+ export function readBatchEvents(opts = {}) {
30
+ const { sinceMs = 0, agent = null } = opts;
31
+ try {
32
+ const lines = readFileSync(EVENTS_FILE, "utf-8").trim().split("\n").filter(Boolean);
33
+ return lines
34
+ .map((l) => { try { return JSON.parse(l); } catch { return null; } })
35
+ .filter((e) => e && (!sinceMs || e.ts >= sinceMs) && (!agent || e.agent === agent));
36
+ } catch {
37
+ return [];
38
+ }
39
+ }
40
+
41
+ // ── 에이전트별 통계 ──
42
+ export function getAgentStats(opts = {}) {
43
+ const events = readBatchEvents(opts);
44
+ const stats = {};
45
+
46
+ for (const ev of events) {
47
+ if (!stats[ev.agent]) stats[ev.agent] = { success: 0, fail: 0, timeout: 0, total: 0 };
48
+ const s = stats[ev.agent];
49
+ if (ev.result === "success" || ev.result === "success_with_warnings") s.success++;
50
+ else if (ev.result === "timeout") s.timeout++;
51
+ else s.fail++;
52
+ s.total++;
53
+ }
54
+
55
+ for (const s of Object.values(stats)) {
56
+ s.successRate = s.total > 0 ? +(s.success / s.total).toFixed(2) : 0;
57
+ }
58
+
59
+ return { total: events.length, agents: stats };
60
+ }
61
+
62
+ // ── AIMD batch_size 계산 ──
63
+ // 최근 윈도우 이벤트를 순회하며 성공 시 +1, 실패 시 ×0.5
64
+ export function getAimdBatchSize() {
65
+ const since = Date.now() - WINDOW_MS;
66
+ const events = readBatchEvents({ sinceMs: since });
67
+ if (events.length === 0) return AIMD_INITIAL;
68
+
69
+ let batch = AIMD_INITIAL;
70
+ for (const ev of events) {
71
+ if (ev.result === "success" || ev.result === "success_with_warnings") {
72
+ batch = Math.min(AIMD_MAX, batch + AIMD_INC);
73
+ } else {
74
+ batch = Math.max(AIMD_MIN, Math.floor(batch * AIMD_DEC));
75
+ }
76
+ }
77
+ return batch;
78
+ }
79
+
80
+ // ── CLI 진입점 ──
81
+ const scriptName = process.argv[1] || "";
82
+ if (scriptName.endsWith("tfx-batch-stats.mjs")) {
83
+ const cmd = process.argv[2] || "stats";
84
+ const recent = process.argv.includes("--recent");
85
+ const sinceMs = recent ? Date.now() - WINDOW_MS : 0;
86
+
87
+ if (cmd === "batch") {
88
+ console.log(JSON.stringify({ batchSize: getAimdBatchSize(), window: "30m" }));
89
+ } else if (cmd === "agent") {
90
+ const name = process.argv[3];
91
+ if (!name) { console.error("에이전트명 필수: node tfx-batch-stats.mjs agent executor"); process.exit(1); }
92
+ console.log(JSON.stringify(getAgentStats({ sinceMs, agent: name }), null, 2));
93
+ } else {
94
+ console.log(JSON.stringify(getAgentStats({ sinceMs }), null, 2));
95
+ }
96
+ }