@triflux/remote 10.53.0 → 10.55.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.
Files changed (47) hide show
  1. package/hub/team/backend.mjs +10 -3
  2. package/hub/team/cli/commands/start/index.mjs +0 -2
  3. package/hub/team/handoff.mjs +1 -1
  4. package/hub/team/headless.mjs +41 -57
  5. package/hub/team/intervention.mjs +6 -4
  6. package/hub/team/nativeProxy.mjs +2 -48
  7. package/hub/team/pane.mjs +31 -18
  8. package/hub/team/psmux.mjs +36 -20
  9. package/hub/team/remote-session.mjs +66 -56
  10. package/hub/team/retry-state-machine.mjs +1 -1
  11. package/hub/team/session.mjs +80 -59
  12. package/hub/team/terminal-opener.mjs +5 -19
  13. package/hub/team/tui-viewer.mjs +2 -2
  14. package/hub/team/tui.mjs +0 -6
  15. package/hub/workers/factory.mjs +1 -10
  16. package/hub/workers/interface.mjs +0 -7
  17. package/package.json +2 -6
  18. package/scripts/lib/backup-rotation.mjs +60 -0
  19. package/scripts/lib/codex-profile-config.mjs +20 -7
  20. package/scripts/lib/codex-recovery.sh +2 -3
  21. package/scripts/lib/install-blob-history.json +3196 -0
  22. package/scripts/lib/install-retire.mjs +203 -0
  23. package/scripts/lib/legacy-hook-cleanup.mjs +148 -13
  24. package/scripts/lib/legacy-mcp-cleanup.mjs +405 -104
  25. package/scripts/lib/machine-profile.mjs +2 -2
  26. package/scripts/lib/mcp-filter.mjs +1 -42
  27. package/scripts/lib/mcp-guard-engine.mjs +198 -19
  28. package/scripts/lib/mcp-manifest.mjs +0 -40
  29. package/scripts/lib/process-utils.mjs +19 -0
  30. package/scripts/lib/stable-node.mjs +35 -0
  31. package/scripts/lib/test-env.mjs +20 -0
  32. package/cto/brief.mjs +0 -73
  33. package/cto/collect.mjs +0 -490
  34. package/cto/current.schema.json +0 -148
  35. package/cto/events.mjs +0 -208
  36. package/cto/hygiene.mjs +0 -320
  37. package/cto/lake-root.mjs +0 -127
  38. package/cto/status.mjs +0 -124
  39. package/hub/lib/cto-env.mjs +0 -1
  40. package/hub/team/notify.mjs +0 -385
  41. package/hub/team/process-cleanup.mjs +0 -386
  42. package/hub/team/routing.mjs +0 -249
  43. package/hub/team/staleState.mjs +0 -403
  44. package/hub/team/worker-signal.mjs +0 -263
  45. package/hub/team/worker-signal.types.d.ts +0 -52
  46. package/hub/workers/codex-mcp.mjs +0 -853
  47. package/scripts/lib/remote-spawn-transfer.mjs +0 -226
package/cto/status.mjs DELETED
@@ -1,124 +0,0 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
- import { join } from "node:path";
3
-
4
- import { compactHygieneCounts, projectCtoHygiene } from "./hygiene.mjs";
5
- import { resolveLakeRootDir } from "./lake-root.mjs";
6
-
7
- const SCHEMA_VERSION = "cto-lake.v1";
8
-
9
- function hasFlag(args, flag) {
10
- return Array.isArray(args) && args.includes(flag);
11
- }
12
-
13
- function writeJson(stdout, payload) {
14
- stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
15
- }
16
-
17
- function readJson(filePath) {
18
- return JSON.parse(readFileSync(filePath, "utf8"));
19
- }
20
-
21
- function projectStatus(current) {
22
- return {
23
- schema_version: current?.schema_version || SCHEMA_VERSION,
24
- generated_at: current?.generated_at || null,
25
- repo: current?.repo || {},
26
- sources: current?.sources || {},
27
- summary: current?.summary || {},
28
- ledger_tail: Array.isArray(current?.ledger_tail) ? current.ledger_tail : [],
29
- hygiene: compactHygieneCounts(projectCtoHygiene({ current })),
30
- };
31
- }
32
-
33
- function countAvailableSources(sources) {
34
- const values = Object.values(sources || {});
35
- return {
36
- available: values.filter((source) => source?.available === true).length,
37
- total: values.length,
38
- };
39
- }
40
-
41
- function countActiveGoals(current) {
42
- const sources = current?.sources || {};
43
- return Object.values(sources).reduce((count, source) => {
44
- const goals = source?.detail?.active_goals;
45
- return count + (Array.isArray(goals) ? goals.length : 0);
46
- }, 0);
47
- }
48
-
49
- function formatRepo(repo) {
50
- const branch = repo?.branch || "unknown";
51
- const head = repo?.head ? String(repo.head).slice(0, 12) : "unknown";
52
- const dirty = repo?.dirty ? "dirty" : "clean";
53
- return `${repo?.root || "unknown"} (${branch}@${head}, ${dirty})`;
54
- }
55
-
56
- function formatLedgerEntry(entry) {
57
- const ts = entry?.ts || "?";
58
- const event = entry?.event || "event";
59
- const summary = entry?.summary ? ` - ${entry.summary}` : "";
60
- return `${ts} ${event}${summary}`;
61
- }
62
-
63
- function formatSnapshotAge(generatedAt, now) {
64
- const ageMs = new Date(now).getTime() - Date.parse(generatedAt);
65
- if (!Number.isFinite(ageMs)) return "unknown age";
66
- if (ageMs < 0) return "in the future";
67
- for (const [unit, ms] of [
68
- ["d", 86400000],
69
- ["h", 3600000],
70
- ["m", 60000],
71
- ["s", 1000],
72
- ]) {
73
- if (ageMs >= ms || unit === "s")
74
- return `${Math.floor(ageMs / ms)}${unit} ago`;
75
- }
76
- }
77
-
78
- function renderHumanStatus(status, now) {
79
- const { available, total } = countAvailableSources(status.sources);
80
- const activeGoals = countActiveGoals(status);
81
- const recent = Array.isArray(status.ledger_tail)
82
- ? status.ledger_tail.slice(-2)
83
- : [];
84
- const lines = [
85
- `generated_at: ${status.generated_at || "unknown"} (${formatSnapshotAge(status.generated_at, now)})`,
86
- `repo: ${formatRepo(status.repo)}`,
87
- `sources: ${available}/${total} available`,
88
- `active: ${activeGoals} goals`,
89
- ];
90
- if (recent.length) {
91
- lines.push("ledger:");
92
- for (const entry of recent) lines.push(`- ${formatLedgerEntry(entry)}`);
93
- } else {
94
- lines.push("ledger: no recent events");
95
- }
96
- return `${lines.join("\n")}\n`;
97
- }
98
-
99
- export async function runStatus(args = [], opts = {}) {
100
- const rootDir = opts.rootDir || resolveLakeRootDir(process.cwd());
101
- const lakeRoot = opts.lakeRoot || join(rootDir, ".triflux", "lake");
102
- const stdout = opts.stdout || process.stdout;
103
- const jsonOut = opts.json === true || hasFlag(args, "--json");
104
- const currentPath = join(lakeRoot, "current.json");
105
-
106
- if (!existsSync(currentPath)) {
107
- const missing = {
108
- schema_version: SCHEMA_VERSION,
109
- available: false,
110
- hint: "run tfx cto collect",
111
- };
112
- if (jsonOut) writeJson(stdout, missing);
113
- else stdout.write("cto status unavailable: run tfx cto collect\n");
114
- return missing;
115
- }
116
-
117
- const current = readJson(currentPath);
118
- const status = projectStatus(current);
119
-
120
- if (jsonOut) writeJson(stdout, status);
121
- else stdout.write(renderHumanStatus(status, opts.now ?? Date.now()));
122
-
123
- return status;
124
- }
@@ -1 +0,0 @@
1
- export * from "@triflux/core/hub/lib/cto-env.mjs";
@@ -1,385 +0,0 @@
1
- // hub/team/notify.mjs — team notifier (bell / Windows toast / webhook)
2
-
3
- import { execFile } from "node:child_process";
4
- import os from "node:os";
5
-
6
- export const NOTIFY_EVENT_TYPES = Object.freeze([
7
- "completed",
8
- "failed",
9
- "inputWait",
10
- "ctoHygiene",
11
- ]);
12
- export const NOTIFY_CHANNELS = Object.freeze(["bell", "toast", "webhook"]);
13
-
14
- function freezeRecord(record) {
15
- return Object.freeze({ ...record });
16
- }
17
-
18
- function escapePowerShellSingleQuoted(value) {
19
- return String(value ?? "").replaceAll("'", "''");
20
- }
21
-
22
- function escapeAppleScriptString(value) {
23
- return String(value ?? "")
24
- .replaceAll("\\", "\\\\")
25
- .replaceAll('"', '\\"')
26
- .replace(/\r?\n/g, " ");
27
- }
28
-
29
- function normalizeTimestamp(value) {
30
- if (value instanceof Date) return value.toISOString();
31
- if (value == null || value === "") return new Date().toISOString();
32
- return String(value);
33
- }
34
-
35
- function normalizeEvent(event, defaults = {}) {
36
- if (!event || typeof event !== "object") {
37
- throw new TypeError("notify(event) requires an event object");
38
- }
39
-
40
- const type = String(event.type || "").trim();
41
- if (!NOTIFY_EVENT_TYPES.includes(type)) {
42
- throw new TypeError(`Unsupported notify event type: ${type || "<empty>"}`);
43
- }
44
-
45
- return freezeRecord({
46
- type,
47
- sessionId: event.sessionId == null ? "" : String(event.sessionId),
48
- host:
49
- event.host == null || event.host === ""
50
- ? String(defaults.host || os.hostname())
51
- : String(event.host),
52
- summary: event.summary == null ? "" : String(event.summary),
53
- timestamp: normalizeTimestamp(event.timestamp),
54
- });
55
- }
56
-
57
- function defaultChannelConfig(name, env, platform = process.platform) {
58
- switch (name) {
59
- case "bell":
60
- return { enabled: envFlag(env?.TRIFLUX_NOTIFY_BELL, true) };
61
- case "toast":
62
- return {
63
- // macOS `osascript display notification` can surface Script Editor
64
- // authorization/UI prompts. Keep it opt-in there; Windows keeps the
65
- // historical toast default, and other platforms skip as unsupported.
66
- enabled: envFlag(env?.TRIFLUX_NOTIFY_TOAST, platform !== "darwin"),
67
- };
68
- case "webhook": {
69
- const url = String(env?.TRIFLUX_NOTIFY_WEBHOOK || "");
70
- return { enabled: Boolean(url), url };
71
- }
72
- default:
73
- throw new TypeError(`Unknown notify channel: ${name}`);
74
- }
75
- }
76
-
77
- function envFlag(value, fallback) {
78
- if (value === undefined || value === null || value === "") return fallback;
79
- return !["0", "false", "no", "off"].includes(String(value).toLowerCase());
80
- }
81
-
82
- function normalizeChannelConfig(name, value, env, platform) {
83
- const base = defaultChannelConfig(name, env, platform);
84
- const patch =
85
- typeof value === "boolean"
86
- ? { enabled: value }
87
- : value && typeof value === "object"
88
- ? value
89
- : {};
90
-
91
- const next = {
92
- ...base,
93
- ...patch,
94
- };
95
-
96
- if ("enabled" in next) {
97
- next.enabled = Boolean(next.enabled);
98
- }
99
-
100
- if (name === "webhook") {
101
- next.url = String(next.url || env?.TRIFLUX_NOTIFY_WEBHOOK || "");
102
- }
103
-
104
- if (name === "toast") {
105
- if (next.command != null) next.command = String(next.command);
106
- if (next.timeoutMs != null)
107
- next.timeoutMs = Math.max(
108
- 1,
109
- Number.parseInt(String(next.timeoutMs), 10) || 5000,
110
- );
111
- }
112
-
113
- return freezeRecord(next);
114
- }
115
-
116
- function normalizeChannels(channels, env, platform) {
117
- const source = channels && typeof channels === "object" ? channels : {};
118
- const normalized = {};
119
- for (const name of NOTIFY_CHANNELS) {
120
- normalized[name] = normalizeChannelConfig(
121
- name,
122
- source[name],
123
- env,
124
- platform,
125
- );
126
- }
127
- return Object.freeze(normalized);
128
- }
129
-
130
- function updateChannelConfig(channels, channel, config, env, platform) {
131
- if (!NOTIFY_CHANNELS.includes(channel)) {
132
- throw new TypeError(`Unknown notify channel: ${channel}`);
133
- }
134
-
135
- return Object.freeze({
136
- ...channels,
137
- [channel]: normalizeChannelConfig(
138
- channel,
139
- {
140
- ...channels[channel],
141
- ...(typeof config === "boolean" ? { enabled: config } : config || {}),
142
- },
143
- env,
144
- platform,
145
- ),
146
- });
147
- }
148
-
149
- function formatEventTitle(event) {
150
- switch (event.type) {
151
- case "completed":
152
- return "Triflux completed";
153
- case "failed":
154
- return "Triflux failed";
155
- case "inputWait":
156
- return "Triflux waiting for input";
157
- case "ctoHygiene":
158
- return "Triflux CTO hygiene needs action";
159
- default:
160
- return "Triflux notification";
161
- }
162
- }
163
-
164
- function formatEventBody(event) {
165
- const parts = [];
166
- if (event.summary) parts.push(event.summary);
167
-
168
- const meta = [
169
- event.sessionId ? `session ${event.sessionId}` : "",
170
- event.host ? `host ${event.host}` : "",
171
- ]
172
- .filter(Boolean)
173
- .join(" · ");
174
- if (meta) parts.push(meta);
175
-
176
- return parts.join("\n") || event.timestamp;
177
- }
178
-
179
- function createResult(channel, status, extra = {}) {
180
- return freezeRecord({ channel, status, ...extra });
181
- }
182
-
183
- function execFileAsync(command, args, options, execFileFn) {
184
- return new Promise((resolve, reject) => {
185
- execFileFn(command, args, options, (error, stdout, stderr) => {
186
- if (error) {
187
- error.stdout = stdout;
188
- error.stderr = stderr;
189
- reject(error);
190
- return;
191
- }
192
- resolve({ stdout, stderr });
193
- });
194
- });
195
- }
196
-
197
- async function sendBell(config, deps) {
198
- if (!config.enabled)
199
- return createResult("bell", "skipped", { reason: "disabled" });
200
- const stream = deps.stdout;
201
- if (!stream || typeof stream.write !== "function") {
202
- return createResult("bell", "skipped", { reason: "stdout-unavailable" });
203
- }
204
-
205
- try {
206
- stream.write("\u0007");
207
- return createResult("bell", "sent");
208
- } catch (error) {
209
- return createResult("bell", "failed", { error: error.message });
210
- }
211
- }
212
-
213
- function buildToastScript(title, body) {
214
- const safeTitle = escapePowerShellSingleQuoted(title);
215
- const safeBody = escapePowerShellSingleQuoted(body);
216
- return [
217
- "$ErrorActionPreference = 'Stop'",
218
- `$Title = '${safeTitle}'`,
219
- `$Body = '${safeBody}'`,
220
- "if (Get-Command New-BurntToastNotification -ErrorAction SilentlyContinue) {",
221
- " New-BurntToastNotification -Text @($Title, $Body) | Out-Null",
222
- " return",
223
- "}",
224
- "[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null",
225
- "[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] > $null",
226
- "$escapedTitle = [System.Security.SecurityElement]::Escape($Title)",
227
- "$escapedBody = [System.Security.SecurityElement]::Escape($Body)",
228
- "$xml = New-Object Windows.Data.Xml.Dom.XmlDocument",
229
- "$xml.LoadXml(\"<toast><visual><binding template='ToastGeneric'><text>$escapedTitle</text><text>$escapedBody</text></binding></visual></toast>\")",
230
- "$toast = [Windows.UI.Notifications.ToastNotification]::new($xml)",
231
- "[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('Triflux').Show($toast)",
232
- ].join("; ");
233
- }
234
-
235
- async function sendToast(event, config, deps) {
236
- if (!config.enabled)
237
- return createResult("toast", "skipped", { reason: "disabled" });
238
- const execFileFn = deps.execFile || execFile;
239
-
240
- // macOS: osascript 네이티브 알림
241
- if ((deps.platform || process.platform) === "darwin") {
242
- const title = formatEventTitle(event);
243
- const body = formatEventBody(event);
244
- const safeTitle = escapeAppleScriptString(title);
245
- const safeBody = escapeAppleScriptString(body);
246
- try {
247
- await execFileAsync(
248
- "osascript",
249
- ["-e", `display notification "${safeBody}" with title "${safeTitle}"`],
250
- { timeout: config.timeoutMs || 5000 },
251
- execFileFn,
252
- );
253
- return createResult("toast", "sent", { command: "osascript" });
254
- } catch (error) {
255
- return createResult("toast", "failed", { error: error.message });
256
- }
257
- }
258
-
259
- if ((deps.platform || process.platform) !== "win32") {
260
- return createResult("toast", "skipped", { reason: "unsupported-platform" });
261
- }
262
- const candidates = config.command
263
- ? [config.command]
264
- : Array.isArray(deps.powerShellCandidates) &&
265
- deps.powerShellCandidates.length > 0
266
- ? deps.powerShellCandidates
267
- : ["pwsh", "powershell.exe"];
268
-
269
- const title = formatEventTitle(event);
270
- const body = formatEventBody(event);
271
- const script = buildToastScript(title, body);
272
- const failures = [];
273
-
274
- for (const command of candidates) {
275
- try {
276
- await execFileAsync(
277
- command,
278
- ["-NoLogo", "-NoProfile", "-Command", script],
279
- {
280
- windowsHide: true,
281
- timeout: config.timeoutMs || 5000,
282
- },
283
- execFileFn,
284
- );
285
- return createResult("toast", "sent", { command });
286
- } catch (error) {
287
- failures.push(`${command}: ${error.message}`);
288
- }
289
- }
290
-
291
- return createResult("toast", "failed", {
292
- error: failures.join(" | ") || "toast-send-failed",
293
- });
294
- }
295
-
296
- async function sendWebhook(event, config, deps) {
297
- if (!config.enabled)
298
- return createResult("webhook", "skipped", { reason: "disabled" });
299
- if (!config.url)
300
- return createResult("webhook", "skipped", { reason: "missing-url" });
301
- if (typeof deps.fetch !== "function") {
302
- return createResult("webhook", "failed", { error: "fetch-unavailable" });
303
- }
304
-
305
- try {
306
- const response = await deps.fetch(config.url, {
307
- method: "POST",
308
- headers: { "content-type": "application/json" },
309
- body: JSON.stringify(event),
310
- });
311
-
312
- if (!response?.ok) {
313
- return createResult("webhook", "failed", {
314
- error: `HTTP ${response?.status ?? "unknown"}`,
315
- });
316
- }
317
-
318
- return createResult("webhook", "sent", { statusCode: response.status });
319
- } catch (error) {
320
- return createResult("webhook", "failed", { error: error.message });
321
- }
322
- }
323
-
324
- function createNotifierInstance(channels, deps) {
325
- async function notify(event) {
326
- const normalizedEvent = normalizeEvent(event, { host: deps.hostname });
327
- const results = {
328
- bell: await sendBell(channels.bell, deps),
329
- toast: await sendToast(normalizedEvent, channels.toast, deps),
330
- webhook: await sendWebhook(normalizedEvent, channels.webhook, deps),
331
- };
332
-
333
- return freezeRecord({
334
- event: normalizedEvent,
335
- results: Object.freeze(results),
336
- });
337
- }
338
-
339
- function setChannel(channel, config) {
340
- return createNotifierInstance(
341
- updateChannelConfig(channels, channel, config, deps.env, deps.platform),
342
- deps,
343
- );
344
- }
345
-
346
- return Object.freeze({ notify, setChannel });
347
- }
348
-
349
- /**
350
- * 팀 세션 알림기 팩토리.
351
- * - bell: 터미널 BEL 문자
352
- * - toast: Windows PowerShell 기반 toast (BurntToast 우선, WinRT fallback)
353
- * - webhook: TRIFLUX_NOTIFY_WEBHOOK JSON POST
354
- *
355
- * Immutable pattern: setChannel()은 기존 notifier를 수정하지 않고 새 notifier를 반환한다.
356
- *
357
- * @param {object} [opts]
358
- * @param {object} [opts.channels] — 채널별 초기 설정
359
- * @param {object} [opts.env=process.env] — 환경 변수 소스
360
- * @param {NodeJS.WriteStream|{write:function}} [opts.stdout=process.stdout] — bell 출력 대상
361
- * @param {string} [opts.platform=process.platform] — 플랫폼 override (test 용)
362
- * @param {string} [opts.hostname=os.hostname()] — 기본 host override
363
- * @param {object} [opts.deps] — 테스트용 의존성 주입
364
- * @returns {{ notify(event: object): Promise<object>, setChannel(channel: string, config: object|boolean): object }}
365
- */
366
- export function createNotifier(opts = {}) {
367
- const env = opts.env || process.env;
368
- const platform = opts.platform || process.platform;
369
- const deps = Object.freeze({
370
- env,
371
- stdout: opts.stdout || process.stdout,
372
- execFile: opts.deps?.execFile || execFile,
373
- fetch: opts.deps?.fetch || globalThis.fetch?.bind(globalThis),
374
- platform,
375
- hostname: opts.hostname || os.hostname(),
376
- powerShellCandidates: Object.freeze(
377
- opts.deps?.powerShellCandidates || ["pwsh", "powershell.exe"],
378
- ),
379
- });
380
-
381
- return createNotifierInstance(
382
- normalizeChannels(opts.channels, env, platform),
383
- deps,
384
- );
385
- }