@triflux/remote 10.1.0 → 10.26.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/hub/pipe.mjs +239 -121
- package/hub/public/dashboard.html +61 -0
- package/hub/server.mjs +2179 -709
- package/hub/store-adapter.mjs +59 -26
- package/hub/store.mjs +356 -153
- package/hub/team/agent-map.json +26 -7
- package/hub/team/ansi.mjs +245 -146
- package/hub/team/backend.mjs +78 -17
- package/hub/team/build-worker-prompt.mjs +73 -0
- package/hub/team/check-mcp-hub.mjs +49 -0
- package/hub/team/claude-daemon-control.mjs +1130 -0
- package/hub/team/claude-native-bridge.mjs +1011 -0
- package/hub/team/claude-session-projection.mjs +74 -0
- package/hub/team/cli/commands/attach.mjs +57 -37
- package/hub/team/cli/commands/control.mjs +29 -8
- package/hub/team/cli/commands/debug.mjs +95 -74
- package/hub/team/cli/commands/focus.mjs +80 -53
- package/hub/team/cli/commands/interrupt.mjs +18 -6
- package/hub/team/cli/commands/kill.mjs +16 -5
- package/hub/team/cli/commands/list.mjs +31 -24
- package/hub/team/cli/commands/send.mjs +19 -6
- package/hub/team/cli/commands/start/index.mjs +151 -33
- package/hub/team/cli/commands/start/parse-args.mjs +97 -13
- package/hub/team/cli/commands/start/start-headless.mjs +155 -37
- package/hub/team/cli/commands/start/start-in-process.mjs +50 -40
- package/hub/team/cli/commands/start/start-mux.mjs +122 -73
- package/hub/team/cli/commands/start/start-wt.mjs +86 -69
- package/hub/team/cli/commands/status.mjs +43 -14
- package/hub/team/cli/commands/stop.mjs +11 -4
- package/hub/team/cli/commands/task.mjs +8 -3
- package/hub/team/cli/commands/tasks.mjs +13 -13
- package/hub/team/cli/help.mjs +1 -1
- package/hub/team/cli/index.mjs +2 -2
- package/hub/team/cli/manifest.mjs +38 -8
- package/hub/team/cli/render.mjs +52 -30
- package/hub/team/cli/services/attach-fallback.mjs +74 -54
- package/hub/team/cli/services/hub-client.mjs +44 -16
- package/hub/team/cli/services/member-selector.mjs +37 -30
- package/hub/team/cli/services/native-control.mjs +144 -116
- package/hub/team/cli/services/runtime-mode.mjs +61 -4
- package/hub/team/cli/services/state-store.mjs +66 -9
- package/hub/team/cli/services/task-model.mjs +40 -30
- package/hub/team/codex-review.mjs +521 -0
- package/hub/team/conductor-mesh-bridge.mjs +25 -24
- package/hub/team/conductor-registry.mjs +121 -0
- package/hub/team/conductor.mjs +1379 -671
- package/hub/team/consensus-meta.mjs +176 -0
- package/hub/team/dashboard-anchor.mjs +4 -5
- package/hub/team/dashboard-layout.mjs +3 -1
- package/hub/team/dashboard-open.mjs +20 -118
- package/hub/team/dashboard.mjs +76 -28
- package/hub/team/event-log.mjs +18 -10
- package/hub/team/execution-mode.mjs +256 -0
- package/hub/team/extract-completion-payload.mjs +110 -0
- package/hub/team/git-preflight.mjs +286 -0
- package/hub/team/handoff.mjs +49 -18
- package/hub/team/headless-bridge-session.mjs +17 -0
- package/hub/team/headless.mjs +2213 -1156
- package/hub/team/health-probe.mjs +232 -69
- package/hub/team/interactive-native-launcher.mjs +106 -0
- package/hub/team/interactive-native-worker.mjs +80 -0
- package/hub/team/interactive-tui-transport.mjs +465 -0
- package/hub/team/launcher-template.mjs +49 -17
- package/hub/team/lead-control.mjs +91 -4
- package/hub/team/mcp-selector.mjs +145 -0
- package/hub/team/native-supervisor.mjs +90 -26
- package/hub/team/native.mjs +120 -42
- package/hub/team/nativeProxy.mjs +184 -113
- package/hub/team/notify.mjs +147 -78
- package/hub/team/orchestrator.mjs +173 -161
- package/hub/team/pane.mjs +43 -20
- package/hub/team/process-cleanup.mjs +69 -25
- package/hub/team/psmux.mjs +1022 -281
- package/hub/team/recovery-store.mjs +149 -0
- package/hub/team/remote-probe.mjs +68 -52
- package/hub/team/remote-session.mjs +131 -59
- package/hub/team/remote-watcher.mjs +61 -33
- package/hub/team/retry-state-machine.mjs +336 -0
- package/hub/team/routing.mjs +51 -25
- package/hub/team/runtime-strategy.mjs +200 -0
- package/hub/team/schemas/worker-completion.json +39 -0
- package/hub/team/sentinel-capture.mjs +127 -0
- package/hub/team/session-sync.mjs +153 -6
- package/hub/team/session.mjs +574 -611
- package/hub/team/shared.mjs +13 -13
- package/hub/team/staleState.mjs +72 -30
- package/hub/team/swarm-cli.mjs +390 -0
- package/hub/team/swarm-hypervisor.mjs +1544 -193
- package/hub/team/swarm-intent.mjs +216 -0
- package/hub/team/swarm-locks.mjs +140 -25
- package/hub/team/swarm-planner.mjs +121 -21
- package/hub/team/swarm-preflight.mjs +319 -0
- package/hub/team/swarm-reconciler.mjs +128 -23
- package/hub/team/synapse-cli.mjs +458 -0
- package/hub/team/synapse-http.mjs +60 -0
- package/hub/team/synapse-registry.mjs +329 -0
- package/hub/team/terminal-opener.mjs +216 -0
- package/hub/team/tui-core.mjs +292 -0
- package/hub/team/tui-lite.mjs +237 -148
- package/hub/team/tui-remote-adapter.mjs +18 -34
- package/hub/team/tui-synapse.mjs +223 -0
- package/hub/team/tui-viewer.mjs +108 -48
- package/hub/team/tui-widgets.mjs +262 -0
- package/hub/team/tui.mjs +931 -439
- package/hub/team/worker-completion-validator.mjs +84 -0
- package/hub/team/worker-sandbox.mjs +97 -0
- package/hub/team/worker-signal.mjs +263 -0
- package/hub/team/worker-signal.types.d.ts +52 -0
- package/hub/team/worktree-lifecycle.mjs +517 -63
- package/hub/team/wt-manager.mjs +297 -26
- package/hub/tools.mjs +477 -252
- package/hub/tray.mjs +411 -368
- package/hub/workers/antigravity-route-worker.mjs +169 -0
- package/hub/workers/claude-worker.mjs +162 -118
- package/hub/workers/codex-app-server-worker.mjs +1015 -0
- package/hub/workers/codex-mcp.mjs +724 -507
- package/hub/workers/delegator-mcp.mjs +570 -499
- package/hub/workers/factory.mjs +103 -21
- package/hub/workers/gemini-worker.mjs +126 -284
- package/hub/workers/interface.mjs +24 -2
- package/hub/workers/lib/jsonrpc-stdio.mjs +463 -0
- package/hub/workers/worker-utils.mjs +23 -17
- package/package.json +4 -3
- package/scripts/lib/agent-json.mjs +27 -0
- package/scripts/lib/async.mjs +174 -0
- package/scripts/lib/claudemd-scanner.mjs +223 -0
- package/scripts/lib/cli-agy.mjs +62 -0
- package/scripts/lib/cli-claude.mjs +78 -0
- package/scripts/lib/cli-codex.mjs +199 -0
- package/scripts/lib/cli-gemini.mjs +37 -0
- package/scripts/lib/codex-recovery.sh +50 -0
- package/scripts/lib/context.mjs +67 -0
- package/scripts/lib/cross-review-utils.mjs +59 -0
- package/scripts/lib/doctor-env-checks.mjs +121 -0
- package/scripts/lib/dynamic-route-cli.mjs +107 -0
- package/scripts/lib/env-probe.mjs +269 -0
- package/scripts/lib/env.mjs +14 -0
- package/scripts/lib/gemini-profiles.mjs +119 -0
- package/scripts/lib/handoff.mjs +187 -0
- package/scripts/lib/hook-utils.mjs +16 -0
- package/scripts/lib/hub.mjs +48 -0
- package/scripts/lib/keyword-rules.mjs +195 -0
- package/scripts/lib/logger.mjs +105 -0
- package/scripts/lib/mcp-filter.mjs +883 -0
- package/scripts/lib/mcp-gateway-health-check.mjs +159 -0
- package/scripts/lib/mcp-gateway-servers.mjs +70 -0
- package/scripts/lib/mcp-gateway-wrapper-check.mjs +35 -0
- package/scripts/lib/mcp-guard-engine.mjs +2120 -0
- package/scripts/lib/mcp-health.mjs +773 -0
- package/scripts/lib/mcp-manifest.mjs +89 -0
- package/scripts/lib/mcp-server-catalog.mjs +355 -0
- package/scripts/lib/pid.mjs +63 -0
- package/scripts/lib/process-utils.mjs +26 -0
- package/scripts/lib/psmux-info.mjs +224 -0
- package/scripts/lib/quota.mjs +47 -0
- package/scripts/lib/remote-spawn-transfer.mjs +226 -0
- package/scripts/lib/skill-state.mjs +220 -0
- package/scripts/lib/skill-template.mjs +375 -0
- package/scripts/lib/team.mjs +75 -0
- package/scripts/lib/timeout.mjs +66 -0
- package/scripts/lib/tmp.mjs +26 -0
- package/scripts/lib/toml.mjs +113 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const DEFAULT_SYNAPSE_BASE_URL = "http://127.0.0.1:27888";
|
|
2
|
+
|
|
3
|
+
function resolveSynapseFetch(fetchImpl) {
|
|
4
|
+
if (fetchImpl === null) return null;
|
|
5
|
+
if (typeof fetchImpl === "function") return fetchImpl;
|
|
6
|
+
if (typeof globalThis.fetch === "function") {
|
|
7
|
+
return globalThis.fetch.bind(globalThis);
|
|
8
|
+
}
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function buildSynapseTaskSummary(prompt, maxLength = 100) {
|
|
13
|
+
if (maxLength <= 0) return "";
|
|
14
|
+
return String(prompt ?? "").slice(0, maxLength);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function fireAndForgetSynapse(path, payload, opts = {}) {
|
|
18
|
+
const fetchImpl = resolveSynapseFetch(opts.fetchImpl);
|
|
19
|
+
if (!fetchImpl) return false;
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const url = new URL(
|
|
23
|
+
path,
|
|
24
|
+
opts.baseUrl || DEFAULT_SYNAPSE_BASE_URL,
|
|
25
|
+
).toString();
|
|
26
|
+
Promise.resolve(
|
|
27
|
+
fetchImpl(url, {
|
|
28
|
+
method: "POST",
|
|
29
|
+
headers: { "content-type": "application/json" },
|
|
30
|
+
body: JSON.stringify(payload),
|
|
31
|
+
}),
|
|
32
|
+
).catch(() => {});
|
|
33
|
+
return true;
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function registerSynapseSession(meta, opts = {}) {
|
|
40
|
+
return fireAndForgetSynapse("/synapse/register", meta, opts);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function heartbeatSynapseSession(
|
|
44
|
+
sessionId,
|
|
45
|
+
partialMeta = {},
|
|
46
|
+
opts = {},
|
|
47
|
+
) {
|
|
48
|
+
return fireAndForgetSynapse(
|
|
49
|
+
"/synapse/heartbeat",
|
|
50
|
+
{
|
|
51
|
+
sessionId,
|
|
52
|
+
...(partialMeta && typeof partialMeta === "object" ? partialMeta : {}),
|
|
53
|
+
},
|
|
54
|
+
opts,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function unregisterSynapseSession(sessionId, opts = {}) {
|
|
59
|
+
return fireAndForgetSynapse("/synapse/unregister", { sessionId }, opts);
|
|
60
|
+
}
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_LOCAL_HEARTBEAT_INTERVAL_MS = 5_000;
|
|
5
|
+
const DEFAULT_LOCAL_TIMEOUT_MS = 30_000;
|
|
6
|
+
const DEFAULT_REMOTE_HEARTBEAT_INTERVAL_MS = 15_000;
|
|
7
|
+
const DEFAULT_REMOTE_TIMEOUT_MS = 90_000;
|
|
8
|
+
|
|
9
|
+
function normalizeSessionId(sessionId) {
|
|
10
|
+
if (sessionId == null) return "";
|
|
11
|
+
return String(sessionId).trim();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function cloneSession(session) {
|
|
15
|
+
return {
|
|
16
|
+
...session,
|
|
17
|
+
dirtyFiles: Array.isArray(session.dirtyFiles)
|
|
18
|
+
? [...session.dirtyFiles]
|
|
19
|
+
: [],
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function sanitizeSession(raw, fallbackSessionId = "") {
|
|
24
|
+
const sessionId = normalizeSessionId(raw?.sessionId ?? fallbackSessionId);
|
|
25
|
+
if (!sessionId) return null;
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
sessionId,
|
|
29
|
+
host: typeof raw?.host === "string" ? raw.host : "local",
|
|
30
|
+
worktreePath: typeof raw?.worktreePath === "string" ? raw.worktreePath : "",
|
|
31
|
+
branch: typeof raw?.branch === "string" ? raw.branch : "",
|
|
32
|
+
dirtyFiles: Array.isArray(raw?.dirtyFiles) ? [...raw.dirtyFiles] : [],
|
|
33
|
+
taskSummary: typeof raw?.taskSummary === "string" ? raw.taskSummary : "",
|
|
34
|
+
lastHeartbeat:
|
|
35
|
+
typeof raw?.lastHeartbeat === "number" ? raw.lastHeartbeat : Date.now(),
|
|
36
|
+
status:
|
|
37
|
+
raw?.status === "stale" || raw?.status === "expired"
|
|
38
|
+
? raw.status
|
|
39
|
+
: "active",
|
|
40
|
+
isRemote: Boolean(raw?.isRemote),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createSynapseRegistry(opts = {}) {
|
|
45
|
+
const {
|
|
46
|
+
persistPath,
|
|
47
|
+
emitter = null,
|
|
48
|
+
localHeartbeatIntervalMs = DEFAULT_LOCAL_HEARTBEAT_INTERVAL_MS,
|
|
49
|
+
localTimeoutMs = DEFAULT_LOCAL_TIMEOUT_MS,
|
|
50
|
+
remoteHeartbeatIntervalMs = DEFAULT_REMOTE_HEARTBEAT_INTERVAL_MS,
|
|
51
|
+
remoteTimeoutMs = DEFAULT_REMOTE_TIMEOUT_MS,
|
|
52
|
+
} = opts;
|
|
53
|
+
|
|
54
|
+
const sessions = new Map();
|
|
55
|
+
const monitors = new Map();
|
|
56
|
+
const staleCallbacks = new Set();
|
|
57
|
+
const removedCallbacks = new Set();
|
|
58
|
+
|
|
59
|
+
function now() {
|
|
60
|
+
return Date.now();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function intervalFor(session) {
|
|
64
|
+
return session.isRemote
|
|
65
|
+
? remoteHeartbeatIntervalMs
|
|
66
|
+
: localHeartbeatIntervalMs;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function timeoutFor(session) {
|
|
70
|
+
return session.isRemote ? remoteTimeoutMs : localTimeoutMs;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let persistTimer = null;
|
|
74
|
+
let destroyed = false;
|
|
75
|
+
|
|
76
|
+
function persist() {
|
|
77
|
+
if (!persistPath) return;
|
|
78
|
+
try {
|
|
79
|
+
mkdirSync(dirname(persistPath), { recursive: true });
|
|
80
|
+
const data = Object.fromEntries(
|
|
81
|
+
[...sessions].map(([k, v]) => [k, cloneSession(v)]),
|
|
82
|
+
);
|
|
83
|
+
writeFileSync(persistPath, JSON.stringify(data, null, 2), "utf8");
|
|
84
|
+
} catch {
|
|
85
|
+
/* best-effort */
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function schedulePersist() {
|
|
90
|
+
if (destroyed || persistTimer) return;
|
|
91
|
+
persistTimer = setTimeout(() => {
|
|
92
|
+
persistTimer = null;
|
|
93
|
+
if (!destroyed) persist();
|
|
94
|
+
}, 200);
|
|
95
|
+
if (typeof persistTimer.unref === "function") persistTimer.unref();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function restore() {
|
|
99
|
+
if (!persistPath || !existsSync(persistPath)) return;
|
|
100
|
+
try {
|
|
101
|
+
const data = JSON.parse(readFileSync(persistPath, "utf8"));
|
|
102
|
+
for (const [sessionId, session] of Object.entries(data)) {
|
|
103
|
+
const sanitized = sanitizeSession(session, sessionId);
|
|
104
|
+
if (sanitized) sessions.set(sanitized.sessionId, sanitized);
|
|
105
|
+
}
|
|
106
|
+
} catch {
|
|
107
|
+
/* corrupted file — start fresh */
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function stopMonitor(sessionId) {
|
|
112
|
+
const timer = monitors.get(sessionId);
|
|
113
|
+
if (!timer) return;
|
|
114
|
+
clearInterval(timer);
|
|
115
|
+
monitors.delete(sessionId);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function notifyStale(session) {
|
|
119
|
+
const clone = cloneSession(session);
|
|
120
|
+
emitter?.emit("synapse.session.stale", {
|
|
121
|
+
sessionId: session.sessionId,
|
|
122
|
+
session: clone,
|
|
123
|
+
});
|
|
124
|
+
for (const callback of staleCallbacks) {
|
|
125
|
+
try {
|
|
126
|
+
callback(clone);
|
|
127
|
+
} catch {
|
|
128
|
+
/* no-op */
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function notifyRemoved(session) {
|
|
134
|
+
const clone = cloneSession(session);
|
|
135
|
+
emitter?.emit("synapse.session.removed", {
|
|
136
|
+
sessionId: session.sessionId,
|
|
137
|
+
session: clone,
|
|
138
|
+
});
|
|
139
|
+
for (const callback of removedCallbacks) {
|
|
140
|
+
try {
|
|
141
|
+
callback(clone);
|
|
142
|
+
} catch {
|
|
143
|
+
/* no-op */
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function startMonitor(sessionId) {
|
|
149
|
+
stopMonitor(sessionId);
|
|
150
|
+
|
|
151
|
+
const session = sessions.get(sessionId);
|
|
152
|
+
if (!session) return;
|
|
153
|
+
|
|
154
|
+
const timer = setInterval(() => {
|
|
155
|
+
const current = sessions.get(sessionId);
|
|
156
|
+
if (!current) return;
|
|
157
|
+
|
|
158
|
+
const elapsedMs = now() - current.lastHeartbeat;
|
|
159
|
+
if (elapsedMs > timeoutFor(current) && current.status !== "stale") {
|
|
160
|
+
const staled = { ...current, status: "stale" };
|
|
161
|
+
sessions.set(sessionId, staled);
|
|
162
|
+
schedulePersist();
|
|
163
|
+
setImmediate(() => {
|
|
164
|
+
if (!destroyed) notifyStale(staled);
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}, intervalFor(session));
|
|
168
|
+
|
|
169
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
170
|
+
monitors.set(sessionId, timer);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
restore();
|
|
174
|
+
for (const sessionId of sessions.keys()) {
|
|
175
|
+
startMonitor(sessionId);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function register(sessionIdOrMeta, meta = null) {
|
|
179
|
+
const normalizedMeta =
|
|
180
|
+
meta && typeof meta === "object"
|
|
181
|
+
? { ...meta, sessionId: sessionIdOrMeta }
|
|
182
|
+
: sessionIdOrMeta;
|
|
183
|
+
const sessionId = normalizeSessionId(normalizedMeta?.sessionId);
|
|
184
|
+
if (!sessionId) {
|
|
185
|
+
return { ok: false, sessionId, reason: "invalid_id" };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (sessions.has(sessionId)) {
|
|
189
|
+
console.warn(
|
|
190
|
+
"[synapse-registry] duplicate registration rejected:",
|
|
191
|
+
sessionId,
|
|
192
|
+
);
|
|
193
|
+
return { ok: false, sessionId, reason: "duplicate" };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const session = sanitizeSession(
|
|
197
|
+
{
|
|
198
|
+
...normalizedMeta,
|
|
199
|
+
sessionId,
|
|
200
|
+
status: "active",
|
|
201
|
+
lastHeartbeat: now(),
|
|
202
|
+
},
|
|
203
|
+
sessionId,
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
sessions.set(sessionId, session);
|
|
207
|
+
startMonitor(sessionId);
|
|
208
|
+
persist();
|
|
209
|
+
|
|
210
|
+
emitter?.emit("synapse.session.started", {
|
|
211
|
+
sessionId,
|
|
212
|
+
session: cloneSession(session),
|
|
213
|
+
});
|
|
214
|
+
return { ok: true, sessionId };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function unregister(sessionId) {
|
|
218
|
+
const normalized = normalizeSessionId(sessionId);
|
|
219
|
+
const session = sessions.get(normalized);
|
|
220
|
+
if (!session) return false;
|
|
221
|
+
|
|
222
|
+
stopMonitor(normalized);
|
|
223
|
+
sessions.delete(normalized);
|
|
224
|
+
persist();
|
|
225
|
+
notifyRemoved(session);
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function heartbeat(sessionId, partialMeta = null) {
|
|
230
|
+
const normalized = normalizeSessionId(sessionId);
|
|
231
|
+
const session = sessions.get(normalized);
|
|
232
|
+
if (!session) return false;
|
|
233
|
+
|
|
234
|
+
const wasRemote = session.isRemote;
|
|
235
|
+
const updated = { ...session, lastHeartbeat: now(), status: "active" };
|
|
236
|
+
|
|
237
|
+
if (partialMeta && typeof partialMeta === "object") {
|
|
238
|
+
if (typeof partialMeta.host === "string") updated.host = partialMeta.host;
|
|
239
|
+
if (typeof partialMeta.worktreePath === "string") {
|
|
240
|
+
updated.worktreePath = partialMeta.worktreePath;
|
|
241
|
+
}
|
|
242
|
+
if (typeof partialMeta.branch === "string")
|
|
243
|
+
updated.branch = partialMeta.branch;
|
|
244
|
+
if (Array.isArray(partialMeta.dirtyFiles)) {
|
|
245
|
+
updated.dirtyFiles = partialMeta.dirtyFiles.filter(
|
|
246
|
+
(f) => typeof f === "string" && f.length > 0,
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
if (typeof partialMeta.taskSummary === "string") {
|
|
250
|
+
updated.taskSummary = partialMeta.taskSummary;
|
|
251
|
+
}
|
|
252
|
+
if (typeof partialMeta.isRemote === "boolean") {
|
|
253
|
+
updated.isRemote = partialMeta.isRemote;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
sessions.set(normalized, updated);
|
|
258
|
+
|
|
259
|
+
if (updated.isRemote !== wasRemote) {
|
|
260
|
+
startMonitor(normalized);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
schedulePersist();
|
|
264
|
+
emitter?.emit("synapse.session.heartbeat", {
|
|
265
|
+
sessionId: normalized,
|
|
266
|
+
session: cloneSession(updated),
|
|
267
|
+
partial: partialMeta,
|
|
268
|
+
});
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function getActive() {
|
|
273
|
+
return [...sessions.values()]
|
|
274
|
+
.filter((session) => session.status === "active")
|
|
275
|
+
.map((session) => cloneSession(session));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function getAll() {
|
|
279
|
+
return [...sessions.values()].map((session) => cloneSession(session));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function getSession(sessionId) {
|
|
283
|
+
const normalized = normalizeSessionId(sessionId);
|
|
284
|
+
if (!normalized) return null;
|
|
285
|
+
const session = sessions.get(normalized);
|
|
286
|
+
return session ? cloneSession(session) : null;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function onStale(callback) {
|
|
290
|
+
if (typeof callback !== "function") return;
|
|
291
|
+
staleCallbacks.add(callback);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function onRemoved(callback) {
|
|
295
|
+
if (typeof callback !== "function") return;
|
|
296
|
+
removedCallbacks.add(callback);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function snapshot() {
|
|
300
|
+
return {
|
|
301
|
+
sessions: getAll(),
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function destroy() {
|
|
306
|
+
destroyed = true;
|
|
307
|
+
for (const sessionId of monitors.keys()) {
|
|
308
|
+
stopMonitor(sessionId);
|
|
309
|
+
}
|
|
310
|
+
if (persistTimer) {
|
|
311
|
+
clearTimeout(persistTimer);
|
|
312
|
+
persistTimer = null;
|
|
313
|
+
}
|
|
314
|
+
persist();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return Object.freeze({
|
|
318
|
+
register,
|
|
319
|
+
unregister,
|
|
320
|
+
heartbeat,
|
|
321
|
+
getActive,
|
|
322
|
+
getAll,
|
|
323
|
+
getSession,
|
|
324
|
+
onStale,
|
|
325
|
+
onRemoved,
|
|
326
|
+
snapshot,
|
|
327
|
+
destroy,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { exec as defaultExec, spawnSync } from "node:child_process";
|
|
2
|
+
import { platform as osPlatform } from "node:os";
|
|
3
|
+
import { psmuxExec as defaultPsmuxExec } from "./psmux.mjs";
|
|
4
|
+
import { tmuxExec as defaultTmuxExec, detectMultiplexer } from "./session.mjs";
|
|
5
|
+
import { createWtManager as defaultCreateWtManager } from "./wt-manager.mjs";
|
|
6
|
+
|
|
7
|
+
const TMUX_LIKE_MUXES = new Set(["tmux", "git-bash-tmux"]);
|
|
8
|
+
|
|
9
|
+
export function sanitizeTerminalTitle(value, fallback = "triflux") {
|
|
10
|
+
const title = String(value ?? "")
|
|
11
|
+
.replace(/\s+/gu, " ")
|
|
12
|
+
.trim();
|
|
13
|
+
return title || fallback;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function shellQuote(value) {
|
|
17
|
+
return `'${String(value).replace(/'/g, "'\\''")}'`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function powershellSingleQuote(value) {
|
|
21
|
+
return `'${String(value).replace(/'/g, "''")}'`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function buildCommandString({ cwd, command }) {
|
|
25
|
+
const commandString = String(command ?? "");
|
|
26
|
+
if (!cwd) return commandString;
|
|
27
|
+
return `cd ${shellQuote(cwd)} && ${commandString}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function resolvePlatform(deps) {
|
|
31
|
+
if (typeof deps.platform === "function") return deps.platform();
|
|
32
|
+
return deps.platform || osPlatform();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function resolveMux(deps) {
|
|
36
|
+
if (Object.hasOwn(deps, "mux")) return deps.mux;
|
|
37
|
+
if (typeof deps.detectMultiplexer === "function")
|
|
38
|
+
return deps.detectMultiplexer();
|
|
39
|
+
return detectMultiplexer();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function createTabSpec(spec, title) {
|
|
43
|
+
return {
|
|
44
|
+
title,
|
|
45
|
+
command: String(spec.command ?? ""),
|
|
46
|
+
cwd: spec.cwd,
|
|
47
|
+
profile: spec.profile ?? "triflux",
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function execOpenTerminal(execFn) {
|
|
52
|
+
return new Promise((resolve) => {
|
|
53
|
+
execFn("open -a Terminal", { timeout: 5000 }, (error) => {
|
|
54
|
+
resolve(!error);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// psmux는 Windows에서 wt-manager 경유 (createTab/splitPane 등) — tmux 호환 unix 명령군과 다른 표면.
|
|
60
|
+
// 비-Windows(macOS/Linux) 환경에서는 psmux가 tmux-compatible new-window/select-pane 명령을 받아주므로
|
|
61
|
+
// tmux 어댑터와 동일하게 취급한다.
|
|
62
|
+
function isTmuxLikeMux(mux, platform) {
|
|
63
|
+
return TMUX_LIKE_MUXES.has(mux) || (platform !== "win32" && mux === "psmux");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function shellCommandName(value) {
|
|
67
|
+
const command = String(value);
|
|
68
|
+
return /^[A-Za-z0-9_./:-]+$/u.test(command) ? command : shellQuote(command);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function powershellCommandName(value) {
|
|
72
|
+
const command = String(value);
|
|
73
|
+
return /^[A-Za-z0-9_.:-]+$/u.test(command)
|
|
74
|
+
? command
|
|
75
|
+
: `& ${powershellSingleQuote(command)}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function getCommandVersion(command) {
|
|
79
|
+
const r = spawnSync(command, ["-V"], {
|
|
80
|
+
encoding: "utf8",
|
|
81
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
82
|
+
timeout: 3000,
|
|
83
|
+
windowsHide: true,
|
|
84
|
+
});
|
|
85
|
+
if ((r.status ?? 1) !== 0) return null;
|
|
86
|
+
return `${r.stdout || ""}${r.stderr || ""}`.trim();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function defaultPsmuxBinaryExists(command) {
|
|
90
|
+
try {
|
|
91
|
+
return /\bpsmux\b/i.test(getCommandVersion(command) || "");
|
|
92
|
+
} catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function buildAttachCommand(mux, sessionName, deps = {}) {
|
|
98
|
+
if (mux === "psmux") {
|
|
99
|
+
const command = process.env.PSMUX_BIN || "psmux";
|
|
100
|
+
const psmuxBinaryExists =
|
|
101
|
+
deps.psmuxBinaryExists || defaultPsmuxBinaryExists;
|
|
102
|
+
if (!psmuxBinaryExists(command)) return null;
|
|
103
|
+
return `${shellCommandName(command)} attach-session -t ${shellQuote(sessionName)}`;
|
|
104
|
+
}
|
|
105
|
+
return `tmux attach-session -t ${shellQuote(sessionName)}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// wt-manager.createTab은 (a) undefined/void 반환 — legacy success 의미 (b) {success:true|false, ...} 객체 반환의 두 형태가 공존.
|
|
109
|
+
// `result?.success !== false`는 undefined/null/{}을 모두 success로 취급하고 명시적 {success:false}만 실패로 본다.
|
|
110
|
+
function wtResultSucceeded(result) {
|
|
111
|
+
return result?.success !== false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function createTerminalOpener(deps = {}) {
|
|
115
|
+
const platform = resolvePlatform(deps);
|
|
116
|
+
const tmuxExec = deps.tmuxExec || defaultTmuxExec;
|
|
117
|
+
const psmuxExec = deps.psmuxExec || defaultPsmuxExec;
|
|
118
|
+
const exec = deps.exec || defaultExec;
|
|
119
|
+
const createWtManager = deps.createWtManager || defaultCreateWtManager;
|
|
120
|
+
|
|
121
|
+
async function openCommand(spec = {}) {
|
|
122
|
+
const title = sanitizeTerminalTitle(spec.title);
|
|
123
|
+
|
|
124
|
+
if (platform === "win32") {
|
|
125
|
+
const wt = createWtManager();
|
|
126
|
+
try {
|
|
127
|
+
return wtResultSucceeded(
|
|
128
|
+
await wt.createTab(createTabSpec(spec, title)),
|
|
129
|
+
);
|
|
130
|
+
} catch {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const mux = resolveMux(deps);
|
|
136
|
+
if (isTmuxLikeMux(mux, platform)) {
|
|
137
|
+
tmuxExec(
|
|
138
|
+
`new-window -n ${shellQuote(title)} ${shellQuote(
|
|
139
|
+
buildCommandString(spec),
|
|
140
|
+
)}`,
|
|
141
|
+
);
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (platform === "darwin") {
|
|
146
|
+
return execOpenTerminal(exec);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function openSession(sessionName, opts = {}) {
|
|
153
|
+
if (platform === "win32") {
|
|
154
|
+
const command = process.env.PSMUX_BIN || "psmux";
|
|
155
|
+
const psmuxBinaryExists =
|
|
156
|
+
deps.psmuxBinaryExists || defaultPsmuxBinaryExists;
|
|
157
|
+
if (!psmuxBinaryExists(command)) return false;
|
|
158
|
+
const wt = createWtManager();
|
|
159
|
+
try {
|
|
160
|
+
return wtResultSucceeded(
|
|
161
|
+
await wt.createTab({
|
|
162
|
+
title: sanitizeTerminalTitle(opts.title ?? sessionName),
|
|
163
|
+
command: `${powershellCommandName(
|
|
164
|
+
command,
|
|
165
|
+
)} attach-session -t ${powershellSingleQuote(sessionName)}`,
|
|
166
|
+
cwd: opts.cwd,
|
|
167
|
+
profile: opts.profile ?? "triflux",
|
|
168
|
+
}),
|
|
169
|
+
);
|
|
170
|
+
} catch {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const mux = resolveMux(deps);
|
|
176
|
+
if (isTmuxLikeMux(mux, platform)) {
|
|
177
|
+
const attachCommand = buildAttachCommand(mux, sessionName, deps);
|
|
178
|
+
if (!attachCommand) return false;
|
|
179
|
+
tmuxExec(
|
|
180
|
+
`new-window -n ${shellQuote(opts.title ?? sessionName)} ${shellQuote(
|
|
181
|
+
attachCommand,
|
|
182
|
+
)}`,
|
|
183
|
+
);
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function focusPane(sessionName, workerNumber) {
|
|
191
|
+
const target = `${sessionName}:0.${workerNumber}`;
|
|
192
|
+
const mux = resolveMux(deps);
|
|
193
|
+
|
|
194
|
+
if (mux === "psmux") {
|
|
195
|
+
psmuxExec(["select-pane", "-t", target]);
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (isTmuxLikeMux(mux, platform)) {
|
|
200
|
+
tmuxExec(`select-pane -t ${shellQuote(target)}`);
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return { openCommand, openSession, focusPane };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function openSessionTarget(sessionName, opts = {}) {
|
|
211
|
+
return createTerminalOpener(opts._deps).openSession(sessionName, opts);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function focusSessionPane(sessionName, workerNumber, opts = {}) {
|
|
215
|
+
return createTerminalOpener(opts._deps).focusPane(sessionName, workerNumber);
|
|
216
|
+
}
|