@triflux/remote 10.0.0-alpha.1

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 (68) hide show
  1. package/hub/pipe.mjs +579 -0
  2. package/hub/public/dashboard.html +355 -0
  3. package/hub/public/tray-icon.ico +0 -0
  4. package/hub/public/tray-icon.png +0 -0
  5. package/hub/server.mjs +1124 -0
  6. package/hub/store-adapter.mjs +851 -0
  7. package/hub/store.mjs +897 -0
  8. package/hub/team/agent-map.json +11 -0
  9. package/hub/team/ansi.mjs +379 -0
  10. package/hub/team/backend.mjs +90 -0
  11. package/hub/team/cli/commands/attach.mjs +37 -0
  12. package/hub/team/cli/commands/control.mjs +43 -0
  13. package/hub/team/cli/commands/debug.mjs +74 -0
  14. package/hub/team/cli/commands/focus.mjs +53 -0
  15. package/hub/team/cli/commands/interrupt.mjs +36 -0
  16. package/hub/team/cli/commands/kill.mjs +37 -0
  17. package/hub/team/cli/commands/list.mjs +24 -0
  18. package/hub/team/cli/commands/send.mjs +37 -0
  19. package/hub/team/cli/commands/start/index.mjs +106 -0
  20. package/hub/team/cli/commands/start/parse-args.mjs +130 -0
  21. package/hub/team/cli/commands/start/start-headless.mjs +109 -0
  22. package/hub/team/cli/commands/start/start-in-process.mjs +40 -0
  23. package/hub/team/cli/commands/start/start-mux.mjs +73 -0
  24. package/hub/team/cli/commands/start/start-wt.mjs +69 -0
  25. package/hub/team/cli/commands/status.mjs +87 -0
  26. package/hub/team/cli/commands/stop.mjs +31 -0
  27. package/hub/team/cli/commands/task.mjs +30 -0
  28. package/hub/team/cli/commands/tasks.mjs +13 -0
  29. package/hub/team/cli/help.mjs +42 -0
  30. package/hub/team/cli/index.mjs +41 -0
  31. package/hub/team/cli/manifest.mjs +29 -0
  32. package/hub/team/cli/render.mjs +30 -0
  33. package/hub/team/cli/services/attach-fallback.mjs +54 -0
  34. package/hub/team/cli/services/hub-client.mjs +208 -0
  35. package/hub/team/cli/services/member-selector.mjs +30 -0
  36. package/hub/team/cli/services/native-control.mjs +117 -0
  37. package/hub/team/cli/services/runtime-mode.mjs +62 -0
  38. package/hub/team/cli/services/state-store.mjs +48 -0
  39. package/hub/team/cli/services/task-model.mjs +30 -0
  40. package/hub/team/dashboard-anchor.mjs +14 -0
  41. package/hub/team/dashboard-layout.mjs +33 -0
  42. package/hub/team/dashboard-open.mjs +153 -0
  43. package/hub/team/dashboard.mjs +274 -0
  44. package/hub/team/handoff.mjs +303 -0
  45. package/hub/team/headless.mjs +1149 -0
  46. package/hub/team/native-supervisor.mjs +392 -0
  47. package/hub/team/native.mjs +649 -0
  48. package/hub/team/nativeProxy.mjs +681 -0
  49. package/hub/team/orchestrator.mjs +161 -0
  50. package/hub/team/pane.mjs +153 -0
  51. package/hub/team/psmux.mjs +1354 -0
  52. package/hub/team/routing.mjs +223 -0
  53. package/hub/team/session.mjs +611 -0
  54. package/hub/team/shared.mjs +13 -0
  55. package/hub/team/staleState.mjs +361 -0
  56. package/hub/team/tui-lite.mjs +380 -0
  57. package/hub/team/tui-viewer.mjs +463 -0
  58. package/hub/team/tui.mjs +1245 -0
  59. package/hub/tools.mjs +554 -0
  60. package/hub/tray.mjs +376 -0
  61. package/hub/workers/claude-worker.mjs +475 -0
  62. package/hub/workers/codex-mcp.mjs +504 -0
  63. package/hub/workers/delegator-mcp.mjs +1076 -0
  64. package/hub/workers/factory.mjs +21 -0
  65. package/hub/workers/gemini-worker.mjs +373 -0
  66. package/hub/workers/interface.mjs +52 -0
  67. package/hub/workers/worker-utils.mjs +104 -0
  68. package/package.json +31 -0
@@ -0,0 +1,117 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { spawn } from "node:child_process";
4
+
5
+ import { buildLeadPrompt, buildPrompt } from "../../orchestrator.mjs";
6
+ import { HUB_PID_DIR, PKG_ROOT } from "./state-store.mjs";
7
+
8
+ import { buildExecArgs } from "../../../codex-adapter.mjs";
9
+
10
+ export function buildNativeCliCommand(cli) {
11
+ switch (cli) {
12
+ case "codex":
13
+ return buildExecArgs({});
14
+ case "gemini":
15
+ return "gemini";
16
+ case "claude":
17
+ return "claude";
18
+ default:
19
+ return cli;
20
+ }
21
+ }
22
+
23
+ export async function startNativeSupervisor({ sessionId, task, lead, agents, subtasks, hubUrl }) {
24
+ const configPath = join(HUB_PID_DIR, `team-native-${sessionId}.config.json`);
25
+ const runtimePath = join(HUB_PID_DIR, `team-native-${sessionId}.runtime.json`);
26
+ const logsDir = join(HUB_PID_DIR, "team-logs", sessionId);
27
+ mkdirSync(logsDir, { recursive: true });
28
+
29
+ const leadMember = {
30
+ role: "lead",
31
+ name: "lead",
32
+ cli: lead,
33
+ agentId: `${lead}-lead`,
34
+ command: buildNativeCliCommand(lead),
35
+ };
36
+ const workers = agents.map((cli, index) => ({
37
+ role: "worker",
38
+ name: `${cli}-${index + 1}`,
39
+ cli,
40
+ agentId: `${cli}-w${index + 1}`,
41
+ command: buildNativeCliCommand(cli),
42
+ subtask: subtasks[index],
43
+ }));
44
+ const members = [
45
+ {
46
+ ...leadMember,
47
+ prompt: buildLeadPrompt(task, {
48
+ agentId: leadMember.agentId,
49
+ hubUrl,
50
+ teammateMode: "in-process",
51
+ workers: workers.map((worker) => ({
52
+ agentId: worker.agentId,
53
+ cli: worker.cli,
54
+ subtask: worker.subtask,
55
+ })),
56
+ }),
57
+ },
58
+ ...workers.map((worker) => ({
59
+ ...worker,
60
+ prompt: buildPrompt(worker.subtask, { cli: worker.cli, agentId: worker.agentId, hubUrl }),
61
+ })),
62
+ ];
63
+
64
+ writeFileSync(configPath, JSON.stringify({
65
+ sessionName: sessionId,
66
+ hubUrl,
67
+ startupDelayMs: 3000,
68
+ logsDir,
69
+ runtimeFile: runtimePath,
70
+ members,
71
+ }, null, 2) + "\n");
72
+
73
+ const child = spawn(process.execPath, [join(PKG_ROOT, "hub", "team", "native-supervisor.mjs"), "--config", configPath], {
74
+ detached: true,
75
+ stdio: "ignore",
76
+ env: { ...process.env },
77
+ windowsHide: true,
78
+ });
79
+ child.unref();
80
+
81
+ const deadline = Date.now() + 5000;
82
+ while (Date.now() < deadline) {
83
+ if (existsSync(runtimePath)) {
84
+ try {
85
+ const runtime = JSON.parse(readFileSync(runtimePath, "utf8"));
86
+ return { runtime, members };
87
+ } catch {}
88
+ }
89
+ await new Promise((resolve) => setTimeout(resolve, 100));
90
+ }
91
+
92
+ return { runtime: null, members };
93
+ }
94
+
95
+ export async function nativeRequest(state, path, body = {}) {
96
+ if (!state?.native?.controlUrl) return null;
97
+ try {
98
+ const res = await fetch(`${state.native.controlUrl}${path}`, {
99
+ method: "POST",
100
+ headers: { "Content-Type": "application/json" },
101
+ body: JSON.stringify(body),
102
+ });
103
+ return await res.json();
104
+ } catch {
105
+ return null;
106
+ }
107
+ }
108
+
109
+ export async function nativeGetStatus(state) {
110
+ if (!state?.native?.controlUrl) return null;
111
+ try {
112
+ const res = await fetch(`${state.native.controlUrl}/status`);
113
+ return await res.json();
114
+ } catch {
115
+ return null;
116
+ }
117
+ }
@@ -0,0 +1,62 @@
1
+ import {
2
+ detectMultiplexer,
3
+ hasWindowsTerminal,
4
+ hasWindowsTerminalSession,
5
+ sessionExists,
6
+ } from "../../session.mjs";
7
+
8
+ export function normalizeTeammateMode(mode = "auto") {
9
+ const raw = String(mode).toLowerCase();
10
+ if (raw === "inline" || raw === "native") return "in-process";
11
+ if (raw === "headless" || raw === "hl") return "headless";
12
+ if (raw === "psmux") return "headless";
13
+ if (raw === "in-process" || raw === "tmux" || raw === "wt") return raw;
14
+ if (raw === "windows-terminal" || raw === "windows_terminal") return "wt";
15
+ if (raw === "auto") {
16
+ if (process.env.TMUX) return "tmux";
17
+ return detectMultiplexer() === "psmux" ? "headless" : "in-process";
18
+ }
19
+ return "in-process";
20
+ }
21
+
22
+ export function normalizeLayout(layout = "2x2") {
23
+ const raw = String(layout).toLowerCase();
24
+ if (raw === "2x2" || raw === "grid") return "2x2";
25
+ if (raw === "1xn" || raw === "1x3" || raw === "vertical" || raw === "columns") return "1xN";
26
+ if (raw === "nx1" || raw === "horizontal" || raw === "rows") return "Nx1";
27
+ return "2x2";
28
+ }
29
+
30
+ export function isNativeMode(state) {
31
+ return state?.teammateMode === "in-process" && !!state?.native?.controlUrl;
32
+ }
33
+
34
+ export function isWtMode(state) {
35
+ return state?.teammateMode === "wt";
36
+ }
37
+
38
+ export function isTeamAlive(state) {
39
+ if (!state) return false;
40
+ if (isNativeMode(state)) {
41
+ try {
42
+ process.kill(state.native.supervisorPid, 0);
43
+ return true;
44
+ } catch {
45
+ return false;
46
+ }
47
+ }
48
+ if (isWtMode(state)) {
49
+ if (!hasWindowsTerminal()) return false;
50
+ if (hasWindowsTerminalSession()) return true;
51
+ return Array.isArray(state.members) && state.members.length > 0;
52
+ }
53
+ return sessionExists(state.sessionName);
54
+ }
55
+
56
+ export function ensureTmuxOrExit() {
57
+ const mux = detectMultiplexer();
58
+ if (mux) return mux;
59
+ const error = new Error("tmux 미발견");
60
+ error.code = "TMUX_REQUIRED";
61
+ throw error;
62
+ }
@@ -0,0 +1,48 @@
1
+ import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { mkdirSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { homedir } from "node:os";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ export const PKG_ROOT = fileURLToPath(new URL("../../../../", import.meta.url));
8
+ export const HUB_PID_DIR = join(homedir(), ".claude", "cache", "tfx-hub");
9
+ export const TEAM_PROFILE = (() => {
10
+ const raw = String(process.env.TFX_TEAM_PROFILE || "team").trim().toLowerCase();
11
+ return raw === "codex-team" ? "codex-team" : "team";
12
+ })();
13
+
14
+ export const SESSION_ID = process.env.CLAUDE_SESSION_ID || `s${Date.now()}`;
15
+
16
+ function getStatePath(sessionId) {
17
+ if (sessionId) return join(HUB_PID_DIR, `team-state-${sessionId}.json`);
18
+ return join(HUB_PID_DIR, TEAM_PROFILE === "codex-team" ? "team-state-codex-team.json" : "team-state.json");
19
+ }
20
+
21
+ export function loadTeamState(sessionId) {
22
+ const resolvedId = sessionId || SESSION_ID;
23
+ const sessionPath = getStatePath(resolvedId);
24
+ try {
25
+ if (existsSync(sessionPath)) return JSON.parse(readFileSync(sessionPath, "utf8"));
26
+ } catch {
27
+ return null;
28
+ }
29
+ // 세션별 파일 없으면 기존 team-state.json fallback
30
+ const legacyPath = getStatePath(null);
31
+ try {
32
+ if (existsSync(legacyPath)) return JSON.parse(readFileSync(legacyPath, "utf8"));
33
+ } catch {
34
+ return null;
35
+ }
36
+ return null;
37
+ }
38
+
39
+ export function saveTeamState(state, sessionId) {
40
+ const path = getStatePath(sessionId || state.sessionId || SESSION_ID);
41
+ mkdirSync(dirname(path), { recursive: true });
42
+ writeFileSync(path, JSON.stringify({ ...state, profile: TEAM_PROFILE }, null, 2) + "\n");
43
+ }
44
+
45
+ export function clearTeamState(sessionId) {
46
+ const path = getStatePath(sessionId || SESSION_ID);
47
+ if (existsSync(path)) unlinkSync(path);
48
+ }
@@ -0,0 +1,30 @@
1
+ export function buildTasks(subtasks, workers) {
2
+ return subtasks.map((subtask, index) => ({
3
+ id: `T${index + 1}`,
4
+ title: subtask,
5
+ owner: workers[index]?.name || null,
6
+ status: "pending",
7
+ depends_on: index === 0 ? [] : [`T${index}`],
8
+ }));
9
+ }
10
+
11
+ export function normalizeTaskStatus(action) {
12
+ const value = String(action || "").toLowerCase();
13
+ if (value === "done" || value === "complete" || value === "completed") return "completed";
14
+ if (value === "progress" || value === "in-progress" || value === "in_progress") return "in_progress";
15
+ if (value === "pending") return "pending";
16
+ return null;
17
+ }
18
+
19
+ export function updateTaskStatus(tasks = [], taskId, nextStatus) {
20
+ const normalizedId = String(taskId || "").toUpperCase();
21
+ const target = tasks.find((task) => String(task.id).toUpperCase() === normalizedId);
22
+ if (!target) return { tasks, target: null };
23
+
24
+ return {
25
+ target: { ...target, status: nextStatus },
26
+ tasks: tasks.map((task) => (
27
+ String(task.id).toUpperCase() === normalizedId ? { ...task, status: nextStatus } : task
28
+ )),
29
+ };
30
+ }
@@ -0,0 +1,14 @@
1
+ const DASHBOARD_ANCHORS = new Set([
2
+ "window",
3
+ "tab",
4
+ ]);
5
+
6
+ export function normalizeDashboardAnchor(value) {
7
+ const normalized = String(value ?? "").trim().toLowerCase();
8
+ if (!normalized) return "window";
9
+ return DASHBOARD_ANCHORS.has(normalized) ? normalized : "window";
10
+ }
11
+
12
+ export function parseDashboardAnchor(value) {
13
+ return normalizeDashboardAnchor(value);
14
+ }
@@ -0,0 +1,33 @@
1
+ const USER_DASHBOARD_LAYOUTS = new Set([
2
+ "single",
3
+ "split-2col",
4
+ "split-3col",
5
+ "auto",
6
+ "lite",
7
+ ]);
8
+
9
+ const DASHBOARD_LAYOUTS = new Set([
10
+ ...USER_DASHBOARD_LAYOUTS,
11
+ "summary+detail",
12
+ ]);
13
+
14
+ export function normalizeDashboardLayout(value, { allowAuto = true } = {}) {
15
+ const normalized = String(value ?? "").trim().toLowerCase();
16
+ if (!normalized) return "single";
17
+ if (normalized === "auto" && !allowAuto) return "single";
18
+ return DASHBOARD_LAYOUTS.has(normalized) ? normalized : "single";
19
+ }
20
+
21
+ export function parseDashboardLayout(value) {
22
+ return normalizeDashboardLayout(value, { allowAuto: true });
23
+ }
24
+
25
+ export function resolveDashboardLayout(value, workerCount = 0) {
26
+ const normalized = normalizeDashboardLayout(value, { allowAuto: true });
27
+ if (normalized === "lite") return "lite";
28
+ if (normalized !== "auto") return normalized;
29
+ if (workerCount >= 4) return "summary+detail";
30
+ if (workerCount === 3) return "split-3col";
31
+ if (workerCount === 2) return "split-2col";
32
+ return "single";
33
+ }
@@ -0,0 +1,153 @@
1
+ import { spawn } from "node:child_process";
2
+
3
+ import { psmuxExec } from "./psmux.mjs";
4
+ import {
5
+ detectMultiplexer,
6
+ focusWtPane,
7
+ hasWindowsTerminal,
8
+ resolveAttachCommand,
9
+ tmuxExec,
10
+ } from "./session.mjs";
11
+
12
+ function sanitizeWindowTitle(value, fallback = "triflux") {
13
+ const text = String(value || "").replace(/[\r\n]+/g, " ").trim();
14
+ return text || fallback;
15
+ }
16
+
17
+ function sanitizeSessionName(value) {
18
+ return String(value || "").replace(/[^a-zA-Z0-9_\-]/g, "") || "tfx-session";
19
+ }
20
+
21
+ function sanitizeWorkingDirectory(value) {
22
+ const text = String(value || "").replace(/[\r\n\x00-\x1f]/g, "").trim();
23
+ return text || process.cwd();
24
+ }
25
+
26
+ export function parseWorkerNumber(value) {
27
+ const text = String(value || "").trim();
28
+ const workerMatch = text.match(/^worker-(\d+)$/i);
29
+ if (workerMatch) return Number.parseInt(workerMatch[1], 10);
30
+ const paneMatch = text.match(/:(\d+)$/);
31
+ if (paneMatch) return Number.parseInt(paneMatch[1], 10);
32
+ return null;
33
+ }
34
+
35
+ export function decideDashboardOpenMode({ openAll = false, hasWtSession = !!process.env.WT_SESSION } = {}) {
36
+ if (openAll) return hasWtSession ? "tab" : "window";
37
+ return hasWtSession ? "split" : "window";
38
+ }
39
+
40
+ function spawnWindowsTerminal(spec, opts = {}) {
41
+ if (!hasWindowsTerminal()) return false;
42
+
43
+ const {
44
+ mode = "window",
45
+ title = "triflux",
46
+ cwd = process.cwd(),
47
+ split = { orientation: "H", size: 0.50 },
48
+ } = opts;
49
+
50
+ const safeTitle = sanitizeWindowTitle(title);
51
+ const safeCwd = sanitizeWorkingDirectory(cwd);
52
+ const orientation = split?.orientation === "V" ? "V" : "H";
53
+ const size = Number.isFinite(split?.size) ? Math.min(0.8, Math.max(0.2, split.size)) : 0.50;
54
+ const baseArgs = ["--profile", "triflux", "--title", safeTitle, "-d", safeCwd, "--", spec.command, ...spec.args];
55
+ const args = mode === "split"
56
+ ? ["-w", "0", "sp", `-${orientation}`, "-s", String(size), ...baseArgs]
57
+ : mode === "tab"
58
+ ? ["-w", "0", "nt", ...baseArgs]
59
+ : ["-w", "new", ...baseArgs];
60
+
61
+ const child = spawn("wt.exe", args, {
62
+ detached: true,
63
+ stdio: "ignore",
64
+ windowsHide: false,
65
+ });
66
+ child.unref();
67
+ return true;
68
+ }
69
+
70
+ export function focusManagedPane(target, opts = {}) {
71
+ const { teammateMode = "", layout = "1xN" } = opts;
72
+ const paneRef = String(target || "");
73
+
74
+ if (teammateMode === "wt" || paneRef.startsWith("wt:")) {
75
+ const paneIndex = parseWorkerNumber(paneRef);
76
+ return paneIndex != null && focusWtPane(paneIndex, { layout });
77
+ }
78
+
79
+ if (!paneRef) return false;
80
+ try {
81
+ if (detectMultiplexer() === "psmux") psmuxExec(["select-pane", "-t", paneRef]);
82
+ else tmuxExec(`select-pane -t ${paneRef}`);
83
+ return true;
84
+ } catch {
85
+ return false;
86
+ }
87
+ }
88
+
89
+ export function openHeadlessDashboardTarget(sessionName, opts = {}) {
90
+ const {
91
+ worker = null,
92
+ openAll = false,
93
+ cwd = process.cwd(),
94
+ title,
95
+ } = opts;
96
+
97
+ const safeSession = sanitizeSessionName(sessionName);
98
+ const workerNumber = worker == null ? null : parseWorkerNumber(worker);
99
+
100
+ // 선택 워커 → pane focus만 (새 창 열지 않음)
101
+ if (!openAll && workerNumber != null) {
102
+ try {
103
+ psmuxExec(["select-pane", "-t", `${safeSession}:0.${workerNumber}`]);
104
+ } catch {}
105
+ return true;
106
+ }
107
+
108
+ // 전체 열기 (Shift+Enter) → 새 WT 창으로 세션 attach
109
+ return spawnWindowsTerminal(
110
+ { command: "psmux", args: ["attach-session", "-t", safeSession] },
111
+ {
112
+ mode: decideDashboardOpenMode({ openAll }),
113
+ title: title || `▲ ${safeSession}`,
114
+ cwd,
115
+ },
116
+ );
117
+ }
118
+
119
+ export function openDashboardRuntimeTarget(runtime, opts = {}) {
120
+ const {
121
+ teammateMode = "",
122
+ sessionName = "",
123
+ targetPane = "",
124
+ layout = "1xN",
125
+ openAll = false,
126
+ cwd = process.cwd(),
127
+ title = "",
128
+ } = { ...runtime, ...opts };
129
+
130
+ if (teammateMode === "headless") {
131
+ return openHeadlessDashboardTarget(sessionName, {
132
+ worker: openAll ? null : targetPane,
133
+ openAll,
134
+ cwd,
135
+ title,
136
+ });
137
+ }
138
+
139
+ if ((teammateMode === "wt" || String(targetPane).startsWith("wt:")) && !openAll) {
140
+ return focusManagedPane(targetPane, { teammateMode: "wt", layout });
141
+ }
142
+
143
+ try {
144
+ if (!openAll && targetPane) focusManagedPane(targetPane, { teammateMode, layout });
145
+ return spawnWindowsTerminal(resolveAttachCommand(sessionName), {
146
+ mode: decideDashboardOpenMode({ openAll }),
147
+ title: title || `▲ ${sanitizeSessionName(sessionName)}`,
148
+ cwd,
149
+ });
150
+ } catch {
151
+ return false;
152
+ }
153
+ }