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 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
- const fCacheDir = join(CLAUDE_DIR, "cache");
379
- const staleNames = ["claude-usage-cache.json", ".claude-refresh-lock", "codex-rate-limits-cache.json"];
380
- let cleaned = 0;
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. Gemini CLI
429
- section(`Gemini CLI ${BLUE}●${RESET}`);
430
- issues += checkCliCrossShell("gemini", "npm install -g @google/gemini-cli");
431
- if (which("gemini")) {
432
- if (process.env.GEMINI_API_KEY) {
433
- ok("GEMINI_API_KEY 설정됨");
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
- // 5. Claude Code
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
- // 6. 스킬 설치 상태
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
- // 7. 플러그인 등록
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
- // 8. MCP 인벤토리
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
- // 9. CLI 이슈 트래커
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
@@ -12,6 +12,18 @@
12
12
  }
13
13
  ]
14
14
  }
15
+ ],
16
+ "UserPromptSubmit": [
17
+ {
18
+ "matcher": "*",
19
+ "hooks": [
20
+ {
21
+ "type": "command",
22
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/team-keyword.mjs\"",
23
+ "timeout": 3
24
+ }
25
+ ]
26
+ }
15
27
  ]
16
28
  }
17
29
  }
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
- strict: false,
87
- });
88
- return values;
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 cmdPing() {
205
- try {
206
- const url = `${_cachedHubUrl}/status`;
207
- const controller = new AbortController();
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
  }