triflux 3.2.0-dev.1 → 3.2.0-dev.3
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 +185 -43
- package/hooks/hooks.json +12 -0
- package/hub/bridge.mjs +137 -51
- package/hub/server.mjs +100 -29
- package/hub/team/cli.mjs +1080 -113
- package/hub/team/native-supervisor.mjs +300 -0
- package/hub/team/native.mjs +92 -0
- package/hub/team/nativeProxy.mjs +460 -0
- package/hub/team/orchestrator.mjs +99 -35
- package/hub/team/pane.mjs +30 -16
- package/hub/team/session.mjs +359 -16
- package/hub/tools.mjs +113 -15
- package/package.json +1 -1
- package/scripts/setup.mjs +95 -0
- package/scripts/team-keyword.mjs +35 -0
- package/scripts/tfx-route.sh +138 -102
- package/skills/tfx-team/SKILL.md +239 -152
package/bin/triflux.mjs
CHANGED
|
@@ -7,8 +7,27 @@ import { execSync, spawn } from "child_process";
|
|
|
7
7
|
|
|
8
8
|
const PKG_ROOT = dirname(dirname(new URL(import.meta.url).pathname)).replace(/^\/([A-Z]:)/, "$1");
|
|
9
9
|
const CLAUDE_DIR = join(homedir(), ".claude");
|
|
10
|
+
const CODEX_DIR = join(homedir(), ".codex");
|
|
11
|
+
const CODEX_CONFIG_PATH = join(CODEX_DIR, "config.toml");
|
|
10
12
|
const PKG = JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf8"));
|
|
11
13
|
|
|
14
|
+
const REQUIRED_CODEX_PROFILES = [
|
|
15
|
+
{
|
|
16
|
+
name: "xhigh",
|
|
17
|
+
lines: [
|
|
18
|
+
'model = "gpt-5.3-codex"',
|
|
19
|
+
'model_reasoning_effort = "xhigh"',
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: "spark_fast",
|
|
24
|
+
lines: [
|
|
25
|
+
'model = "gpt-5.1-codex-mini"',
|
|
26
|
+
'model_reasoning_effort = "low"',
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
];
|
|
30
|
+
|
|
12
31
|
// ── 색상 체계 (triflux brand: amber/orange accent) ──
|
|
13
32
|
const CYAN = "\x1b[36m";
|
|
14
33
|
const GREEN = "\x1b[32m";
|
|
@@ -82,6 +101,45 @@ function getVersion(filePath) {
|
|
|
82
101
|
} catch { return null; }
|
|
83
102
|
}
|
|
84
103
|
|
|
104
|
+
function escapeRegExp(value) {
|
|
105
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function hasProfileSection(tomlContent, profileName) {
|
|
109
|
+
const section = `^\\[profiles\\.${escapeRegExp(profileName)}\\]\\s*$`;
|
|
110
|
+
return new RegExp(section, "m").test(tomlContent);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function ensureCodexProfiles() {
|
|
114
|
+
try {
|
|
115
|
+
if (!existsSync(CODEX_DIR)) mkdirSync(CODEX_DIR, { recursive: true });
|
|
116
|
+
|
|
117
|
+
const original = existsSync(CODEX_CONFIG_PATH)
|
|
118
|
+
? readFileSync(CODEX_CONFIG_PATH, "utf8")
|
|
119
|
+
: "";
|
|
120
|
+
|
|
121
|
+
let updated = original;
|
|
122
|
+
let added = 0;
|
|
123
|
+
|
|
124
|
+
for (const profile of REQUIRED_CODEX_PROFILES) {
|
|
125
|
+
if (hasProfileSection(updated, profile.name)) continue;
|
|
126
|
+
|
|
127
|
+
if (updated.length > 0 && !updated.endsWith("\n")) updated += "\n";
|
|
128
|
+
if (updated.trim().length > 0) updated += "\n";
|
|
129
|
+
updated += `[profiles.${profile.name}]\n${profile.lines.join("\n")}\n`;
|
|
130
|
+
added++;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (added > 0) {
|
|
134
|
+
writeFileSync(CODEX_CONFIG_PATH, updated, "utf8");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return { ok: true, added };
|
|
138
|
+
} catch (e) {
|
|
139
|
+
return { ok: false, added: 0, message: e.message };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
85
143
|
function syncFile(src, dst, label) {
|
|
86
144
|
const dstDir = dirname(dst);
|
|
87
145
|
if (!existsSync(dstDir)) mkdirSync(dstDir, { recursive: true });
|
|
@@ -221,6 +279,15 @@ function cmdSetup() {
|
|
|
221
279
|
}
|
|
222
280
|
}
|
|
223
281
|
|
|
282
|
+
const codexProfileResult = ensureCodexProfiles();
|
|
283
|
+
if (!codexProfileResult.ok) {
|
|
284
|
+
warn(`Codex profiles 설정 실패: ${codexProfileResult.message}`);
|
|
285
|
+
} else if (codexProfileResult.added > 0) {
|
|
286
|
+
ok(`Codex profiles: ${codexProfileResult.added}개 추가됨 (~/.codex/config.toml)`);
|
|
287
|
+
} else {
|
|
288
|
+
ok("Codex profiles: 이미 준비됨");
|
|
289
|
+
}
|
|
290
|
+
|
|
224
291
|
// hub MCP 사전 등록 (서버 미실행이어도 설정만 등록 — hub start 시 즉시 사용 가능)
|
|
225
292
|
if (existsSync(join(PKG_ROOT, "hub", "server.mjs"))) {
|
|
226
293
|
const defaultHubUrl = `http://127.0.0.1:${process.env.TFX_HUB_PORT || "27888"}/mcp`;
|
|
@@ -359,10 +426,10 @@ function cmdDoctor(options = {}) {
|
|
|
359
426
|
// 스킬 동기화
|
|
360
427
|
const fSkillsSrc = join(PKG_ROOT, "skills");
|
|
361
428
|
const fSkillsDst = join(CLAUDE_DIR, "skills");
|
|
362
|
-
if (existsSync(fSkillsSrc)) {
|
|
363
|
-
let sc = 0, st = 0;
|
|
364
|
-
for (const name of readdirSync(fSkillsSrc)) {
|
|
365
|
-
const src = join(fSkillsSrc, name, "SKILL.md");
|
|
429
|
+
if (existsSync(fSkillsSrc)) {
|
|
430
|
+
let sc = 0, st = 0;
|
|
431
|
+
for (const name of readdirSync(fSkillsSrc)) {
|
|
432
|
+
const src = join(fSkillsSrc, name, "SKILL.md");
|
|
366
433
|
const dst = join(fSkillsDst, name, "SKILL.md");
|
|
367
434
|
if (!existsSync(src)) continue;
|
|
368
435
|
st++;
|
|
@@ -371,13 +438,21 @@ function cmdDoctor(options = {}) {
|
|
|
371
438
|
if (!existsSync(dst)) { copyFileSync(src, dst); sc++; }
|
|
372
439
|
else if (readFileSync(src, "utf8") !== readFileSync(dst, "utf8")) { copyFileSync(src, dst); sc++; }
|
|
373
440
|
}
|
|
374
|
-
if (sc > 0) ok(`스킬: ${sc}/${st}개 업데이트됨`);
|
|
375
|
-
else ok(`스킬: ${st}개 최신 상태`);
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
441
|
+
if (sc > 0) ok(`스킬: ${sc}/${st}개 업데이트됨`);
|
|
442
|
+
else ok(`스킬: ${st}개 최신 상태`);
|
|
443
|
+
}
|
|
444
|
+
const profileFix = ensureCodexProfiles();
|
|
445
|
+
if (!profileFix.ok) {
|
|
446
|
+
warn(`Codex Profiles 자동 복구 실패: ${profileFix.message}`);
|
|
447
|
+
} else if (profileFix.added > 0) {
|
|
448
|
+
ok(`Codex Profiles: ${profileFix.added}개 추가됨`);
|
|
449
|
+
} else {
|
|
450
|
+
info("Codex Profiles: 이미 최신 상태");
|
|
451
|
+
}
|
|
452
|
+
// 에러/스테일 캐시 정리
|
|
453
|
+
const fCacheDir = join(CLAUDE_DIR, "cache");
|
|
454
|
+
const staleNames = ["claude-usage-cache.json", ".claude-refresh-lock", "codex-rate-limits-cache.json"];
|
|
455
|
+
let cleaned = 0;
|
|
381
456
|
for (const name of staleNames) {
|
|
382
457
|
const fp = join(fCacheDir, name);
|
|
383
458
|
if (!existsSync(fp)) continue;
|
|
@@ -417,39 +492,56 @@ function cmdDoctor(options = {}) {
|
|
|
417
492
|
// 3. Codex CLI
|
|
418
493
|
section(`Codex CLI ${WHITE_BRIGHT}●${RESET}`);
|
|
419
494
|
issues += checkCliCrossShell("codex", "npm install -g @openai/codex");
|
|
420
|
-
if (which("codex")) {
|
|
421
|
-
if (process.env.OPENAI_API_KEY) {
|
|
422
|
-
ok("OPENAI_API_KEY 설정됨");
|
|
423
|
-
} else {
|
|
424
|
-
warn(`OPENAI_API_KEY 미설정 ${GRAY}(Pro 구독이면 불필요)${RESET}`);
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
// 4.
|
|
429
|
-
section(
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
495
|
+
if (which("codex")) {
|
|
496
|
+
if (process.env.OPENAI_API_KEY) {
|
|
497
|
+
ok("OPENAI_API_KEY 설정됨");
|
|
498
|
+
} else {
|
|
499
|
+
warn(`OPENAI_API_KEY 미설정 ${GRAY}(Pro 구독이면 불필요)${RESET}`);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// 4. Codex Profiles
|
|
504
|
+
section("Codex Profiles");
|
|
505
|
+
if (existsSync(CODEX_CONFIG_PATH)) {
|
|
506
|
+
const codexConfig = readFileSync(CODEX_CONFIG_PATH, "utf8");
|
|
507
|
+
for (const profile of REQUIRED_CODEX_PROFILES) {
|
|
508
|
+
if (hasProfileSection(codexConfig, profile.name)) {
|
|
509
|
+
ok(`${profile.name}: 정상`);
|
|
510
|
+
} else {
|
|
511
|
+
warn(`${profile.name}: 미설정`);
|
|
512
|
+
issues++;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
} else {
|
|
516
|
+
warn("config.toml 미존재");
|
|
517
|
+
issues++;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// 5. Gemini CLI
|
|
521
|
+
section(`Gemini CLI ${BLUE}●${RESET}`);
|
|
522
|
+
issues += checkCliCrossShell("gemini", "npm install -g @google/gemini-cli");
|
|
523
|
+
if (which("gemini")) {
|
|
524
|
+
if (process.env.GEMINI_API_KEY) {
|
|
525
|
+
ok("GEMINI_API_KEY 설정됨");
|
|
434
526
|
} else {
|
|
435
527
|
warn(`GEMINI_API_KEY 미설정 ${GRAY}(gemini auth login)${RESET}`);
|
|
436
528
|
}
|
|
437
529
|
}
|
|
438
530
|
|
|
439
|
-
//
|
|
440
|
-
section(`Claude Code ${AMBER}●${RESET}`);
|
|
441
|
-
const claudePath = which("claude");
|
|
442
|
-
if (claudePath) {
|
|
443
|
-
ok("설치됨");
|
|
444
|
-
} else {
|
|
531
|
+
// 6. Claude Code
|
|
532
|
+
section(`Claude Code ${AMBER}●${RESET}`);
|
|
533
|
+
const claudePath = which("claude");
|
|
534
|
+
if (claudePath) {
|
|
535
|
+
ok("설치됨");
|
|
536
|
+
} else {
|
|
445
537
|
fail("미설치 (필수)");
|
|
446
538
|
issues++;
|
|
447
539
|
}
|
|
448
540
|
|
|
449
|
-
//
|
|
450
|
-
section("Skills");
|
|
451
|
-
const skillsSrc = join(PKG_ROOT, "skills");
|
|
452
|
-
const skillsDst = join(CLAUDE_DIR, "skills");
|
|
541
|
+
// 7. 스킬 설치 상태
|
|
542
|
+
section("Skills");
|
|
543
|
+
const skillsSrc = join(PKG_ROOT, "skills");
|
|
544
|
+
const skillsDst = join(CLAUDE_DIR, "skills");
|
|
453
545
|
if (existsSync(skillsSrc)) {
|
|
454
546
|
let installed = 0;
|
|
455
547
|
let total = 0;
|
|
@@ -472,9 +564,9 @@ function cmdDoctor(options = {}) {
|
|
|
472
564
|
}
|
|
473
565
|
}
|
|
474
566
|
|
|
475
|
-
//
|
|
476
|
-
section("Plugin");
|
|
477
|
-
const pluginsFile = join(CLAUDE_DIR, "plugins", "installed_plugins.json");
|
|
567
|
+
// 8. 플러그인 등록
|
|
568
|
+
section("Plugin");
|
|
569
|
+
const pluginsFile = join(CLAUDE_DIR, "plugins", "installed_plugins.json");
|
|
478
570
|
if (existsSync(pluginsFile)) {
|
|
479
571
|
const content = readFileSync(pluginsFile, "utf8");
|
|
480
572
|
if (content.includes("triflux")) {
|
|
@@ -487,9 +579,9 @@ function cmdDoctor(options = {}) {
|
|
|
487
579
|
info("플러그인 시스템 감지 안 됨 — npm 단독 사용");
|
|
488
580
|
}
|
|
489
581
|
|
|
490
|
-
//
|
|
491
|
-
section("MCP Inventory");
|
|
492
|
-
const mcpCache = join(CLAUDE_DIR, "cache", "mcp-inventory.json");
|
|
582
|
+
// 9. MCP 인벤토리
|
|
583
|
+
section("MCP Inventory");
|
|
584
|
+
const mcpCache = join(CLAUDE_DIR, "cache", "mcp-inventory.json");
|
|
493
585
|
if (existsSync(mcpCache)) {
|
|
494
586
|
try {
|
|
495
587
|
const inv = JSON.parse(readFileSync(mcpCache, "utf8"));
|
|
@@ -510,8 +602,8 @@ function cmdDoctor(options = {}) {
|
|
|
510
602
|
info(`수동: node ${join(PKG_ROOT, "scripts", "mcp-check.mjs")}`);
|
|
511
603
|
}
|
|
512
604
|
|
|
513
|
-
//
|
|
514
|
-
section("CLI Issues");
|
|
605
|
+
// 10. CLI 이슈 트래커
|
|
606
|
+
section("CLI Issues");
|
|
515
607
|
const issuesFile = join(CLAUDE_DIR, "cache", "cli-issues.jsonl");
|
|
516
608
|
if (existsSync(issuesFile)) {
|
|
517
609
|
try {
|
|
@@ -827,6 +919,7 @@ ${updateNotice}
|
|
|
827
919
|
${WHITE_BRIGHT}tfx list${RESET} ${GRAY}설치된 스킬 목록${RESET}
|
|
828
920
|
${WHITE_BRIGHT}tfx hub${RESET} ${GRAY}MCP 메시지 버스 관리 (start/stop/status)${RESET}
|
|
829
921
|
${WHITE_BRIGHT}tfx team${RESET} ${GRAY}멀티-CLI 팀 모드 (tmux + Hub)${RESET}
|
|
922
|
+
${WHITE_BRIGHT}tfx codex-team${RESET} ${GRAY}Codex 전용 팀 모드 (기본 lead/agents: codex)${RESET}
|
|
830
923
|
${WHITE_BRIGHT}tfx notion-read${RESET} ${GRAY}Notion 페이지 → 마크다운 (Codex/Gemini MCP)${RESET}
|
|
831
924
|
${WHITE_BRIGHT}tfx version${RESET} ${GRAY}버전 표시${RESET}
|
|
832
925
|
|
|
@@ -843,6 +936,52 @@ ${updateNotice}
|
|
|
843
936
|
`);
|
|
844
937
|
}
|
|
845
938
|
|
|
939
|
+
async function cmdCodexTeam() {
|
|
940
|
+
const args = process.argv.slice(3);
|
|
941
|
+
const sub = String(args[0] || "").toLowerCase();
|
|
942
|
+
const passthrough = new Set([
|
|
943
|
+
"status", "attach", "stop", "kill", "send", "list", "help", "--help", "-h",
|
|
944
|
+
"tasks", "task", "focus", "interrupt", "control", "debug",
|
|
945
|
+
]);
|
|
946
|
+
|
|
947
|
+
if (sub === "help" || sub === "--help" || sub === "-h") {
|
|
948
|
+
console.log(`
|
|
949
|
+
${AMBER}${BOLD}⬡ tfx codex-team${RESET}
|
|
950
|
+
|
|
951
|
+
${WHITE_BRIGHT}tfx codex-team "작업"${RESET} ${GRAY}Codex 리드 + 워커 2개로 팀 시작${RESET}
|
|
952
|
+
${WHITE_BRIGHT}tfx codex-team --layout 1xN "작업"${RESET} ${GRAY}(세로 분할 컬럼)${RESET}
|
|
953
|
+
${WHITE_BRIGHT}tfx codex-team --layout Nx1 "작업"${RESET} ${GRAY}(가로 분할 스택)${RESET}
|
|
954
|
+
${WHITE_BRIGHT}tfx codex-team status${RESET}
|
|
955
|
+
${WHITE_BRIGHT}tfx codex-team debug --lines 30${RESET}
|
|
956
|
+
${WHITE_BRIGHT}tfx codex-team send N "msg"${RESET}
|
|
957
|
+
|
|
958
|
+
${DIM}내부적으로 tfx team을 호출하며, 시작 시 --lead codex --agents codex,codex를 기본 주입합니다.${RESET}
|
|
959
|
+
`);
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
const hasAgents = args.includes("--agents");
|
|
964
|
+
const hasLead = args.includes("--lead");
|
|
965
|
+
const hasLayout = args.includes("--layout");
|
|
966
|
+
const isControl = passthrough.has(sub);
|
|
967
|
+
const inject = [];
|
|
968
|
+
if (!isControl && !hasLead) inject.push("--lead", "codex");
|
|
969
|
+
if (!isControl && !hasAgents) inject.push("--agents", "codex,codex");
|
|
970
|
+
if (!isControl && !hasLayout) inject.push("--layout", "1xN");
|
|
971
|
+
const forwarded = isControl ? args : [...inject, ...args];
|
|
972
|
+
|
|
973
|
+
const { pathToFileURL } = await import("node:url");
|
|
974
|
+
const { cmdTeam } = await import(pathToFileURL(join(PKG_ROOT, "hub", "team", "cli.mjs")).href);
|
|
975
|
+
|
|
976
|
+
const prevArgv = process.argv;
|
|
977
|
+
process.argv = [prevArgv[0], prevArgv[1], "team", ...forwarded];
|
|
978
|
+
try {
|
|
979
|
+
await cmdTeam();
|
|
980
|
+
} finally {
|
|
981
|
+
process.argv = prevArgv;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
846
985
|
// ── hub 서브커맨드 ──
|
|
847
986
|
|
|
848
987
|
const HUB_PID_DIR = join(homedir(), ".claude", "cache", "tfx-hub");
|
|
@@ -1067,6 +1206,9 @@ switch (cmd) {
|
|
|
1067
1206
|
await cmdTeam();
|
|
1068
1207
|
break;
|
|
1069
1208
|
}
|
|
1209
|
+
case "codex-team":
|
|
1210
|
+
await cmdCodexTeam();
|
|
1211
|
+
break;
|
|
1070
1212
|
case "notion-read": case "nr": {
|
|
1071
1213
|
const scriptPath = join(PKG_ROOT, "scripts", "notion-read.mjs");
|
|
1072
1214
|
const nrArgs = process.argv.slice(3).map(a => `"${a}"`).join(" ");
|
package/hooks/hooks.json
CHANGED
package/hub/bridge.mjs
CHANGED
|
@@ -9,21 +9,25 @@
|
|
|
9
9
|
// node bridge.mjs result --agent <id> --file <path> [--topic task.result] [--trace <id>]
|
|
10
10
|
// node bridge.mjs context --agent <id> [--topics t1,t2] [--max 10] [--out <path>]
|
|
11
11
|
// node bridge.mjs deregister --agent <id>
|
|
12
|
+
// node bridge.mjs team-info --team <team_name>
|
|
13
|
+
// node bridge.mjs team-task-list --team <team_name> [--owner <name>] [--statuses s1,s2]
|
|
14
|
+
// node bridge.mjs team-task-update --team <team_name> --task-id <id> [--claim] [--status <s>] [--owner <name>]
|
|
15
|
+
// node bridge.mjs team-send-message --team <team_name> --from <sender> --text <message> [--to team-lead]
|
|
12
16
|
// node bridge.mjs ping
|
|
13
17
|
//
|
|
14
18
|
// Hub 미실행 시 모든 커맨드는 조용히 실패 (exit 0).
|
|
15
19
|
// tfx-route.sh 흐름을 절대 차단하지 않는다.
|
|
16
20
|
|
|
17
|
-
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
18
|
-
import { join } from 'node:path';
|
|
19
|
-
import { homedir } from 'node:os';
|
|
20
|
-
import { parseArgs as nodeParseArgs } from 'node:util';
|
|
21
|
+
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
22
|
+
import { join } from 'node:path';
|
|
23
|
+
import { homedir } from 'node:os';
|
|
24
|
+
import { parseArgs as nodeParseArgs } from 'node:util';
|
|
21
25
|
|
|
22
26
|
const HUB_PID_FILE = join(homedir(), '.claude', 'cache', 'tfx-hub', 'hub.pid');
|
|
23
27
|
|
|
24
28
|
// ── Hub URL 해석 ──
|
|
25
29
|
|
|
26
|
-
function getHubUrl() {
|
|
30
|
+
function getHubUrl() {
|
|
27
31
|
// 환경변수 우선
|
|
28
32
|
if (process.env.TFX_HUB_URL) return process.env.TFX_HUB_URL.replace(/\/mcp$/, '');
|
|
29
33
|
|
|
@@ -36,18 +40,18 @@ function getHubUrl() {
|
|
|
36
40
|
}
|
|
37
41
|
|
|
38
42
|
// 기본값
|
|
39
|
-
const port = process.env.TFX_HUB_PORT || '27888';
|
|
40
|
-
return `http://127.0.0.1:${port}`;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const _cachedHubUrl = getHubUrl();
|
|
44
|
-
|
|
45
|
-
// ── HTTP 요청 ──
|
|
46
|
-
|
|
47
|
-
async function post(path, body, timeoutMs = 5000) {
|
|
48
|
-
const url = `${_cachedHubUrl}${path}`;
|
|
49
|
-
const controller = new AbortController();
|
|
50
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
43
|
+
const port = process.env.TFX_HUB_PORT || '27888';
|
|
44
|
+
return `http://127.0.0.1:${port}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const _cachedHubUrl = getHubUrl();
|
|
48
|
+
|
|
49
|
+
// ── HTTP 요청 ──
|
|
50
|
+
|
|
51
|
+
async function post(path, body, timeoutMs = 5000) {
|
|
52
|
+
const url = `${_cachedHubUrl}${path}`;
|
|
53
|
+
const controller = new AbortController();
|
|
54
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
51
55
|
|
|
52
56
|
try {
|
|
53
57
|
const res = await fetch(url, {
|
|
@@ -66,27 +70,53 @@ async function post(path, body, timeoutMs = 5000) {
|
|
|
66
70
|
|
|
67
71
|
// ── 인자 파싱 ──
|
|
68
72
|
|
|
69
|
-
function parseArgs(argv) {
|
|
70
|
-
const { values } = nodeParseArgs({
|
|
71
|
-
args: argv,
|
|
72
|
-
options: {
|
|
73
|
-
agent: { type: 'string' },
|
|
74
|
-
cli: { type: 'string' },
|
|
75
|
-
timeout: { type: 'string' },
|
|
76
|
-
topics: { type: 'string' },
|
|
77
|
-
capabilities: { type: 'string' },
|
|
78
|
-
file: { type: 'string' },
|
|
79
|
-
topic: { type: 'string' },
|
|
80
|
-
trace: { type: 'string' },
|
|
81
|
-
correlation: { type: 'string' },
|
|
82
|
-
'exit-code': { type: 'string' },
|
|
83
|
-
max: { type: 'string' },
|
|
84
|
-
out: { type: 'string' },
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
73
|
+
function parseArgs(argv) {
|
|
74
|
+
const { values } = nodeParseArgs({
|
|
75
|
+
args: argv,
|
|
76
|
+
options: {
|
|
77
|
+
agent: { type: 'string' },
|
|
78
|
+
cli: { type: 'string' },
|
|
79
|
+
timeout: { type: 'string' },
|
|
80
|
+
topics: { type: 'string' },
|
|
81
|
+
capabilities: { type: 'string' },
|
|
82
|
+
file: { type: 'string' },
|
|
83
|
+
topic: { type: 'string' },
|
|
84
|
+
trace: { type: 'string' },
|
|
85
|
+
correlation: { type: 'string' },
|
|
86
|
+
'exit-code': { type: 'string' },
|
|
87
|
+
max: { type: 'string' },
|
|
88
|
+
out: { type: 'string' },
|
|
89
|
+
team: { type: 'string' },
|
|
90
|
+
'task-id': { type: 'string' },
|
|
91
|
+
owner: { type: 'string' },
|
|
92
|
+
status: { type: 'string' },
|
|
93
|
+
statuses: { type: 'string' },
|
|
94
|
+
claim: { type: 'boolean' },
|
|
95
|
+
actor: { type: 'string' },
|
|
96
|
+
from: { type: 'string' },
|
|
97
|
+
to: { type: 'string' },
|
|
98
|
+
text: { type: 'string' },
|
|
99
|
+
summary: { type: 'string' },
|
|
100
|
+
color: { type: 'string' },
|
|
101
|
+
limit: { type: 'string' },
|
|
102
|
+
'include-internal': { type: 'boolean' },
|
|
103
|
+
subject: { type: 'string' },
|
|
104
|
+
description: { type: 'string' },
|
|
105
|
+
'active-form': { type: 'string' },
|
|
106
|
+
'add-blocks': { type: 'string' },
|
|
107
|
+
'add-blocked-by': { type: 'string' },
|
|
108
|
+
'metadata-patch': { type: 'string' },
|
|
109
|
+
'if-match-mtime-ms': { type: 'string' },
|
|
110
|
+
},
|
|
111
|
+
strict: false,
|
|
112
|
+
});
|
|
113
|
+
return values;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function parseJsonSafe(raw, fallback = null) {
|
|
117
|
+
if (!raw) return fallback;
|
|
118
|
+
try { return JSON.parse(raw); } catch { return fallback; }
|
|
119
|
+
}
|
|
90
120
|
|
|
91
121
|
// ── 커맨드 ──
|
|
92
122
|
|
|
@@ -135,14 +165,14 @@ async function cmdResult(args) {
|
|
|
135
165
|
const result = await post('/bridge/result', {
|
|
136
166
|
agent_id: agentId,
|
|
137
167
|
topic,
|
|
138
|
-
payload: {
|
|
139
|
-
agent_id: agentId,
|
|
140
|
-
exit_code: exitCode,
|
|
141
|
-
output_length: output.length,
|
|
142
|
-
output_preview: output.slice(0, 4096), // 미리보기 4KB
|
|
143
|
-
output_file: filePath || null,
|
|
144
|
-
completed_at: Date.now(),
|
|
145
|
-
},
|
|
168
|
+
payload: {
|
|
169
|
+
agent_id: agentId,
|
|
170
|
+
exit_code: exitCode,
|
|
171
|
+
output_length: output.length,
|
|
172
|
+
output_preview: output.slice(0, 4096), // 미리보기 4KB
|
|
173
|
+
output_file: filePath || null,
|
|
174
|
+
completed_at: Date.now(),
|
|
175
|
+
},
|
|
146
176
|
trace_id: traceId,
|
|
147
177
|
correlation_id: correlationId,
|
|
148
178
|
});
|
|
@@ -201,10 +231,62 @@ async function cmdDeregister(args) {
|
|
|
201
231
|
}
|
|
202
232
|
}
|
|
203
233
|
|
|
204
|
-
async function
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
234
|
+
async function cmdTeamInfo(args) {
|
|
235
|
+
const result = await post('/bridge/team/info', {
|
|
236
|
+
team_name: args.team,
|
|
237
|
+
include_members: true,
|
|
238
|
+
include_paths: true,
|
|
239
|
+
});
|
|
240
|
+
console.log(JSON.stringify(result || { ok: false, reason: 'hub_unavailable' }));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function cmdTeamTaskList(args) {
|
|
244
|
+
const statuses = args.statuses ? args.statuses.split(',').map((s) => s.trim()).filter(Boolean) : [];
|
|
245
|
+
const result = await post('/bridge/team/task-list', {
|
|
246
|
+
team_name: args.team,
|
|
247
|
+
owner: args.owner,
|
|
248
|
+
statuses,
|
|
249
|
+
include_internal: !!args['include-internal'],
|
|
250
|
+
limit: parseInt(args.limit || '200', 10),
|
|
251
|
+
});
|
|
252
|
+
console.log(JSON.stringify(result || { ok: false, reason: 'hub_unavailable' }));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function cmdTeamTaskUpdate(args) {
|
|
256
|
+
const result = await post('/bridge/team/task-update', {
|
|
257
|
+
team_name: args.team,
|
|
258
|
+
task_id: args['task-id'],
|
|
259
|
+
claim: !!args.claim,
|
|
260
|
+
owner: args.owner,
|
|
261
|
+
status: args.status,
|
|
262
|
+
subject: args.subject,
|
|
263
|
+
description: args.description,
|
|
264
|
+
activeForm: args['active-form'],
|
|
265
|
+
add_blocks: args['add-blocks'] ? args['add-blocks'].split(',').map((s) => s.trim()).filter(Boolean) : undefined,
|
|
266
|
+
add_blocked_by: args['add-blocked-by'] ? args['add-blocked-by'].split(',').map((s) => s.trim()).filter(Boolean) : undefined,
|
|
267
|
+
metadata_patch: args['metadata-patch'] ? parseJsonSafe(args['metadata-patch'], null) : undefined,
|
|
268
|
+
if_match_mtime_ms: args['if-match-mtime-ms'] != null ? Number(args['if-match-mtime-ms']) : undefined,
|
|
269
|
+
actor: args.actor,
|
|
270
|
+
});
|
|
271
|
+
console.log(JSON.stringify(result || { ok: false, reason: 'hub_unavailable' }));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function cmdTeamSendMessage(args) {
|
|
275
|
+
const result = await post('/bridge/team/send-message', {
|
|
276
|
+
team_name: args.team,
|
|
277
|
+
from: args.from,
|
|
278
|
+
to: args.to || 'team-lead',
|
|
279
|
+
text: args.text,
|
|
280
|
+
summary: args.summary,
|
|
281
|
+
color: args.color || 'blue',
|
|
282
|
+
});
|
|
283
|
+
console.log(JSON.stringify(result || { ok: false, reason: 'hub_unavailable' }));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async function cmdPing() {
|
|
287
|
+
try {
|
|
288
|
+
const url = `${_cachedHubUrl}/status`;
|
|
289
|
+
const controller = new AbortController();
|
|
208
290
|
const timer = setTimeout(() => controller.abort(), 3000);
|
|
209
291
|
const res = await fetch(url, { signal: controller.signal });
|
|
210
292
|
clearTimeout(timer);
|
|
@@ -225,8 +307,12 @@ switch (cmd) {
|
|
|
225
307
|
case 'result': await cmdResult(args); break;
|
|
226
308
|
case 'context': await cmdContext(args); break;
|
|
227
309
|
case 'deregister': await cmdDeregister(args); break;
|
|
310
|
+
case 'team-info': await cmdTeamInfo(args); break;
|
|
311
|
+
case 'team-task-list': await cmdTeamTaskList(args); break;
|
|
312
|
+
case 'team-task-update': await cmdTeamTaskUpdate(args); break;
|
|
313
|
+
case 'team-send-message': await cmdTeamSendMessage(args); break;
|
|
228
314
|
case 'ping': await cmdPing(); break;
|
|
229
315
|
default:
|
|
230
|
-
console.error('사용법: bridge.mjs <register|result|context|deregister|ping> [--옵션]');
|
|
316
|
+
console.error('사용법: bridge.mjs <register|result|context|deregister|team-info|team-task-list|team-task-update|team-send-message|ping> [--옵션]');
|
|
231
317
|
process.exit(1);
|
|
232
318
|
}
|