@triflux/remote 10.50.2 → 10.52.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/cto/brief.mjs +0 -20
- package/cto/collect.mjs +1 -200
- package/cto/current.schema.json +2 -23
- package/cto/hygiene.mjs +2 -61
- package/cto/lake-root.mjs +0 -56
- package/cto/status.mjs +5 -187
- package/hub/index.mjs +0 -17
- package/hub/team/agent-map.json +0 -1
- package/hub/team/agents-row-attach.py +120 -0
- package/hub/team/agents-row.mjs +200 -0
- package/hub/team/ansi.mjs +1 -35
- package/hub/team/backend.mjs +9 -145
- package/hub/team/claude-daemon-control.mjs +1 -2
- package/hub/team/claude-session-projection.mjs +5 -73
- package/hub/team/cli/commands/attach.mjs +1 -17
- package/hub/team/cli/commands/control.mjs +12 -38
- package/hub/team/cli/commands/debug.mjs +2 -39
- package/hub/team/cli/commands/focus.mjs +5 -31
- package/hub/team/cli/commands/interrupt.mjs +6 -25
- package/hub/team/cli/commands/kill.mjs +1 -30
- package/hub/team/cli/commands/list.mjs +0 -16
- package/hub/team/cli/commands/send.mjs +6 -26
- package/hub/team/cli/commands/start/index.mjs +42 -133
- package/hub/team/cli/commands/start/parse-args.mjs +15 -40
- package/hub/team/cli/commands/start/start-headless.mjs +138 -105
- package/hub/team/cli/commands/start/start-mux.mjs +2 -7
- package/hub/team/cli/commands/status.mjs +2 -57
- package/hub/team/cli/commands/stop.mjs +14 -19
- package/hub/team/cli/help.mjs +4 -5
- package/hub/team/cli/manifest.mjs +5 -1
- package/hub/team/cli/render.mjs +0 -12
- package/hub/team/cli/services/runtime-mode.mjs +27 -98
- package/hub/team/cli/services/state-store.mjs +5 -20
- package/hub/team/handoff.mjs +3 -34
- package/hub/team/headless.mjs +108 -1296
- package/hub/team/orchestrator.mjs +4 -30
- package/hub/team/pane.mjs +6 -28
- package/hub/team/psmux.mjs +3 -34
- package/hub/team/remote-session.mjs +1 -2
- package/hub/team/session-context.mjs +1 -1
- package/hub/team/session.mjs +7 -91
- package/hub/team/tui-core.mjs +1 -1
- package/hub/team/tui-viewer.mjs +11 -58
- package/hub/team/tui.mjs +8 -306
- package/hub/team/uds-orchestrator.mjs +2 -580
- package/hub/team/wt-manager.mjs +2 -2
- package/hub/workers/factory.mjs +6 -84
- package/hub/workers/interface.mjs +1 -7
- package/hub/workers/lib/jsonrpc-core.mjs +11 -0
- package/hub/workers/lib/jsonrpc-ws-uds.mjs +5 -8
- package/package.json +3 -8
- package/scripts/lib/agent-route-policy.mjs +143 -62
- package/scripts/lib/cli-claude.mjs +0 -14
- package/scripts/lib/env-probe.mjs +2 -131
- package/scripts/lib/gemini-profiles.mjs +2 -69
- package/scripts/lib/legacy-mcp-cleanup.mjs +283 -0
- package/scripts/lib/machine-profile.mjs +3 -17
- package/scripts/lib/mcp-filter.mjs +109 -53
- package/scripts/lib/mcp-guard-engine.mjs +40 -98
- package/scripts/lib/mcp-health.mjs +2 -2
- package/scripts/lib/mcp-server-catalog.mjs +2 -2
- package/scripts/lib/psmux-info.mjs +11 -59
- package/scripts/lib/skill-template.mjs +0 -245
- package/hub/hub-lifecycle.mjs +0 -248
- package/hub/pipe.mjs +0 -825
- package/hub/promote-penalties.mjs +0 -138
- package/hub/public/dashboard.html +0 -416
- package/hub/schema.sql +0 -209
- package/hub/server.mjs +0 -3048
- package/hub/store-adapter.mjs +0 -182
- package/hub/store.mjs +0 -1736
- package/hub/team/build-worker-prompt.mjs +0 -92
- package/hub/team/check-mcp-hub.mjs +0 -49
- package/hub/team/claude-native-bridge.mjs +0 -987
- package/hub/team/cli/commands/start/start-in-process.mjs +0 -50
- package/hub/team/cli/commands/start/start-wt.mjs +0 -86
- package/hub/team/cli/services/hub-client.mjs +0 -264
- package/hub/team/cli/services/native-control.mjs +0 -145
- package/hub/team/codex-review.mjs +0 -520
- package/hub/team/conductor-mesh-bridge.mjs +0 -122
- package/hub/team/conductor-registry.mjs +0 -121
- package/hub/team/conductor.mjs +0 -1529
- package/hub/team/consensus-meta.mjs +0 -176
- package/hub/team/cto-auto-collect.mjs +0 -115
- package/hub/team/cto-pull.mjs +0 -31
- package/hub/team/daemon-pty-tmux-bridge.mjs +0 -146
- package/hub/team/dashboard.mjs +0 -322
- package/hub/team/event-log.mjs +0 -84
- package/hub/team/execution-mode.mjs +0 -324
- package/hub/team/extract-completion-payload.mjs +0 -110
- package/hub/team/headless-bridge-session.mjs +0 -17
- package/hub/team/health-probe.mjs +0 -435
- package/hub/team/interactive-native-launcher.mjs +0 -119
- package/hub/team/interactive-native-worker.mjs +0 -80
- package/hub/team/interactive-tui-transport.mjs +0 -479
- package/hub/team/launcher-template.mjs +0 -144
- package/hub/team/lead-control.mjs +0 -191
- package/hub/team/mcp-selector.mjs +0 -145
- package/hub/team/native-supervisor.mjs +0 -460
- package/hub/team/native.mjs +0 -727
- package/hub/team/recovery-store.mjs +0 -149
- package/hub/team/remote-probe.mjs +0 -292
- package/hub/team/remote-watcher.mjs +0 -506
- package/hub/team/runtime-strategy.mjs +0 -200
- package/hub/team/schemas/worker-completion.json +0 -39
- package/hub/team/sentinel-capture.mjs +0 -127
- package/hub/team/session-sync.mjs +0 -316
- package/hub/team/swarm-cli.mjs +0 -390
- package/hub/team/swarm-hypervisor.mjs +0 -2206
- package/hub/team/swarm-intent.mjs +0 -216
- package/hub/team/swarm-locks.mjs +0 -335
- package/hub/team/swarm-planner.mjs +0 -371
- package/hub/team/swarm-preflight.mjs +0 -366
- package/hub/team/swarm-reconciler.mjs +0 -242
- package/hub/team/synapse-cli.mjs +0 -458
- package/hub/team/synapse-http.mjs +0 -311
- package/hub/team/synapse-registry.mjs +0 -618
- package/hub/team/tui-remote-adapter.mjs +0 -377
- package/hub/team/tui-synapse.mjs +0 -223
- package/hub/team/worker-completion-validator.mjs +0 -79
- package/hub/team/worker-sandbox.mjs +0 -125
- package/hub/team/worktree-lifecycle.mjs +0 -639
- package/hub/team/wt-templates.json +0 -43
- package/hub/tools.mjs +0 -900
- package/hub/workers/antigravity-route-worker.mjs +0 -179
- package/hub/workers/codex-app-server-worker.mjs +0 -1067
- package/hub/workers/delegator-mcp.mjs +0 -1098
- package/hub/workers/lib/jsonrpc-stdio.mjs +0 -204
- package/scripts/lib/claudemd-scanner.mjs +0 -223
- package/scripts/lib/context.mjs +0 -67
- package/scripts/lib/dynamic-route-cli.mjs +0 -107
- package/scripts/lib/handoff.mjs +0 -187
- package/scripts/lib/logger.mjs +0 -122
- package/scripts/lib/stealth-fetch.mjs +0 -176
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
// hub/workers/lib/jsonrpc-stdio.mjs
|
|
2
|
-
// Minimal line-delimited JSON-RPC 2.0 client over stdio.
|
|
3
|
-
// Replaces vscode-jsonrpc for the codex app-server transport.
|
|
4
|
-
//
|
|
5
|
-
// Wire format (Issue #95 P1 #1): OpenAI App Server JSONL variant omits the
|
|
6
|
-
// top-level `"jsonrpc": "2.0"` header on outbound frames. Inbound decode is
|
|
7
|
-
// lenient — frames with or without the header are accepted for forward compat.
|
|
8
|
-
//
|
|
9
|
-
// Contract:
|
|
10
|
-
// new JsonRpcStdioClient({ stdin, stdout, onError, maxLineSize })
|
|
11
|
-
// request(method, params, timeoutMs=60000) -> Promise<result>
|
|
12
|
-
// notify(method, params) -> void
|
|
13
|
-
// onNotification(method, cb) -> unsubscribe() ('*' = catch-all)
|
|
14
|
-
// close(reason) -> void (idempotent; optional reason marks closing state)
|
|
15
|
-
// isOpen() -> boolean
|
|
16
|
-
//
|
|
17
|
-
// Lifecycle (Issue #95 P1 #3):
|
|
18
|
-
// State machine: running | closing | closed
|
|
19
|
-
// - `running` is the default. Parse/EOF/max-line errors reject in-flight
|
|
20
|
-
// requests and transition to `closed`.
|
|
21
|
-
// - `closing` is entered via `close("closing")` before a graceful shutdown;
|
|
22
|
-
// EOF in `closing` is a normal termination and does NOT fail-fast.
|
|
23
|
-
//
|
|
24
|
-
// AC18: any single line whose raw-byte length would exceed maxLineSize is
|
|
25
|
-
// rejected at the stream layer (before readline emits it) to defend against
|
|
26
|
-
// OOM/DoS from an unresponsive or malicious peer.
|
|
27
|
-
|
|
28
|
-
import { createInterface } from "node:readline";
|
|
29
|
-
|
|
30
|
-
import { JsonRpcDispatchBase } from "./jsonrpc-core.mjs";
|
|
31
|
-
|
|
32
|
-
export { JsonRpcProtocolError } from "./jsonrpc-core.mjs";
|
|
33
|
-
|
|
34
|
-
const DEFAULT_MAX_LINE_SIZE = 1024 * 1024; // 1 MiB
|
|
35
|
-
const CLOSED_MESSAGE = "JsonRpcStdioClient closed";
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Thrown (via onError) when an inbound line would exceed maxLineSize.
|
|
39
|
-
*/
|
|
40
|
-
export class MaxLineSizeExceededError extends Error {
|
|
41
|
-
/**
|
|
42
|
-
* @param {number} size Bytes observed so far in the offending line.
|
|
43
|
-
* @param {number} max Configured max.
|
|
44
|
-
*/
|
|
45
|
-
constructor(size, max) {
|
|
46
|
-
super(`JSON-RPC line exceeded max size: ${size} > ${max}`);
|
|
47
|
-
this.name = "MaxLineSizeExceededError";
|
|
48
|
-
this.size = size;
|
|
49
|
-
this.max = max;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Thrown when the underlying stream closes unexpectedly (EOF outside `closing`).
|
|
55
|
-
*/
|
|
56
|
-
export class JsonRpcTransportError extends Error {
|
|
57
|
-
/** @param {string} message @param {{ cause?: unknown }} [options] */
|
|
58
|
-
constructor(message, options = {}) {
|
|
59
|
-
super(message, { cause: options.cause });
|
|
60
|
-
this.name = "JsonRpcTransportError";
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Line-delimited JSON-RPC 2.0 client over a pair of Node streams.
|
|
66
|
-
*/
|
|
67
|
-
export class JsonRpcStdioClient extends JsonRpcDispatchBase {
|
|
68
|
-
/**
|
|
69
|
-
* @param {object} options
|
|
70
|
-
* @param {NodeJS.ReadableStream} options.stdin Server -> client bytes.
|
|
71
|
-
* @param {NodeJS.WritableStream} options.stdout Client -> server bytes.
|
|
72
|
-
* @param {(err: Error) => void} [options.onError] Protocol error sink.
|
|
73
|
-
* @param {number} [options.maxLineSize] Max bytes per inbound line.
|
|
74
|
-
*/
|
|
75
|
-
constructor({ stdin, stdout, onError, maxLineSize = DEFAULT_MAX_LINE_SIZE }) {
|
|
76
|
-
if (!stdin || typeof stdin.on !== "function") {
|
|
77
|
-
throw new TypeError("JsonRpcStdioClient requires a readable stdin");
|
|
78
|
-
}
|
|
79
|
-
if (!stdout || typeof stdout.write !== "function") {
|
|
80
|
-
throw new TypeError("JsonRpcStdioClient requires a writable stdout");
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
super({ onError, closedMessage: CLOSED_MESSAGE });
|
|
84
|
-
|
|
85
|
-
this._stdin = stdin;
|
|
86
|
-
this._stdout = stdout;
|
|
87
|
-
this._maxLineSize =
|
|
88
|
-
Number.isFinite(maxLineSize) && maxLineSize > 0
|
|
89
|
-
? maxLineSize
|
|
90
|
-
: DEFAULT_MAX_LINE_SIZE;
|
|
91
|
-
|
|
92
|
-
/** @type {'running'|'closing'|'closed'} */
|
|
93
|
-
this._state = "running";
|
|
94
|
-
|
|
95
|
-
// AC18: track bytes since last newline at the raw stream layer so an
|
|
96
|
-
// oversized line is rejected *before* readline concatenates it internally.
|
|
97
|
-
this._pendingLineSize = 0;
|
|
98
|
-
this._oversized = false;
|
|
99
|
-
|
|
100
|
-
this._onStdinData = (chunk) => this._trackRawBytes(chunk);
|
|
101
|
-
this._stdin.on("data", this._onStdinData);
|
|
102
|
-
|
|
103
|
-
// Without these handlers, an EPIPE/ERR_STREAM_DESTROYED on either pipe
|
|
104
|
-
// would bubble up as an unhandled 'error' and take down the hub process.
|
|
105
|
-
this._onStdinError = (err) => this._handleStreamError("stdin", err);
|
|
106
|
-
this._onStdoutError = (err) => this._handleStreamError("stdout", err);
|
|
107
|
-
if (typeof this._stdin.on === "function") {
|
|
108
|
-
this._stdin.on("error", this._onStdinError);
|
|
109
|
-
}
|
|
110
|
-
if (typeof this._stdout.on === "function") {
|
|
111
|
-
this._stdout.on("error", this._onStdoutError);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
this._rl = createInterface({ input: this._stdin, crlfDelay: Infinity });
|
|
115
|
-
this._rl.on("line", (line) => this._handleInboundMessage(line));
|
|
116
|
-
// readline re-emits the input stream 'error' on itself; the raw stdin
|
|
117
|
-
// handler above already converts it into a JsonRpcTransportError, so
|
|
118
|
-
// suppress the re-emit to avoid an unhandled 'error' on the Interface.
|
|
119
|
-
this._rl.on("error", () => {});
|
|
120
|
-
this._rl.on("close", () => {
|
|
121
|
-
// P1 #3 fail-fast: EOF during `running` is a transport error. Pending
|
|
122
|
-
// requests are rejected with JsonRpcTransportError. EOF during `closing`
|
|
123
|
-
// is a normal shutdown — pending requests are rejected with the generic
|
|
124
|
-
// CLOSED_MESSAGE via close().
|
|
125
|
-
if (this._state === "running") {
|
|
126
|
-
const err = new JsonRpcTransportError(
|
|
127
|
-
"JSON-RPC stream closed unexpectedly (EOF during running state)",
|
|
128
|
-
);
|
|
129
|
-
this._emitError(err);
|
|
130
|
-
this._closeWith("closed", err);
|
|
131
|
-
} else if (this._state !== "closed") {
|
|
132
|
-
this._closeWith("closed");
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// --- internals ---------------------------------------------------------
|
|
138
|
-
|
|
139
|
-
_encodeAndSend(frame) {
|
|
140
|
-
if (this._state === "closed") return;
|
|
141
|
-
const line = `${JSON.stringify(frame)}\n`;
|
|
142
|
-
try {
|
|
143
|
-
this._stdout.write(line, (err) => {
|
|
144
|
-
if (err) this._handleStreamError("stdout-write", err);
|
|
145
|
-
});
|
|
146
|
-
} catch (err) {
|
|
147
|
-
this._handleStreamError("stdout-write", err);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
_teardownTransport() {
|
|
152
|
-
try {
|
|
153
|
-
this._stdin.off?.("data", this._onStdinData);
|
|
154
|
-
} catch {
|
|
155
|
-
/* ignore */
|
|
156
|
-
}
|
|
157
|
-
try {
|
|
158
|
-
this._rl.close();
|
|
159
|
-
} catch {
|
|
160
|
-
/* ignore */
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Convert a raw stream error into a JsonRpcTransportError, emit it to the
|
|
166
|
-
* error sink, and close the client so pending requests are rejected.
|
|
167
|
-
* Idempotent: repeated errors after close are swallowed.
|
|
168
|
-
* @param {string} which identifier for the originating pipe/operation
|
|
169
|
-
* @param {unknown} err raw error from the stream
|
|
170
|
-
*/
|
|
171
|
-
_handleStreamError(which, err) {
|
|
172
|
-
if (this._state === "closed") return;
|
|
173
|
-
const base = err instanceof Error ? err : new Error(String(err));
|
|
174
|
-
const wrapped = new JsonRpcTransportError(
|
|
175
|
-
`JSON-RPC stream error on ${which}: ${base.message}`,
|
|
176
|
-
);
|
|
177
|
-
wrapped.cause = base;
|
|
178
|
-
this._emitError(wrapped);
|
|
179
|
-
this._closeWith("closed", wrapped);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
_trackRawBytes(chunk) {
|
|
183
|
-
if (this._oversized || this._state === "closed") return;
|
|
184
|
-
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
185
|
-
for (let i = 0; i < buf.length; i++) {
|
|
186
|
-
if (buf[i] === 0x0a /* \n */) {
|
|
187
|
-
this._pendingLineSize = 0;
|
|
188
|
-
continue;
|
|
189
|
-
}
|
|
190
|
-
this._pendingLineSize += 1;
|
|
191
|
-
if (this._pendingLineSize > this._maxLineSize) {
|
|
192
|
-
this._oversized = true;
|
|
193
|
-
const err = new MaxLineSizeExceededError(
|
|
194
|
-
this._pendingLineSize,
|
|
195
|
-
this._maxLineSize,
|
|
196
|
-
);
|
|
197
|
-
this._emitError(err);
|
|
198
|
-
// P1 #3 fail-fast: oversized line → reject pending with the actual error
|
|
199
|
-
this._closeWith("closed", err);
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { dirname, join, resolve } from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
|
|
6
|
-
export const TFX_START = "<!-- TFX:START -->";
|
|
7
|
-
export const TFX_END = "<!-- TFX:END -->";
|
|
8
|
-
export const OMC_END = "<!-- OMC:END -->";
|
|
9
|
-
const TFX_VERSION_RE = /<!-- TFX:VERSION:([\d.]+) -->/u;
|
|
10
|
-
|
|
11
|
-
const LEGACY_PATTERNS = [
|
|
12
|
-
/<user_cli_routing>/u,
|
|
13
|
-
/Codex Pro 무료 기간/u,
|
|
14
|
-
/codex exec --dangerously-bypass.*skip-git-repo-check/u,
|
|
15
|
-
/OMC 에이전트 → CLI 매핑/u,
|
|
16
|
-
/Spark 가드레일/u,
|
|
17
|
-
];
|
|
18
|
-
|
|
19
|
-
const DEFAULT_TFX_TEMPLATE = [
|
|
20
|
-
"### triflux CLI routing (managed)",
|
|
21
|
-
"- 이 블록은 triflux setup에서 자동으로 관리됩니다.",
|
|
22
|
-
"- 직접 수정이 필요하면 블록 바깥에 사용자 섹션을 추가하세요.",
|
|
23
|
-
].join("\n");
|
|
24
|
-
|
|
25
|
-
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
|
|
26
|
-
const DEFAULT_TEMPLATE_PATH = join(
|
|
27
|
-
SCRIPT_DIR,
|
|
28
|
-
"..",
|
|
29
|
-
"templates",
|
|
30
|
-
"claudemd-tfx-section.md",
|
|
31
|
-
);
|
|
32
|
-
|
|
33
|
-
function resolveVersion(version) {
|
|
34
|
-
if (version) return version;
|
|
35
|
-
try {
|
|
36
|
-
const pkgPath = join(SCRIPT_DIR, "..", "..", "package.json");
|
|
37
|
-
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
38
|
-
return pkg.version || "0.0.0";
|
|
39
|
-
} catch {
|
|
40
|
-
return "0.0.0";
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function resolveTemplate(template, templatePath = DEFAULT_TEMPLATE_PATH) {
|
|
45
|
-
if (typeof template === "string" && template.trim()) {
|
|
46
|
-
return template.trim();
|
|
47
|
-
}
|
|
48
|
-
if (existsSync(templatePath)) {
|
|
49
|
-
const raw = readFileSync(templatePath, "utf8").trim();
|
|
50
|
-
if (raw) return raw;
|
|
51
|
-
}
|
|
52
|
-
return DEFAULT_TFX_TEMPLATE;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function findManagedSection(rawText) {
|
|
56
|
-
const raw = String(rawText || "");
|
|
57
|
-
const startIdx = raw.indexOf(TFX_START);
|
|
58
|
-
const endMarkerIdx = raw.indexOf(TFX_END);
|
|
59
|
-
if (startIdx === -1 || endMarkerIdx === -1 || endMarkerIdx <= startIdx) {
|
|
60
|
-
return {
|
|
61
|
-
found: false,
|
|
62
|
-
content: null,
|
|
63
|
-
version: null,
|
|
64
|
-
startIdx: -1,
|
|
65
|
-
endIdx: -1,
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const endIdx = endMarkerIdx + TFX_END.length;
|
|
70
|
-
const content = raw.slice(startIdx, endIdx);
|
|
71
|
-
const versionMatch = content.match(TFX_VERSION_RE);
|
|
72
|
-
|
|
73
|
-
return {
|
|
74
|
-
found: true,
|
|
75
|
-
content,
|
|
76
|
-
version: versionMatch ? versionMatch[1] : null,
|
|
77
|
-
startIdx,
|
|
78
|
-
endIdx,
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function detectLegacyRange(rawText) {
|
|
83
|
-
const raw = String(rawText || "");
|
|
84
|
-
const matches = LEGACY_PATTERNS.some((pattern) => pattern.test(raw));
|
|
85
|
-
if (!matches) {
|
|
86
|
-
return { found: false, startIdx: -1, endIdx: -1 };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const startTag = raw.indexOf("<user_cli_routing>");
|
|
90
|
-
const endTag = raw.indexOf("</user_cli_routing>");
|
|
91
|
-
if (startTag === -1 || endTag === -1 || endTag <= startTag) {
|
|
92
|
-
return { found: true, startIdx: -1, endIdx: -1 };
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
let removeStart = startTag;
|
|
96
|
-
const userOverridesComment = "<!-- USER OVERRIDES";
|
|
97
|
-
const commentIdx = raw.lastIndexOf(userOverridesComment, startTag);
|
|
98
|
-
if (commentIdx !== -1 && startTag - commentIdx < 200) {
|
|
99
|
-
removeStart = commentIdx;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return {
|
|
103
|
-
found: true,
|
|
104
|
-
startIdx: removeStart,
|
|
105
|
-
endIdx: endTag + "</user_cli_routing>".length,
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function normalizeSpacing(text) {
|
|
110
|
-
return String(text || "").replace(/\n{3,}/gu, "\n\n");
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export function findAllClaudeMdPaths(options = {}) {
|
|
114
|
-
const cwd = options.cwd ? resolve(options.cwd) : process.cwd();
|
|
115
|
-
const homeDir = options.homeDir ? resolve(options.homeDir) : homedir();
|
|
116
|
-
const includeGlobal = options.includeGlobal !== false;
|
|
117
|
-
const includeProject = options.includeProject !== false;
|
|
118
|
-
|
|
119
|
-
const candidates = [];
|
|
120
|
-
if (includeGlobal) candidates.push(join(homeDir, ".claude", "CLAUDE.md"));
|
|
121
|
-
if (includeProject) candidates.push(join(cwd, "CLAUDE.md"));
|
|
122
|
-
|
|
123
|
-
const seen = new Set();
|
|
124
|
-
const paths = [];
|
|
125
|
-
for (const candidate of candidates) {
|
|
126
|
-
const normalized = resolve(candidate);
|
|
127
|
-
if (seen.has(normalized)) continue;
|
|
128
|
-
seen.add(normalized);
|
|
129
|
-
if (existsSync(normalized)) {
|
|
130
|
-
paths.push(normalized);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
return paths;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export function writeSection(filePath, options = {}) {
|
|
137
|
-
const absolutePath = resolve(filePath);
|
|
138
|
-
const version = resolveVersion(options.version);
|
|
139
|
-
const template = resolveTemplate(options.template, options.templatePath);
|
|
140
|
-
const block = [
|
|
141
|
-
TFX_START,
|
|
142
|
-
`<!-- TFX:VERSION:${version} -->`,
|
|
143
|
-
template,
|
|
144
|
-
TFX_END,
|
|
145
|
-
].join("\n");
|
|
146
|
-
|
|
147
|
-
if (!existsSync(absolutePath)) {
|
|
148
|
-
writeFileSync(absolutePath, `${block}\n`, "utf8");
|
|
149
|
-
return { action: "created", version, path: absolutePath };
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const raw = readFileSync(absolutePath, "utf8");
|
|
153
|
-
const existing = findManagedSection(raw);
|
|
154
|
-
|
|
155
|
-
if (existing.found) {
|
|
156
|
-
const nextContent = `${raw.slice(0, existing.startIdx)}${block}${raw.slice(existing.endIdx)}`;
|
|
157
|
-
writeFileSync(absolutePath, nextContent, "utf8");
|
|
158
|
-
return {
|
|
159
|
-
action: "updated",
|
|
160
|
-
oldVersion: existing.version,
|
|
161
|
-
version,
|
|
162
|
-
path: absolutePath,
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const omcEndIdx = raw.indexOf(OMC_END);
|
|
167
|
-
if (omcEndIdx !== -1) {
|
|
168
|
-
const insertAt = omcEndIdx + OMC_END.length;
|
|
169
|
-
const before = raw.slice(0, insertAt);
|
|
170
|
-
const after = raw.slice(insertAt);
|
|
171
|
-
const nextContent = `${before}\n${block}${after}`;
|
|
172
|
-
writeFileSync(absolutePath, nextContent, "utf8");
|
|
173
|
-
return { action: "inserted_after_omc", version, path: absolutePath };
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const separator = raw.endsWith("\n") ? "" : "\n";
|
|
177
|
-
writeFileSync(absolutePath, `${raw}${separator}${block}\n`, "utf8");
|
|
178
|
-
return { action: "appended", version, path: absolutePath };
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
export function migrateClaudeMd(filePath, options = {}) {
|
|
182
|
-
const absolutePath = resolve(filePath);
|
|
183
|
-
if (!existsSync(absolutePath)) {
|
|
184
|
-
return { action: "no_file", removed: [], path: absolutePath };
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const raw = readFileSync(absolutePath, "utf8");
|
|
188
|
-
const existing = findManagedSection(raw);
|
|
189
|
-
const legacy = detectLegacyRange(raw);
|
|
190
|
-
|
|
191
|
-
if (existing.found && !legacy.found) {
|
|
192
|
-
return {
|
|
193
|
-
action: "already_managed",
|
|
194
|
-
removed: [],
|
|
195
|
-
version: existing.version,
|
|
196
|
-
path: absolutePath,
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const removed = [];
|
|
201
|
-
let nextContent = raw;
|
|
202
|
-
|
|
203
|
-
if (legacy.found && legacy.startIdx !== -1 && legacy.endIdx !== -1) {
|
|
204
|
-
nextContent = `${nextContent.slice(0, legacy.startIdx)}${nextContent.slice(legacy.endIdx)}`;
|
|
205
|
-
removed.push("<user_cli_routing> block");
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
const existingAfterLegacy = findManagedSection(nextContent);
|
|
209
|
-
if (existingAfterLegacy.found) {
|
|
210
|
-
nextContent = `${nextContent.slice(0, existingAfterLegacy.startIdx)}${nextContent.slice(existingAfterLegacy.endIdx)}`;
|
|
211
|
-
removed.push("old TFX block");
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
writeFileSync(absolutePath, normalizeSpacing(nextContent), "utf8");
|
|
215
|
-
const writeResult = writeSection(absolutePath, options);
|
|
216
|
-
|
|
217
|
-
return {
|
|
218
|
-
action: "migrated",
|
|
219
|
-
removed,
|
|
220
|
-
version: writeResult.version,
|
|
221
|
-
path: absolutePath,
|
|
222
|
-
};
|
|
223
|
-
}
|
package/scripts/lib/context.mjs
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 요청별 로그 컨텍스트 전파 (AsyncLocalStorage 기반).
|
|
3
|
-
*
|
|
4
|
-
* Hub HTTP 서버의 요청마다 correlationId를 자동 할당하여,
|
|
5
|
-
* 하나의 요청에서 발생한 모든 로그를 추적할 수 있다.
|
|
6
|
-
*
|
|
7
|
-
* 사용법:
|
|
8
|
-
* import { getLogger, getCorrelationId, withRequestContext } from './lib/context.mjs';
|
|
9
|
-
*
|
|
10
|
-
* // 미들웨어에서 컨텍스트 생성
|
|
11
|
-
* withRequestContext({ method: 'POST', path: '/bridge/result' }, () => {
|
|
12
|
-
* const log = getLogger();
|
|
13
|
-
* log.info({ agentId }, 'bridge.result_received');
|
|
14
|
-
* });
|
|
15
|
-
*
|
|
16
|
-
* // 내부 함수에서 자동 상관 ID
|
|
17
|
-
* function processResult() {
|
|
18
|
-
* const log = getLogger();
|
|
19
|
-
* log.info('result.processed'); // correlationId 자동 포함
|
|
20
|
-
* }
|
|
21
|
-
*/
|
|
22
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
23
|
-
import { randomUUID } from "node:crypto";
|
|
24
|
-
|
|
25
|
-
import { logger } from "./logger.mjs";
|
|
26
|
-
|
|
27
|
-
/** @type {AsyncLocalStorage<{logger: import('pino').Logger, correlationId: string}>} */
|
|
28
|
-
export const asyncLocalStorage = new AsyncLocalStorage();
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* 현재 요청 컨텍스트의 로거를 반환한다.
|
|
32
|
-
* 요청 컨텍스트 밖에서 호출하면 기본 로거를 반환한다.
|
|
33
|
-
*
|
|
34
|
-
* @returns {import('pino').Logger}
|
|
35
|
-
*/
|
|
36
|
-
export function getLogger() {
|
|
37
|
-
return asyncLocalStorage.getStore()?.logger || logger;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* 현재 요청의 상관 ID를 반환한다.
|
|
42
|
-
*
|
|
43
|
-
* @returns {string|undefined}
|
|
44
|
-
*/
|
|
45
|
-
export function getCorrelationId() {
|
|
46
|
-
return asyncLocalStorage.getStore()?.correlationId;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* 요청 컨텍스트를 생성하고 콜백을 실행한다.
|
|
51
|
-
*
|
|
52
|
-
* @param {object} context — 컨텍스트 필드 (method, path 등)
|
|
53
|
-
* @param {string} [context.correlationId] — 외부에서 전달된 상관 ID (없으면 자동 생성)
|
|
54
|
-
* @param {function} callback — 컨텍스트 내에서 실행할 함수
|
|
55
|
-
* @returns {*}
|
|
56
|
-
*/
|
|
57
|
-
export function withRequestContext(context, callback) {
|
|
58
|
-
const correlationId = context.correlationId || randomUUID();
|
|
59
|
-
const { correlationId: _, ...rest } = context;
|
|
60
|
-
|
|
61
|
-
const store = {
|
|
62
|
-
correlationId,
|
|
63
|
-
logger: logger.child({ correlationId, ...rest }),
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
return asyncLocalStorage.run(store, callback);
|
|
67
|
-
}
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// scripts/lib/dynamic-route-cli.mjs
|
|
3
|
-
//
|
|
4
|
-
// Phase 1 Wire-up 3 helper — tfx-route.sh 등 sh 경로에서 dynamic routing
|
|
5
|
-
// decision 을 받아 단일 CLI 문자열 (또는 JSON) 으로 출력한다.
|
|
6
|
-
//
|
|
7
|
-
// 사용:
|
|
8
|
-
// node scripts/lib/dynamic-route-cli.mjs --agent-hint codex
|
|
9
|
-
// → "codex"
|
|
10
|
-
// node scripts/lib/dynamic-route-cli.mjs --agent-hint codex --json
|
|
11
|
-
// → {"scenario":"fallback","mode":"codex-default","shards":[{"cli":"codex",...}]}
|
|
12
|
-
// node scripts/lib/dynamic-route-cli.mjs --agent-hint codex --field scenario
|
|
13
|
-
// → "fallback"
|
|
14
|
-
//
|
|
15
|
-
// 정책:
|
|
16
|
-
// - opt-in env TRIFLUX_DYNAMIC_ROUTING=1|true 미설정 시 즉시 silent exit 0
|
|
17
|
-
// (override 없음 = stdout empty, sh caller 가 graceful no-op).
|
|
18
|
-
// - error 발생 시 stderr 에 한 줄 + exit 0 (sh caller 가 fallback path 진행).
|
|
19
|
-
|
|
20
|
-
import { createDynamicRouter } from "@triflux/core/hub/dynamic-routing-engine.mjs";
|
|
21
|
-
|
|
22
|
-
function parseArgs(argv) {
|
|
23
|
-
const args = {
|
|
24
|
-
taskId: `tfx-route-${process.pid}-${Date.now()}`,
|
|
25
|
-
agentHint: "codex",
|
|
26
|
-
teamSize: 1,
|
|
27
|
-
json: false,
|
|
28
|
-
field: null,
|
|
29
|
-
};
|
|
30
|
-
for (let i = 2; i < argv.length; i++) {
|
|
31
|
-
const v = argv[i];
|
|
32
|
-
switch (v) {
|
|
33
|
-
case "--task-id":
|
|
34
|
-
args.taskId = argv[++i] || args.taskId;
|
|
35
|
-
break;
|
|
36
|
-
case "--agent-hint":
|
|
37
|
-
args.agentHint = argv[++i] || args.agentHint;
|
|
38
|
-
break;
|
|
39
|
-
case "--team-size":
|
|
40
|
-
args.teamSize = Number(argv[++i]) || args.teamSize;
|
|
41
|
-
break;
|
|
42
|
-
case "--json":
|
|
43
|
-
args.json = true;
|
|
44
|
-
break;
|
|
45
|
-
case "--field":
|
|
46
|
-
args.field = argv[++i] || null;
|
|
47
|
-
break;
|
|
48
|
-
case "--help":
|
|
49
|
-
case "-h":
|
|
50
|
-
process.stdout.write(
|
|
51
|
-
"Usage: dynamic-route-cli.mjs [--task-id ID] [--agent-hint CLI] [--team-size N] [--json|--field FIELD]\n",
|
|
52
|
-
);
|
|
53
|
-
process.exit(0);
|
|
54
|
-
break;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
return args;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async function main() {
|
|
61
|
-
const args = parseArgs(process.argv);
|
|
62
|
-
|
|
63
|
-
// opt-in env 미설정 시 silent no-op (sh caller graceful path)
|
|
64
|
-
const flag = process.env.TRIFLUX_DYNAMIC_ROUTING;
|
|
65
|
-
if (flag !== "1" && flag !== "true") return;
|
|
66
|
-
|
|
67
|
-
let decision;
|
|
68
|
-
try {
|
|
69
|
-
const router = createDynamicRouter();
|
|
70
|
-
decision = await router.routeRequest({
|
|
71
|
-
task_id: args.taskId,
|
|
72
|
-
team_size: args.teamSize,
|
|
73
|
-
agent_hint: args.agentHint,
|
|
74
|
-
});
|
|
75
|
-
} catch (err) {
|
|
76
|
-
process.stderr.write(
|
|
77
|
-
`[dynamic-route-cli] error: ${err?.message || String(err)}\n`,
|
|
78
|
-
);
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (args.json) {
|
|
83
|
-
process.stdout.write(JSON.stringify(decision));
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (args.field) {
|
|
88
|
-
const value = decision?.[args.field];
|
|
89
|
-
if (value !== undefined && value !== null) {
|
|
90
|
-
process.stdout.write(String(value));
|
|
91
|
-
}
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// 기본 — shards[0].cli 만 stdout 으로 (sh caller 가 즉시 비교)
|
|
96
|
-
const cli = decision?.shards?.[0]?.cli;
|
|
97
|
-
if (cli) {
|
|
98
|
-
process.stdout.write(String(cli));
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
main().catch((err) => {
|
|
103
|
-
process.stderr.write(
|
|
104
|
-
`[dynamic-route-cli] fatal: ${err?.message || String(err)}\n`,
|
|
105
|
-
);
|
|
106
|
-
process.exit(0); // sh caller 가 정상 fallback 하도록 always 0
|
|
107
|
-
});
|