triflux 10.34.0 → 10.35.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.
- package/bin/tfx-live.mjs +519 -45
- package/bin/triflux.mjs +16 -46
- package/cto/status.mjs +77 -1
- package/hooks/agy-session-hook.mjs +140 -0
- package/hooks/claude-cwd-projection-refresh.mjs +2 -2
- package/hub/bridge.mjs +130 -70
- package/hub/server.mjs +6 -4
- package/hub/team/build-worker-prompt.mjs +23 -4
- package/hub/team/claude-daemon-control.mjs +348 -4
- package/hub/team/claude-native-bridge.mjs +6 -8
- package/hub/team/conductor.mjs +1 -0
- package/hub/team/handoff.mjs +8 -4
- package/hub/team/headless.mjs +33 -11
- package/hub/team/native-supervisor.mjs +4 -0
- package/hub/team/orchestrator.mjs +7 -2
- package/hub/team/swarm-hypervisor.mjs +230 -44
- package/hub/team/worker-completion-validator.mjs +11 -16
- package/hub/team/worker-sandbox.mjs +29 -1
- package/hub/tray-lifecycle.mjs +2 -1
- package/hub/workers/delegator-mcp.mjs +1 -0
- package/hud/constants.mjs +2 -0
- package/hud/providers/gemini.mjs +135 -3
- package/hud/renderers.mjs +37 -35
- package/package.json +1 -1
- package/scripts/ensure-agy-hooks.mjs +134 -0
- package/scripts/ensure-codex-hooks.mjs +8 -0
- package/scripts/lib/mcp-manifest.mjs +2 -2
- package/scripts/mcp-gateway-start.ps1 +0 -1
- package/scripts/preflight-cache.mjs +230 -55
- package/scripts/setup.mjs +33 -2
- package/scripts/test-lock.mjs +15 -1
- package/scripts/tfx-route.sh +43 -9
- package/skills/tfx-setup/SKILL.md +6 -6
- package/skills/tfx-ship/SKILL.md +54 -23
- package/config/mcp-registry.json.bak-pre-serena-removal +0 -89
package/bin/tfx-live.mjs
CHANGED
|
@@ -2,9 +2,15 @@
|
|
|
2
2
|
import { execFile } from "node:child_process";
|
|
3
3
|
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import { homedir, tmpdir } from "node:os";
|
|
5
|
-
import { join as pathJoin } from "node:path";
|
|
5
|
+
import { join as pathJoin, resolve as pathResolve } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { promisify } from "node:util";
|
|
8
|
+
import {
|
|
9
|
+
escapePwshSingleQuoted as escapeRemotePwshSingleQuoted,
|
|
10
|
+
probeRemoteEnv as probeRemoteHostEnv,
|
|
11
|
+
shellQuote as remoteShellQuote,
|
|
12
|
+
validateHost as validateRemoteHost,
|
|
13
|
+
} from "../hub/team/remote-session.mjs";
|
|
8
14
|
|
|
9
15
|
const execFileAsync = promisify(execFile);
|
|
10
16
|
|
|
@@ -23,6 +29,7 @@ const BRIDGE_TIMEOUT_BUFFER_MS = 15_000;
|
|
|
23
29
|
// spill the JSON to a temp file and pass --payload-file instead.
|
|
24
30
|
const PAYLOAD_FILE_THRESHOLD = 96 * 1024;
|
|
25
31
|
const VALID_TRANSPORTS = ["tmux", "uds", "auto"];
|
|
32
|
+
const BOOLEAN_FLAGS = new Set(["json"]);
|
|
26
33
|
// uds-fallback diagnostics land here, written async so a failed daemon attach
|
|
27
34
|
// never blocks the tmux fallback path (see writeUdsBugReport / doAskAuto).
|
|
28
35
|
const BUG_REPORT_DIR =
|
|
@@ -34,11 +41,11 @@ function usage() {
|
|
|
34
41
|
"Usage:",
|
|
35
42
|
" tfx-live start --session NAME [--cli codex|claude] [--cwd DIR] [--remote HOST] [--resume ID] [--resume-last 1] [--ready-timeout 30] [--poll-interval 1500]",
|
|
36
43
|
" tfx-live ask --session NAME --prompt TEXT [--cli codex|claude] [--timeout 60] [--remote HOST] [--settle 1500] [--poll-interval 1500]",
|
|
37
|
-
" tfx-live ask --transport uds|auto (--short SHORT | --session-id ID) --prompt TEXT [--bridge ABS] [--session NAME (auto fallback)] [--timeout 60]",
|
|
44
|
+
" tfx-live ask --transport uds|auto (--short SHORT | --session-id ID) --prompt TEXT [--config-dir DIR] [--bridge ABS] [--session NAME (auto fallback)] [--timeout 60]",
|
|
38
45
|
" transport: auto is the default for Claude when --short/--session-id is present; otherwise tmux. bridge path: --bridge > $TFX_BRIDGE > $TFX_REPO_ROOT/hub/bridge.mjs > bundled Triflux hub/bridge.mjs.",
|
|
39
|
-
" tfx-live interrupt --session NAME [--cli codex|claude] [--transport tmux|uds|auto] [--short SHORT | --session-id ID] [--bridge ABS] [--timeout 5]",
|
|
46
|
+
" tfx-live interrupt --session NAME [--cli codex|claude] [--transport tmux|uds|auto] [--short SHORT | --session-id ID] [--config-dir DIR] [--bridge ABS] [--timeout 5]",
|
|
40
47
|
" tfx-live stop --session NAME [--cli codex|claude] [--remote HOST]",
|
|
41
|
-
" tfx-live probe [--short SHORT] [--session-id ID] [--bridge ABS] [--timeout 10]",
|
|
48
|
+
" tfx-live probe [--short SHORT] [--session-id ID] [--config-dir DIR] [--bridge ABS] [--timeout 10]",
|
|
42
49
|
" tfx-live converse --session NAME --prompts-file PATH [--cli codex|claude] [--remote HOST] [--cwd DIR] [--timeout 60] [--settle 1500]",
|
|
43
50
|
" tfx-live goal-driven --session NAME --goal TEXT [--cli codex|claude] [--remote HOST] [--cwd DIR] [--timeout 60] [--settle 1500] [--max-rounds 8] [--done-token DONE]",
|
|
44
51
|
" tfx-live peer [--cli-a codex] [--cli-b claude] [--session-a peerA] [--session-b peerB] [--transport-a tmux|uds|auto] [--transport-b tmux|uds|auto] [--short-a SHORT] [--short-b SHORT] [--session-id-a ID] [--session-id-b ID] [--bridge ABS] [--remote HOST] [--cwd DIR] [--rounds 4] [--mode counting|freeform] [--seed TEXT] [--timeout 60]",
|
|
@@ -65,6 +72,11 @@ function parseCli(argv) {
|
|
|
65
72
|
throw new Error("Empty flag is not valid");
|
|
66
73
|
}
|
|
67
74
|
|
|
75
|
+
if (BOOLEAN_FLAGS.has(key)) {
|
|
76
|
+
flags[key] = "1";
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
68
80
|
const value = rest[index + 1];
|
|
69
81
|
if (value === undefined) {
|
|
70
82
|
throw new Error(`Missing value for --${key}`);
|
|
@@ -168,6 +180,152 @@ function buildTmuxCommand(remote, tmuxArgs) {
|
|
|
168
180
|
};
|
|
169
181
|
}
|
|
170
182
|
|
|
183
|
+
function timeoutSeconds(timeoutMs) {
|
|
184
|
+
return String(Math.max(1, Math.ceil((timeoutMs ?? 1000) / 1000)));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function buildRemoteLiveArgv(verb, opts) {
|
|
188
|
+
const args = [
|
|
189
|
+
"tfx-live",
|
|
190
|
+
verb,
|
|
191
|
+
"--cli",
|
|
192
|
+
"claude",
|
|
193
|
+
"--transport",
|
|
194
|
+
opts.transport ?? "uds",
|
|
195
|
+
];
|
|
196
|
+
if (opts.short) args.push("--short", opts.short);
|
|
197
|
+
if (opts.sessionId) args.push("--session-id", opts.sessionId);
|
|
198
|
+
if (opts.session) args.push("--session", opts.session);
|
|
199
|
+
if (opts.configDir) args.push("--config-dir", opts.configDir);
|
|
200
|
+
if (verb === "ask") args.push("--prompt", opts.prompt ?? "");
|
|
201
|
+
args.push("--timeout", timeoutSeconds(opts.timeoutMs));
|
|
202
|
+
if (verb === "ask" && opts.settleMs) {
|
|
203
|
+
args.push("--settle", String(opts.settleMs));
|
|
204
|
+
}
|
|
205
|
+
if (verb === "ask" && opts.pollIntervalMs) {
|
|
206
|
+
args.push("--poll-interval", String(opts.pollIntervalMs));
|
|
207
|
+
}
|
|
208
|
+
return args;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function buildRemoteLiveCommand(host, verb, opts, env = {}) {
|
|
212
|
+
validateRemoteHost(host);
|
|
213
|
+
const argv = buildRemoteLiveArgv(verb, opts);
|
|
214
|
+
if (env.os === "win32") {
|
|
215
|
+
const [commandPath, ...args] = argv;
|
|
216
|
+
const command = [
|
|
217
|
+
`& '${escapeRemotePwshSingleQuoted(commandPath)}'`,
|
|
218
|
+
...args.map((arg) => `'${escapeRemotePwshSingleQuoted(arg)}'`),
|
|
219
|
+
].join(" ");
|
|
220
|
+
// Live-unverified for Windows remotes; apply the same SSH single-argument
|
|
221
|
+
// contract proven on darwin so the remote login shell does not re-split it.
|
|
222
|
+
const remoteCmd = `pwsh -NoProfile -Command ${remoteShellQuote(command)}`;
|
|
223
|
+
return {
|
|
224
|
+
command: "ssh",
|
|
225
|
+
args: [host, remoteCmd],
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const shell = env.os === "darwin" && env.shell === "zsh" ? "zsh" : "sh";
|
|
230
|
+
const inner = argv.map(remoteShellQuote).join(" ");
|
|
231
|
+
const remoteCmd = `${shell} -lc ${remoteShellQuote(inner)}`;
|
|
232
|
+
return {
|
|
233
|
+
command: "ssh",
|
|
234
|
+
args: [host, remoteCmd],
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function tryParseJsonObject(text) {
|
|
239
|
+
try {
|
|
240
|
+
return JSON.parse(text);
|
|
241
|
+
} catch {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function parseRemoteLiveJson(stdout) {
|
|
247
|
+
const text = String(stdout).trim();
|
|
248
|
+
if (!text) {
|
|
249
|
+
throw new Error("remote tfx-live returned empty output");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const direct = tryParseJsonObject(text);
|
|
253
|
+
if (direct && typeof direct === "object" && !Array.isArray(direct)) {
|
|
254
|
+
return direct;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
let parsed = null;
|
|
258
|
+
for (let start = 0; start < text.length; start += 1) {
|
|
259
|
+
if (text[start] !== "{") continue;
|
|
260
|
+
let depth = 0;
|
|
261
|
+
let inString = false;
|
|
262
|
+
let escaped = false;
|
|
263
|
+
for (let end = start; end < text.length; end += 1) {
|
|
264
|
+
const char = text[end];
|
|
265
|
+
if (inString) {
|
|
266
|
+
if (escaped) {
|
|
267
|
+
escaped = false;
|
|
268
|
+
} else if (char === "\\") {
|
|
269
|
+
escaped = true;
|
|
270
|
+
} else if (char === '"') {
|
|
271
|
+
inString = false;
|
|
272
|
+
}
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (char === '"') {
|
|
276
|
+
inString = true;
|
|
277
|
+
} else if (char === "{") {
|
|
278
|
+
depth += 1;
|
|
279
|
+
} else if (char === "}") {
|
|
280
|
+
depth -= 1;
|
|
281
|
+
if (depth === 0) {
|
|
282
|
+
const candidate = tryParseJsonObject(text.slice(start, end + 1));
|
|
283
|
+
if (
|
|
284
|
+
candidate &&
|
|
285
|
+
typeof candidate === "object" &&
|
|
286
|
+
!Array.isArray(candidate)
|
|
287
|
+
) {
|
|
288
|
+
parsed = candidate;
|
|
289
|
+
}
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (parsed) return parsed;
|
|
297
|
+
throw new Error(`remote tfx-live output is not JSON: ${text.slice(0, 200)}`);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async function callRemoteLive(verb, opts, deps = {}) {
|
|
301
|
+
const host = validateRemoteHost(opts.remote);
|
|
302
|
+
const probeRemoteEnv = deps.probeRemoteEnv ?? probeRemoteHostEnv;
|
|
303
|
+
const execRemote = deps.sshExec ?? execFileAsync;
|
|
304
|
+
const env = await probeRemoteEnv(host);
|
|
305
|
+
const plan = buildRemoteLiveCommand(host, verb, opts, env);
|
|
306
|
+
const execOptions = {
|
|
307
|
+
timeout: (opts.timeoutMs ?? DEFAULT_ANSWER_TIMEOUT_MS) + 30_000,
|
|
308
|
+
maxBuffer: MAX_BUFFER,
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
const { stdout } = await execRemote(plan.command, plan.args, execOptions);
|
|
313
|
+
return parseRemoteLiveJson(stdout);
|
|
314
|
+
} catch (error) {
|
|
315
|
+
if (error?.stdout) {
|
|
316
|
+
try {
|
|
317
|
+
return parseRemoteLiveJson(error.stdout);
|
|
318
|
+
} catch {
|
|
319
|
+
// stdout was not a tfx-live JSON result; fall through.
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
const stderr = error?.stderr ? String(error.stderr).trim() : "";
|
|
323
|
+
throw new Error(
|
|
324
|
+
`remote tfx-live ${verb} failed: ${stderr || error.message}`,
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
171
329
|
async function runTmux(remote, tmuxArgs, options = {}) {
|
|
172
330
|
const { command, args } = buildTmuxCommand(remote, tmuxArgs);
|
|
173
331
|
try {
|
|
@@ -397,6 +555,144 @@ function extractAssistantResponse(adapter, text, prompt = null) {
|
|
|
397
555
|
return response.join("\n").trim();
|
|
398
556
|
}
|
|
399
557
|
|
|
558
|
+
function normalizeClaudeTaskText(text) {
|
|
559
|
+
return String(text)
|
|
560
|
+
.replace(/[.…]+/g, " ")
|
|
561
|
+
.replace(/\s+/g, " ")
|
|
562
|
+
.trim()
|
|
563
|
+
.toLowerCase();
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function isTaskListBoundary(line) {
|
|
567
|
+
const trimmed = line.trim();
|
|
568
|
+
return (
|
|
569
|
+
!trimmed ||
|
|
570
|
+
trimmed === "Working" ||
|
|
571
|
+
trimmed === "Completed" ||
|
|
572
|
+
/^[─-]{8,}$/.test(trimmed) ||
|
|
573
|
+
hasClaudeComposerPrompt(line) ||
|
|
574
|
+
/enter to open|space to reply|ctrl\+x to delete|\? for shortcuts/i.test(
|
|
575
|
+
trimmed,
|
|
576
|
+
)
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function parseClaudeCompletedTaskListEntries(text) {
|
|
581
|
+
const lines = String(text)
|
|
582
|
+
.split("\n")
|
|
583
|
+
.map((line) => line.replace(/\s+$/g, ""));
|
|
584
|
+
const entries = [];
|
|
585
|
+
let inCompleted = false;
|
|
586
|
+
|
|
587
|
+
for (const line of lines) {
|
|
588
|
+
const trimmed = line.trim();
|
|
589
|
+
if (trimmed === "Completed") {
|
|
590
|
+
inCompleted = true;
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
if (!inCompleted) {
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
if (isTaskListBoundary(line)) {
|
|
597
|
+
if (trimmed) {
|
|
598
|
+
inCompleted = false;
|
|
599
|
+
}
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
if (!/^\s*✻\s+/.test(line)) {
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const body = trimmed.replace(/^✻\s+/, "");
|
|
607
|
+
const columns = body
|
|
608
|
+
.split(/\s{2,}/)
|
|
609
|
+
.map((column) => column.trim())
|
|
610
|
+
.filter(Boolean);
|
|
611
|
+
if (columns.length < 2) {
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
let responseIndex = columns.length - 1;
|
|
616
|
+
const tail = columns[responseIndex];
|
|
617
|
+
if (/^(?:\d+\s*[smhdw]|now|just now)$/i.test(tail)) {
|
|
618
|
+
responseIndex -= 1;
|
|
619
|
+
}
|
|
620
|
+
if (responseIndex <= 0) {
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const summary = columns.slice(0, responseIndex).join(" ").trim();
|
|
625
|
+
const response = columns[responseIndex].trim();
|
|
626
|
+
if (!summary || !response) {
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
entries.push({
|
|
630
|
+
summary,
|
|
631
|
+
response,
|
|
632
|
+
key: `${normalizeClaudeTaskText(summary)}\0${normalizeClaudeTaskText(
|
|
633
|
+
response,
|
|
634
|
+
)}`,
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
return entries;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function claudeTaskMatchesPrompt(entry, prompt) {
|
|
642
|
+
if (!prompt) {
|
|
643
|
+
return true;
|
|
644
|
+
}
|
|
645
|
+
const summary = normalizeClaudeTaskText(entry.summary);
|
|
646
|
+
const normalizedPrompt = normalizeClaudeTaskText(prompt);
|
|
647
|
+
return (
|
|
648
|
+
Boolean(summary && normalizedPrompt) &&
|
|
649
|
+
(summary.includes(normalizedPrompt) || normalizedPrompt.includes(summary))
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function extractClaudeCompletedTaskListResponse(text, options = {}) {
|
|
654
|
+
const beforeEntries = parseClaudeCompletedTaskListEntries(options.beforeText);
|
|
655
|
+
const beforeKeys = new Set(beforeEntries.map((entry) => entry.key));
|
|
656
|
+
const newEntries = parseClaudeCompletedTaskListEntries(text).filter(
|
|
657
|
+
(entry) => !beforeKeys.has(entry.key),
|
|
658
|
+
);
|
|
659
|
+
if (newEntries.length === 0) {
|
|
660
|
+
return "";
|
|
661
|
+
}
|
|
662
|
+
const promptMatches = options.prompt
|
|
663
|
+
? newEntries.filter((entry) =>
|
|
664
|
+
claudeTaskMatchesPrompt(entry, options.prompt),
|
|
665
|
+
)
|
|
666
|
+
: [];
|
|
667
|
+
if (promptMatches.length > 0) {
|
|
668
|
+
return promptMatches[0].response;
|
|
669
|
+
}
|
|
670
|
+
if (newEntries.length === 1) {
|
|
671
|
+
return newEntries[0].response;
|
|
672
|
+
}
|
|
673
|
+
return newEntries.at(-1).response;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function hasClaudeCompletedTaskListResponse(text, options = {}) {
|
|
677
|
+
return extractClaudeCompletedTaskListResponse(text, options).length > 0;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function hasTmuxAssistantResponseAfterPrompt(
|
|
681
|
+
adapter,
|
|
682
|
+
text,
|
|
683
|
+
prompt,
|
|
684
|
+
beforeText,
|
|
685
|
+
) {
|
|
686
|
+
return (
|
|
687
|
+
hasAssistantResponseAfterPrompt(adapter, text, prompt) ||
|
|
688
|
+
(adapter.cli === "claude" &&
|
|
689
|
+
hasClaudeCompletedTaskListResponse(text, {
|
|
690
|
+
beforeText,
|
|
691
|
+
prompt,
|
|
692
|
+
}))
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
|
|
400
696
|
async function capturePane(remote, session) {
|
|
401
697
|
const { stdout } = await runTmux(remote, [
|
|
402
698
|
"capture-pane",
|
|
@@ -787,18 +1083,81 @@ async function probeDaemon(bridgePath, ref, timeoutMs) {
|
|
|
787
1083
|
const payload = {};
|
|
788
1084
|
if (ref?.short) payload.short = ref.short;
|
|
789
1085
|
if (ref?.sessionId) payload.sessionId = ref.sessionId;
|
|
1086
|
+
if (ref?.configDir) payload.configDir = ref.configDir;
|
|
790
1087
|
const result = await callBridgeVerb(
|
|
791
1088
|
bridgePath,
|
|
792
1089
|
"daemon-probe",
|
|
793
1090
|
payload,
|
|
794
1091
|
timeoutMs,
|
|
795
1092
|
);
|
|
796
|
-
return {
|
|
1093
|
+
return {
|
|
1094
|
+
ok: result?.ok === true,
|
|
1095
|
+
reason: result?.reason,
|
|
1096
|
+
sessions: result?.sessions,
|
|
1097
|
+
raw: result,
|
|
1098
|
+
};
|
|
797
1099
|
} catch (error) {
|
|
798
1100
|
return { ok: false, reason: error.message };
|
|
799
1101
|
}
|
|
800
1102
|
}
|
|
801
1103
|
|
|
1104
|
+
async function hasTmuxSession(adapter, opts) {
|
|
1105
|
+
if (!opts.session) return false;
|
|
1106
|
+
try {
|
|
1107
|
+
await runTmux(opts.remote, ["has-session", "-t", opts.session]);
|
|
1108
|
+
return true;
|
|
1109
|
+
} catch {
|
|
1110
|
+
return false;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
function daemonProbeUnavailableReason(probe, targetAttachable) {
|
|
1115
|
+
if (targetAttachable) return null;
|
|
1116
|
+
if (!probe?.ok) return probe?.reason ?? "probe-failed";
|
|
1117
|
+
return "target-not-found";
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
async function resolveAskTransport(adapter, opts, deps = {}) {
|
|
1121
|
+
const transport = opts.transport ?? "tmux";
|
|
1122
|
+
if (transport !== "auto") {
|
|
1123
|
+
return {
|
|
1124
|
+
transport,
|
|
1125
|
+
transportSelected: transport,
|
|
1126
|
+
transportProbe: null,
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
const tmuxProbe = deps.hasTmuxSession ?? hasTmuxSession;
|
|
1131
|
+
const daemonProbe = deps.probeDaemon ?? probeDaemon;
|
|
1132
|
+
const [tmux, probe] = await Promise.all([
|
|
1133
|
+
tmuxProbe(adapter, opts),
|
|
1134
|
+
daemonProbe(
|
|
1135
|
+
opts.bridgePath,
|
|
1136
|
+
{
|
|
1137
|
+
short: opts.short,
|
|
1138
|
+
sessionId: opts.sessionId,
|
|
1139
|
+
configDir: opts.configDir,
|
|
1140
|
+
},
|
|
1141
|
+
opts.timeoutMs,
|
|
1142
|
+
),
|
|
1143
|
+
]);
|
|
1144
|
+
const daemon = daemonProbeTargetAttachable(probe, opts);
|
|
1145
|
+
const daemonReason = daemonProbeUnavailableReason(probe, daemon);
|
|
1146
|
+
const transportProbe = {
|
|
1147
|
+
tmux: tmux === true,
|
|
1148
|
+
daemon,
|
|
1149
|
+
...(daemonReason ? { daemonReason } : {}),
|
|
1150
|
+
};
|
|
1151
|
+
|
|
1152
|
+
return {
|
|
1153
|
+
transport: "auto",
|
|
1154
|
+
transportSelected: daemon ? "uds" : tmux ? "tmux" : "none",
|
|
1155
|
+
transportProbe,
|
|
1156
|
+
daemonProbe: probe,
|
|
1157
|
+
daemonConfigDir: opts.configDir ?? probe?.raw?.daemon?.configDir ?? null,
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
|
|
802
1161
|
async function doStart(adapter, opts) {
|
|
803
1162
|
const {
|
|
804
1163
|
session,
|
|
@@ -874,7 +1233,7 @@ async function doAskViaTmux(adapter, opts) {
|
|
|
874
1233
|
const doneSignal =
|
|
875
1234
|
!adapter.isBusy(visible) &&
|
|
876
1235
|
ready &&
|
|
877
|
-
|
|
1236
|
+
hasTmuxAssistantResponseAfterPrompt(adapter, visible, prompt, beforeRaw);
|
|
878
1237
|
if (doneSignal && quietPolls >= FALLBACK_QUIET_POLLS) {
|
|
879
1238
|
done = true;
|
|
880
1239
|
break;
|
|
@@ -885,6 +1244,15 @@ async function doAskViaTmux(adapter, opts) {
|
|
|
885
1244
|
|
|
886
1245
|
raw = await capturePane(remote, session);
|
|
887
1246
|
response = extractAssistantResponse(adapter, raw, prompt);
|
|
1247
|
+
if (!response && adapter.cli === "claude") {
|
|
1248
|
+
response = extractClaudeCompletedTaskListResponse(raw, {
|
|
1249
|
+
beforeText: beforeRaw,
|
|
1250
|
+
prompt,
|
|
1251
|
+
});
|
|
1252
|
+
if (response) {
|
|
1253
|
+
done = true;
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
888
1256
|
contextPctAfter = adapter.contextPct(raw);
|
|
889
1257
|
|
|
890
1258
|
return addLoginGuard(adapter, raw, {
|
|
@@ -895,6 +1263,7 @@ async function doAskViaTmux(adapter, opts) {
|
|
|
895
1263
|
response,
|
|
896
1264
|
contextPctBefore,
|
|
897
1265
|
contextPctAfter,
|
|
1266
|
+
matchedCompletion: done,
|
|
898
1267
|
done,
|
|
899
1268
|
raw,
|
|
900
1269
|
});
|
|
@@ -918,6 +1287,10 @@ async function doAsk(adapter, opts) {
|
|
|
918
1287
|
);
|
|
919
1288
|
}
|
|
920
1289
|
|
|
1290
|
+
if (opts.remote) {
|
|
1291
|
+
return doAskViaRemoteLive(adapter, opts);
|
|
1292
|
+
}
|
|
1293
|
+
|
|
921
1294
|
if (transport === "uds") {
|
|
922
1295
|
if (!opts.bridgePath) {
|
|
923
1296
|
throw new Error(
|
|
@@ -930,11 +1303,22 @@ async function doAsk(adapter, opts) {
|
|
|
930
1303
|
return doAskAuto(adapter, opts);
|
|
931
1304
|
}
|
|
932
1305
|
|
|
1306
|
+
async function doAskViaRemoteLive(adapter, opts, deps = {}) {
|
|
1307
|
+
const result = await callRemoteLive("ask", opts, deps);
|
|
1308
|
+
return {
|
|
1309
|
+
...result,
|
|
1310
|
+
cli: result.cli ?? adapter.cli,
|
|
1311
|
+
remote: opts.remote,
|
|
1312
|
+
remoteRelay: true,
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
|
|
933
1316
|
async function doAskViaDaemon(opts, meta = {}) {
|
|
934
|
-
const { bridgePath, prompt, timeoutMs, short, sessionId } = opts;
|
|
1317
|
+
const { bridgePath, prompt, timeoutMs, short, sessionId, configDir } = opts;
|
|
935
1318
|
const payload = { prompt, timeoutMs };
|
|
936
1319
|
if (short) payload.short = short;
|
|
937
1320
|
if (sessionId) payload.sessionId = sessionId;
|
|
1321
|
+
if (configDir) payload.configDir = configDir;
|
|
938
1322
|
|
|
939
1323
|
const result = await callBridgeVerb(
|
|
940
1324
|
bridgePath,
|
|
@@ -956,12 +1340,35 @@ async function doAskViaDaemon(opts, meta = {}) {
|
|
|
956
1340
|
timedOut: result?.timedOut === true,
|
|
957
1341
|
closed: result?.closed === true,
|
|
958
1342
|
inputSent: result?.inputSent === true,
|
|
1343
|
+
daemon: result?.daemon ?? null,
|
|
1344
|
+
daemons: result?.daemons ?? [],
|
|
1345
|
+
matches: result?.matches ?? [],
|
|
1346
|
+
candidateResults: result?.candidateResults ?? [],
|
|
1347
|
+
callerProvenance: result?.callerProvenance ?? null,
|
|
959
1348
|
done: matchedCompletion,
|
|
960
1349
|
...(result?.error ? { error: result.error } : {}),
|
|
961
1350
|
...meta,
|
|
962
1351
|
};
|
|
963
1352
|
}
|
|
964
1353
|
|
|
1354
|
+
function daemonProbeTargetAttachable(probe, opts) {
|
|
1355
|
+
if (!probe?.ok) return false;
|
|
1356
|
+
// New bridge responses carry `target`; daemon-control owns selection policy.
|
|
1357
|
+
if (probe.raw?.target) return true;
|
|
1358
|
+
// Compatibility only for older bridge binaries that list sessions but do not
|
|
1359
|
+
// expose `target` yet. Do not add new selection semantics here.
|
|
1360
|
+
if (!Array.isArray(probe.sessions)) return false;
|
|
1361
|
+
return probe.sessions.some(
|
|
1362
|
+
(entry) =>
|
|
1363
|
+
(opts.short && entry?.short === opts.short) ||
|
|
1364
|
+
(opts.sessionId &&
|
|
1365
|
+
(entry?.sessionId === opts.sessionId ||
|
|
1366
|
+
entry?.session_id === opts.sessionId ||
|
|
1367
|
+
entry?.dispatch?.sessionId === opts.sessionId ||
|
|
1368
|
+
entry?.d?.sessionId === opts.sessionId)),
|
|
1369
|
+
);
|
|
1370
|
+
}
|
|
1371
|
+
|
|
965
1372
|
async function ensureTmuxSession(adapter, opts) {
|
|
966
1373
|
if (!opts.session) return false;
|
|
967
1374
|
try {
|
|
@@ -1004,6 +1411,13 @@ function writeUdsBugReport(reason, context) {
|
|
|
1004
1411
|
.map((entry) => entry?.short)
|
|
1005
1412
|
.filter(Boolean)
|
|
1006
1413
|
: [],
|
|
1414
|
+
// Diagnostic-only bridge metadata; callers should not depend on this
|
|
1415
|
+
// shape as a versioned control contract.
|
|
1416
|
+
daemon: context.probe.raw?.daemon ?? null,
|
|
1417
|
+
daemons: context.probe.raw?.daemons ?? [],
|
|
1418
|
+
matches: context.probe.raw?.matches ?? [],
|
|
1419
|
+
candidateResults: context.probe.raw?.candidateResults ?? [],
|
|
1420
|
+
callerProvenance: context.probe.raw?.callerProvenance ?? null,
|
|
1007
1421
|
}
|
|
1008
1422
|
: null,
|
|
1009
1423
|
attachError: context.attachError ?? null,
|
|
@@ -1050,28 +1464,28 @@ async function runTmuxFallback(adapter, opts, reason, udsResult) {
|
|
|
1050
1464
|
}
|
|
1051
1465
|
|
|
1052
1466
|
async function doAskAuto(adapter, opts) {
|
|
1053
|
-
const
|
|
1054
|
-
|
|
1055
|
-
{ short: opts.short, sessionId: opts.sessionId },
|
|
1056
|
-
opts.timeoutMs,
|
|
1057
|
-
);
|
|
1058
|
-
// daemon-probe returns the full session list (it does not filter by the
|
|
1059
|
-
// requested short), so confirm the target is actually attachable here rather
|
|
1060
|
-
// than firing a doomed attach at a missing short.
|
|
1061
|
-
const targetAttachable =
|
|
1062
|
-
probe.ok &&
|
|
1063
|
-
Array.isArray(probe.sessions) &&
|
|
1064
|
-
probe.sessions.some(
|
|
1065
|
-
(entry) =>
|
|
1066
|
-
(opts.short && entry?.short === opts.short) ||
|
|
1067
|
-
(opts.sessionId && entry?.sessionId === opts.sessionId),
|
|
1068
|
-
);
|
|
1467
|
+
const resolution = await resolveAskTransport(adapter, opts);
|
|
1468
|
+
const probe = resolution.daemonProbe;
|
|
1069
1469
|
|
|
1070
|
-
if (
|
|
1071
|
-
const
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1470
|
+
if (resolution.transportSelected === "tmux") {
|
|
1471
|
+
const tmuxResult = await doAskViaTmux(adapter, opts);
|
|
1472
|
+
return {
|
|
1473
|
+
...tmuxResult,
|
|
1474
|
+
transport: "auto",
|
|
1475
|
+
transportSelected: "tmux",
|
|
1476
|
+
transportProbe: resolution.transportProbe,
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
if (resolution.transportSelected === "uds") {
|
|
1481
|
+
const daemonConfigDir = resolution.daemonConfigDir;
|
|
1482
|
+
const udsResult = await doAskViaDaemon(
|
|
1483
|
+
{ ...opts, configDir: daemonConfigDir },
|
|
1484
|
+
{
|
|
1485
|
+
transportSelected: "uds",
|
|
1486
|
+
transportProbe: resolution.transportProbe,
|
|
1487
|
+
},
|
|
1488
|
+
);
|
|
1075
1489
|
if (udsResult.matchedCompletion) {
|
|
1076
1490
|
return udsResult;
|
|
1077
1491
|
}
|
|
@@ -1089,27 +1503,48 @@ async function doAskAuto(adapter, opts) {
|
|
|
1089
1503
|
probe,
|
|
1090
1504
|
attachError,
|
|
1091
1505
|
});
|
|
1092
|
-
const fallback =
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1506
|
+
const fallback =
|
|
1507
|
+
resolution.transportProbe?.tmux === true
|
|
1508
|
+
? await runTmuxFallback(
|
|
1509
|
+
adapter,
|
|
1510
|
+
opts,
|
|
1511
|
+
"uds-attach-incomplete",
|
|
1512
|
+
udsResult,
|
|
1513
|
+
)
|
|
1514
|
+
: {
|
|
1515
|
+
cli: adapter.cli,
|
|
1516
|
+
transport: "auto",
|
|
1517
|
+
transportSelected: "none",
|
|
1518
|
+
transportProbe: resolution.transportProbe,
|
|
1519
|
+
response: "",
|
|
1520
|
+
done: false,
|
|
1521
|
+
error:
|
|
1522
|
+
"uds unavailable (uds-attach-incomplete) and no tmux session for fallback",
|
|
1523
|
+
udsError: udsResult.error ?? attachError,
|
|
1524
|
+
};
|
|
1098
1525
|
await reportPromise.catch(() => {});
|
|
1099
1526
|
return fallback;
|
|
1100
1527
|
}
|
|
1101
1528
|
|
|
1102
1529
|
// uds is unavailable: daemon unreachable, or reachable but target not listed.
|
|
1103
|
-
const reason =
|
|
1104
|
-
? (probe.reason ?? "probe-failed")
|
|
1105
|
-
: "target-not-found";
|
|
1530
|
+
const reason = resolution.transportProbe?.daemonReason ?? "target-not-found";
|
|
1106
1531
|
const reportPromise = writeUdsBugReport(reason, {
|
|
1107
1532
|
short: opts.short,
|
|
1108
1533
|
sessionId: opts.sessionId,
|
|
1109
1534
|
bridgePath: opts.bridgePath,
|
|
1110
1535
|
probe,
|
|
1111
1536
|
});
|
|
1112
|
-
const fallback =
|
|
1537
|
+
const fallback = {
|
|
1538
|
+
cli: adapter.cli,
|
|
1539
|
+
transport: "auto",
|
|
1540
|
+
transportSelected: "none",
|
|
1541
|
+
transportProbe: resolution.transportProbe,
|
|
1542
|
+
response: "",
|
|
1543
|
+
done: false,
|
|
1544
|
+
error: opts.session
|
|
1545
|
+
? `uds unavailable (${reason}) and tmux session unavailable`
|
|
1546
|
+
: `uds unavailable (${reason}) and no --session for tmux fallback`,
|
|
1547
|
+
};
|
|
1113
1548
|
await reportPromise.catch(() => {});
|
|
1114
1549
|
return fallback;
|
|
1115
1550
|
}
|
|
@@ -1139,10 +1574,11 @@ async function doInterruptViaTmux(adapter, opts) {
|
|
|
1139
1574
|
}
|
|
1140
1575
|
|
|
1141
1576
|
async function doInterruptViaDaemon(opts, meta = {}) {
|
|
1142
|
-
const { bridgePath, timeoutMs, short, sessionId } = opts;
|
|
1577
|
+
const { bridgePath, timeoutMs, short, sessionId, configDir } = opts;
|
|
1143
1578
|
const payload = { timeoutMs };
|
|
1144
1579
|
if (short) payload.short = short;
|
|
1145
1580
|
if (sessionId) payload.sessionId = sessionId;
|
|
1581
|
+
if (configDir) payload.configDir = configDir;
|
|
1146
1582
|
const result = await callBridgeVerb(
|
|
1147
1583
|
bridgePath,
|
|
1148
1584
|
"daemon-interrupt",
|
|
@@ -1161,11 +1597,26 @@ async function doInterruptViaDaemon(opts, meta = {}) {
|
|
|
1161
1597
|
inputSent: result?.inputSent === true,
|
|
1162
1598
|
timedOut: result?.timedOut === true,
|
|
1163
1599
|
closed: result?.closed === true,
|
|
1600
|
+
daemon: result?.daemon ?? null,
|
|
1601
|
+
daemons: result?.daemons ?? [],
|
|
1602
|
+
matches: result?.matches ?? [],
|
|
1603
|
+
candidateResults: result?.candidateResults ?? [],
|
|
1604
|
+
callerProvenance: result?.callerProvenance ?? null,
|
|
1164
1605
|
...(result?.error ? { error: result.error } : {}),
|
|
1165
1606
|
...meta,
|
|
1166
1607
|
};
|
|
1167
1608
|
}
|
|
1168
1609
|
|
|
1610
|
+
async function doInterruptViaRemoteLive(adapter, opts, deps = {}) {
|
|
1611
|
+
const result = await callRemoteLive("interrupt", opts, deps);
|
|
1612
|
+
return {
|
|
1613
|
+
...result,
|
|
1614
|
+
cli: result.cli ?? adapter.cli,
|
|
1615
|
+
remote: opts.remote,
|
|
1616
|
+
remoteRelay: true,
|
|
1617
|
+
};
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1169
1620
|
async function doInterrupt(adapter, opts) {
|
|
1170
1621
|
const transport = opts.transport ?? "tmux";
|
|
1171
1622
|
if (transport === "tmux") {
|
|
@@ -1181,6 +1632,9 @@ async function doInterrupt(adapter, opts) {
|
|
|
1181
1632
|
`--transport ${transport} requires --short or --session-id`,
|
|
1182
1633
|
);
|
|
1183
1634
|
}
|
|
1635
|
+
if (opts.remote) {
|
|
1636
|
+
return doInterruptViaRemoteLive(adapter, opts);
|
|
1637
|
+
}
|
|
1184
1638
|
if (!opts.bridgePath) {
|
|
1185
1639
|
throw new Error(
|
|
1186
1640
|
`--transport ${transport} requires a bridge path (--bridge, TFX_BRIDGE, or TFX_REPO_ROOT)`,
|
|
@@ -1269,6 +1723,7 @@ function askOpts(flags, adapter) {
|
|
|
1269
1723
|
transport === "tmux" ? requireFlag(flags, "session") : flags.session,
|
|
1270
1724
|
short,
|
|
1271
1725
|
sessionId,
|
|
1726
|
+
configDir: flags["config-dir"],
|
|
1272
1727
|
transport,
|
|
1273
1728
|
bridgePath: resolveBridgePath(flags),
|
|
1274
1729
|
prompt: requireFlag(flags, "prompt"),
|
|
@@ -1295,6 +1750,7 @@ function interruptOpts(flags, adapter) {
|
|
|
1295
1750
|
transport === "tmux" ? requireFlag(flags, "session") : flags.session,
|
|
1296
1751
|
short,
|
|
1297
1752
|
sessionId,
|
|
1753
|
+
configDir: flags["config-dir"],
|
|
1298
1754
|
transport,
|
|
1299
1755
|
bridgePath: resolveBridgePath(flags),
|
|
1300
1756
|
remote: flags.remote,
|
|
@@ -1326,6 +1782,7 @@ async function probe(flags) {
|
|
|
1326
1782
|
const payload = {};
|
|
1327
1783
|
if (flags.short) payload.short = flags.short;
|
|
1328
1784
|
if (flags["session-id"]) payload.sessionId = flags["session-id"];
|
|
1785
|
+
if (flags["config-dir"]) payload.configDir = flags["config-dir"];
|
|
1329
1786
|
const timeoutMs = secondsFlag(flags, "timeout", 10_000);
|
|
1330
1787
|
printJson(
|
|
1331
1788
|
await callBridgeVerb(
|
|
@@ -1768,10 +2225,27 @@ async function main() {
|
|
|
1768
2225
|
}
|
|
1769
2226
|
}
|
|
1770
2227
|
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
2228
|
+
export {
|
|
2229
|
+
ADAPTERS,
|
|
2230
|
+
buildRemoteLiveCommand,
|
|
2231
|
+
callRemoteLive,
|
|
2232
|
+
extractAssistantResponse,
|
|
2233
|
+
extractClaudeCompletedTaskListResponse,
|
|
2234
|
+
hasClaudeCompletedTaskListResponse,
|
|
2235
|
+
parseRemoteLiveJson,
|
|
2236
|
+
resolveAskTransport,
|
|
2237
|
+
};
|
|
2238
|
+
|
|
2239
|
+
const isDirectRun =
|
|
2240
|
+
process.argv[1] &&
|
|
2241
|
+
fileURLToPath(import.meta.url) === pathResolve(process.argv[1]);
|
|
2242
|
+
|
|
2243
|
+
if (isDirectRun) {
|
|
2244
|
+
main().catch((error) => {
|
|
2245
|
+
printJson({
|
|
2246
|
+
ok: false,
|
|
2247
|
+
error: error.message,
|
|
2248
|
+
});
|
|
2249
|
+
process.exitCode = 1;
|
|
1775
2250
|
});
|
|
1776
|
-
|
|
1777
|
-
});
|
|
2251
|
+
}
|