triflux 10.3.2 → 10.3.4
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/.claude-plugin/plugin.json +22 -22
- package/LICENSE +21 -21
- package/README.ko.md +16 -0
- package/README.md +8 -0
- package/hooks/hook-registry.json +256 -256
- package/hub/adaptive-inject.mjs +1 -1
- package/hub/assign-callbacks.mjs +120 -120
- package/hub/delegator/index.mjs +14 -14
- package/hub/delegator/tool-definitions.mjs +35 -35
- package/hub/hitl.mjs +143 -143
- package/hub/lib/path-utils.mjs +167 -0
- package/hub/router.mjs +791 -791
- package/hub/session-fingerprint.mjs +1 -1
- package/hub/team/cli/commands/attach.mjs +37 -37
- package/hub/team/cli/commands/debug.mjs +74 -74
- package/hub/team/cli/commands/focus.mjs +53 -53
- package/hub/team/cli/commands/list.mjs +24 -24
- package/hub/team/cli/commands/start/start-in-process.mjs +40 -40
- package/hub/team/cli/commands/start/start-mux.mjs +73 -73
- package/hub/team/cli/commands/start/start-wt.mjs +69 -69
- package/hub/team/cli/commands/tasks.mjs +13 -13
- package/hub/team/cli/render.mjs +30 -30
- package/hub/team/cli/services/attach-fallback.mjs +54 -54
- package/hub/team/cli/services/member-selector.mjs +30 -30
- package/hub/team/cli/services/native-control.mjs +116 -116
- package/hub/team/cli/services/task-model.mjs +30 -30
- package/hub/team/notify.mjs +1 -1
- package/hub/team/orchestrator.mjs +161 -161
- package/hub/team/runtime-strategy.mjs +74 -0
- package/hub/team/session.mjs +611 -611
- package/hub/team/shared.mjs +13 -13
- package/hub/team/worktree-lifecycle.mjs +61 -2
- package/hub/tray.mjs +368 -368
- package/hub/workers/codex-mcp.mjs +507 -507
- package/hub/workers/factory.mjs +21 -21
- package/hud/hud-qos-status.mjs +17 -3
- package/hud/mission-board.mjs +53 -0
- package/hud/providers/claude.mjs +95 -22
- package/hud/renderers.mjs +39 -5
- package/mesh/index.mjs +63 -0
- package/mesh/mesh-budget.mjs +128 -0
- package/mesh/mesh-heartbeat.mjs +100 -0
- package/mesh/mesh-protocol.mjs +96 -0
- package/mesh/mesh-queue.mjs +165 -0
- package/mesh/mesh-registry.mjs +78 -0
- package/mesh/mesh-router.mjs +76 -0
- package/package.json +2 -1
- package/scripts/completions/tfx.bash +47 -47
- package/scripts/completions/tfx.fish +44 -44
- package/scripts/completions/tfx.zsh +83 -83
- package/scripts/demo.mjs +169 -0
- package/scripts/headless-guard.mjs +16 -4
- package/scripts/hub-ensure.mjs +120 -120
- package/scripts/keyword-detector.mjs +272 -272
- package/scripts/keyword-rules-expander.mjs +521 -521
- package/scripts/lib/mcp-server-catalog.mjs +118 -118
- package/scripts/lib/skill-state.mjs +220 -0
- package/scripts/notion-read.mjs +553 -553
- package/scripts/test-tfx-route-no-claude-native.mjs +57 -57
- package/scripts/tfx-batch-stats.mjs +96 -96
- package/skills/.omc/state/agent-replay-8f0e10a9-9693-4410-96f5-a6b07e8ed995.jsonl +0 -1
- package/skills/.omc/state/idle-notif-cooldown.json +0 -3
- package/skills/.omc/state/last-tool-error.json +0 -7
- package/skills/.omc/state/subagent-tracking.json +0 -7
- package/skills/tfx-remote-spawn/references/hosts.json +0 -16
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
const DEFAULT_INTERVAL_MS = 30_000;
|
|
2
|
+
const DEFAULT_THRESHOLD_MS = 60_000;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates a heartbeat monitor that tracks agent liveness.
|
|
6
|
+
*
|
|
7
|
+
* @param {object} registry - A mesh-registry instance
|
|
8
|
+
* @param {object} [opts]
|
|
9
|
+
* @param {number} [opts.intervalMs=30000] - Scan interval
|
|
10
|
+
* @param {number} [opts.thresholdMs=60000] - Stale threshold
|
|
11
|
+
* @param {function} [opts.onStale] - Called with agentId when stale detected
|
|
12
|
+
* @returns {object} HeartbeatMonitor API
|
|
13
|
+
*/
|
|
14
|
+
export function createHeartbeatMonitor(registry, opts = {}) {
|
|
15
|
+
const {
|
|
16
|
+
intervalMs = DEFAULT_INTERVAL_MS,
|
|
17
|
+
thresholdMs = DEFAULT_THRESHOLD_MS,
|
|
18
|
+
onStale,
|
|
19
|
+
} = opts;
|
|
20
|
+
|
|
21
|
+
/** @type {Map<string, number>} agentId → last heartbeat timestamp */
|
|
22
|
+
const heartbeats = new Map();
|
|
23
|
+
let timer = null;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Records a heartbeat for an agent.
|
|
27
|
+
* @param {string} agentId
|
|
28
|
+
*/
|
|
29
|
+
function recordHeartbeat(agentId) {
|
|
30
|
+
if (!agentId || typeof agentId !== "string") {
|
|
31
|
+
throw new TypeError("agentId must be a non-empty string");
|
|
32
|
+
}
|
|
33
|
+
heartbeats.set(agentId, Date.now());
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Returns agent IDs whose last heartbeat exceeds the threshold.
|
|
38
|
+
* Only considers agents currently registered in the registry.
|
|
39
|
+
*
|
|
40
|
+
* @param {number} [customThresholdMs] - Override default threshold
|
|
41
|
+
* @returns {string[]}
|
|
42
|
+
*/
|
|
43
|
+
function getStaleAgents(customThresholdMs) {
|
|
44
|
+
const threshold = customThresholdMs ?? thresholdMs;
|
|
45
|
+
const now = Date.now();
|
|
46
|
+
const registered = registry.listAll();
|
|
47
|
+
const stale = [];
|
|
48
|
+
|
|
49
|
+
for (const agent of registered) {
|
|
50
|
+
const lastBeat = heartbeats.get(agent.agentId);
|
|
51
|
+
if (lastBeat === undefined || (now - lastBeat) >= threshold) {
|
|
52
|
+
stale.push(agent.agentId);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return stale;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Runs a single scan: finds stale agents and invokes onStale callback.
|
|
60
|
+
*/
|
|
61
|
+
function scan() {
|
|
62
|
+
const stale = getStaleAgents();
|
|
63
|
+
if (typeof onStale === "function") {
|
|
64
|
+
for (const agentId of stale) {
|
|
65
|
+
onStale(agentId);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Starts periodic heartbeat scanning.
|
|
72
|
+
* @param {number} [customIntervalMs]
|
|
73
|
+
*/
|
|
74
|
+
function start(customIntervalMs) {
|
|
75
|
+
stop();
|
|
76
|
+
const interval = customIntervalMs ?? intervalMs;
|
|
77
|
+
timer = setInterval(scan, interval);
|
|
78
|
+
timer.unref?.();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Stops periodic scanning.
|
|
83
|
+
*/
|
|
84
|
+
function stop() {
|
|
85
|
+
if (timer !== null) {
|
|
86
|
+
clearInterval(timer);
|
|
87
|
+
timer = null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Removes heartbeat record for an agent.
|
|
93
|
+
* @param {string} agentId
|
|
94
|
+
*/
|
|
95
|
+
function remove(agentId) {
|
|
96
|
+
heartbeats.delete(agentId);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { recordHeartbeat, getStaleAgents, start, stop, scan, remove };
|
|
100
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export const MSG_TYPES = Object.freeze({
|
|
4
|
+
REQUEST: "request",
|
|
5
|
+
RESPONSE: "response",
|
|
6
|
+
EVENT: "event",
|
|
7
|
+
HEARTBEAT: "heartbeat",
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const VALID_TYPES = new Set(Object.values(MSG_TYPES));
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Creates an immutable mesh message.
|
|
14
|
+
* @param {string} type - One of MSG_TYPES values
|
|
15
|
+
* @param {string} from - Sender agent ID
|
|
16
|
+
* @param {string} to - Recipient agent ID (or "*" for broadcast)
|
|
17
|
+
* @param {unknown} payload - Message payload
|
|
18
|
+
* @returns {Readonly<object>}
|
|
19
|
+
*/
|
|
20
|
+
export function createMessage(type, from, to, payload = null) {
|
|
21
|
+
if (!VALID_TYPES.has(type)) {
|
|
22
|
+
throw new TypeError(`Invalid message type: ${type}`);
|
|
23
|
+
}
|
|
24
|
+
if (!from || typeof from !== "string") {
|
|
25
|
+
throw new TypeError("from must be a non-empty string");
|
|
26
|
+
}
|
|
27
|
+
if (!to || typeof to !== "string") {
|
|
28
|
+
throw new TypeError("to must be a non-empty string");
|
|
29
|
+
}
|
|
30
|
+
return Object.freeze({
|
|
31
|
+
type,
|
|
32
|
+
from,
|
|
33
|
+
to,
|
|
34
|
+
payload,
|
|
35
|
+
timestamp: new Date().toISOString(),
|
|
36
|
+
correlationId: randomUUID(),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Serializes a message to a JSON string.
|
|
42
|
+
* @param {object} message
|
|
43
|
+
* @returns {string}
|
|
44
|
+
*/
|
|
45
|
+
export function serialize(message) {
|
|
46
|
+
return JSON.stringify(message);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Deserializes a JSON string to a message object.
|
|
51
|
+
* @param {string} raw
|
|
52
|
+
* @returns {object}
|
|
53
|
+
*/
|
|
54
|
+
export function deserialize(raw) {
|
|
55
|
+
if (typeof raw !== "string") {
|
|
56
|
+
throw new TypeError("raw must be a string");
|
|
57
|
+
}
|
|
58
|
+
let parsed;
|
|
59
|
+
try {
|
|
60
|
+
parsed = JSON.parse(raw);
|
|
61
|
+
} catch {
|
|
62
|
+
throw new SyntaxError(`Failed to parse message: ${raw}`);
|
|
63
|
+
}
|
|
64
|
+
return parsed;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Validates a message object.
|
|
69
|
+
* @param {unknown} message
|
|
70
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
71
|
+
*/
|
|
72
|
+
export function validate(message) {
|
|
73
|
+
const errors = [];
|
|
74
|
+
|
|
75
|
+
if (!message || typeof message !== "object") {
|
|
76
|
+
return { valid: false, errors: ["message must be an object"] };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!VALID_TYPES.has(message.type)) {
|
|
80
|
+
errors.push(`Invalid type: ${message.type}`);
|
|
81
|
+
}
|
|
82
|
+
if (!message.from || typeof message.from !== "string") {
|
|
83
|
+
errors.push("from must be a non-empty string");
|
|
84
|
+
}
|
|
85
|
+
if (!message.to || typeof message.to !== "string") {
|
|
86
|
+
errors.push("to must be a non-empty string");
|
|
87
|
+
}
|
|
88
|
+
if (!message.timestamp || typeof message.timestamp !== "string") {
|
|
89
|
+
errors.push("timestamp must be a non-empty string");
|
|
90
|
+
}
|
|
91
|
+
if (!message.correlationId || typeof message.correlationId !== "string") {
|
|
92
|
+
errors.push("correlationId must be a non-empty string");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { valid: errors.length === 0, errors };
|
|
96
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
const DEFAULT_MAX_QUEUE_SIZE = 100;
|
|
2
|
+
const DEFAULT_TTL_MS = 0; // 0 = no expiry
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates a per-agent message queue with TTL and size limits.
|
|
6
|
+
*
|
|
7
|
+
* @param {object} [opts]
|
|
8
|
+
* @param {number} [opts.maxQueueSize=100] - Max messages per agent queue
|
|
9
|
+
* @param {number} [opts.ttlMs=0] - Message TTL in ms (0 = no expiry)
|
|
10
|
+
* @returns {object} Queue API
|
|
11
|
+
*/
|
|
12
|
+
export function createMessageQueue(opts = {}) {
|
|
13
|
+
const {
|
|
14
|
+
maxQueueSize = DEFAULT_MAX_QUEUE_SIZE,
|
|
15
|
+
ttlMs = DEFAULT_TTL_MS,
|
|
16
|
+
} = opts;
|
|
17
|
+
|
|
18
|
+
/** @type {Map<string, Array<{ message: object, enqueuedAt: number }>>} */
|
|
19
|
+
const queues = new Map();
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Returns the queue array for an agent (creates if absent).
|
|
23
|
+
* @param {string} agentId
|
|
24
|
+
* @returns {Array}
|
|
25
|
+
*/
|
|
26
|
+
function getQueue(agentId) {
|
|
27
|
+
let q = queues.get(agentId);
|
|
28
|
+
if (!q) {
|
|
29
|
+
q = [];
|
|
30
|
+
queues.set(agentId, q);
|
|
31
|
+
}
|
|
32
|
+
return q;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Removes expired messages from the front of a queue.
|
|
37
|
+
* @param {Array} q
|
|
38
|
+
* @param {number} now
|
|
39
|
+
*/
|
|
40
|
+
function purgeExpired(q, now) {
|
|
41
|
+
if (ttlMs <= 0) return;
|
|
42
|
+
while (q.length > 0 && (now - q[0].enqueuedAt) > ttlMs) {
|
|
43
|
+
q.shift();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Adds a message to the target agent's queue.
|
|
49
|
+
* If queue exceeds maxQueueSize, the oldest message is dropped.
|
|
50
|
+
*
|
|
51
|
+
* @param {string} agentId - Target agent
|
|
52
|
+
* @param {object} message - Mesh message
|
|
53
|
+
* @returns {{ queued: boolean, dropped: boolean }}
|
|
54
|
+
*/
|
|
55
|
+
function enqueue(agentId, message) {
|
|
56
|
+
if (!agentId || typeof agentId !== "string") {
|
|
57
|
+
throw new TypeError("agentId must be a non-empty string");
|
|
58
|
+
}
|
|
59
|
+
const q = getQueue(agentId);
|
|
60
|
+
const now = Date.now();
|
|
61
|
+
|
|
62
|
+
purgeExpired(q, now);
|
|
63
|
+
|
|
64
|
+
let dropped = false;
|
|
65
|
+
if (q.length >= maxQueueSize) {
|
|
66
|
+
q.shift();
|
|
67
|
+
dropped = true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
q.push({ message, enqueuedAt: now });
|
|
71
|
+
return { queued: true, dropped };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Removes and returns the next message for an agent.
|
|
76
|
+
*
|
|
77
|
+
* @param {string} agentId
|
|
78
|
+
* @returns {object | null} The message, or null if queue is empty
|
|
79
|
+
*/
|
|
80
|
+
function dequeue(agentId) {
|
|
81
|
+
const q = queues.get(agentId);
|
|
82
|
+
if (!q || q.length === 0) return null;
|
|
83
|
+
|
|
84
|
+
const now = Date.now();
|
|
85
|
+
purgeExpired(q, now);
|
|
86
|
+
|
|
87
|
+
if (q.length === 0) return null;
|
|
88
|
+
return q.shift().message;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Returns the next message without removing it.
|
|
93
|
+
*
|
|
94
|
+
* @param {string} agentId
|
|
95
|
+
* @returns {object | null}
|
|
96
|
+
*/
|
|
97
|
+
function peek(agentId) {
|
|
98
|
+
const q = queues.get(agentId);
|
|
99
|
+
if (!q || q.length === 0) return null;
|
|
100
|
+
|
|
101
|
+
const now = Date.now();
|
|
102
|
+
purgeExpired(q, now);
|
|
103
|
+
|
|
104
|
+
if (q.length === 0) return null;
|
|
105
|
+
return q[0].message;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Returns the number of (non-expired) messages in an agent's queue.
|
|
110
|
+
*
|
|
111
|
+
* @param {string} agentId
|
|
112
|
+
* @returns {number}
|
|
113
|
+
*/
|
|
114
|
+
function size(agentId) {
|
|
115
|
+
const q = queues.get(agentId);
|
|
116
|
+
if (!q) return 0;
|
|
117
|
+
|
|
118
|
+
const now = Date.now();
|
|
119
|
+
purgeExpired(q, now);
|
|
120
|
+
|
|
121
|
+
return q.length;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Drains all messages for an agent.
|
|
126
|
+
*
|
|
127
|
+
* @param {string} agentId
|
|
128
|
+
* @returns {object[]} Array of messages
|
|
129
|
+
*/
|
|
130
|
+
function drain(agentId) {
|
|
131
|
+
const q = queues.get(agentId);
|
|
132
|
+
if (!q || q.length === 0) return [];
|
|
133
|
+
|
|
134
|
+
const now = Date.now();
|
|
135
|
+
purgeExpired(q, now);
|
|
136
|
+
|
|
137
|
+
const messages = q.map((entry) => entry.message);
|
|
138
|
+
q.length = 0;
|
|
139
|
+
return messages;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Removes an agent's queue entirely.
|
|
144
|
+
* @param {string} agentId
|
|
145
|
+
*/
|
|
146
|
+
function clear(agentId) {
|
|
147
|
+
queues.delete(agentId);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Returns total message count across all agent queues.
|
|
152
|
+
* @returns {number}
|
|
153
|
+
*/
|
|
154
|
+
function totalSize() {
|
|
155
|
+
let total = 0;
|
|
156
|
+
const now = Date.now();
|
|
157
|
+
for (const [, q] of queues) {
|
|
158
|
+
purgeExpired(q, now);
|
|
159
|
+
total += q.length;
|
|
160
|
+
}
|
|
161
|
+
return total;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return { enqueue, dequeue, peek, size, drain, clear, totalSize };
|
|
165
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates an agent registry for the mesh network.
|
|
3
|
+
* Agents register with capabilities; registry enables discovery.
|
|
4
|
+
* @returns {object} Registry API
|
|
5
|
+
*/
|
|
6
|
+
export function createRegistry() {
|
|
7
|
+
// Map<agentId, AgentInfo>
|
|
8
|
+
const agents = new Map();
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Registers an agent with the registry.
|
|
12
|
+
* @param {string} agentId
|
|
13
|
+
* @param {string[]} capabilities
|
|
14
|
+
*/
|
|
15
|
+
function register(agentId, capabilities = []) {
|
|
16
|
+
if (!agentId || typeof agentId !== "string") {
|
|
17
|
+
throw new TypeError("agentId must be a non-empty string");
|
|
18
|
+
}
|
|
19
|
+
if (!Array.isArray(capabilities)) {
|
|
20
|
+
throw new TypeError("capabilities must be an array");
|
|
21
|
+
}
|
|
22
|
+
const info = Object.freeze({
|
|
23
|
+
agentId,
|
|
24
|
+
capabilities: Object.freeze([...capabilities]),
|
|
25
|
+
registeredAt: new Date().toISOString(),
|
|
26
|
+
});
|
|
27
|
+
agents.set(agentId, info);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Unregisters an agent from the registry.
|
|
32
|
+
* @param {string} agentId
|
|
33
|
+
*/
|
|
34
|
+
function unregister(agentId) {
|
|
35
|
+
agents.delete(agentId);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Discovers agents that have a specific capability.
|
|
40
|
+
* @param {string} capability
|
|
41
|
+
* @returns {string[]} Array of agentIds
|
|
42
|
+
*/
|
|
43
|
+
function discover(capability) {
|
|
44
|
+
const result = [];
|
|
45
|
+
for (const [agentId, info] of agents) {
|
|
46
|
+
if (info.capabilities.includes(capability)) {
|
|
47
|
+
result.push(agentId);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Gets agent info by ID.
|
|
55
|
+
* @param {string} agentId
|
|
56
|
+
* @returns {object | null}
|
|
57
|
+
*/
|
|
58
|
+
function getAgent(agentId) {
|
|
59
|
+
return agents.get(agentId) ?? null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Lists all registered agents.
|
|
64
|
+
* @returns {object[]}
|
|
65
|
+
*/
|
|
66
|
+
function listAll() {
|
|
67
|
+
return [...agents.values()];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Clears all registered agents.
|
|
72
|
+
*/
|
|
73
|
+
function clear() {
|
|
74
|
+
agents.clear();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return { register, unregister, discover, getAgent, listAll, clear };
|
|
78
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { validate } from "./mesh-protocol.mjs";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Routes a mesh message to target agent(s) based on the `to` field.
|
|
5
|
+
*
|
|
6
|
+
* Addressing modes:
|
|
7
|
+
* - "agent-id" → direct delivery (registry lookup)
|
|
8
|
+
* - "*" → broadcast to all registered agents
|
|
9
|
+
* - "capability:X" → discover agents with capability X
|
|
10
|
+
*
|
|
11
|
+
* @param {object} message - A mesh-protocol message
|
|
12
|
+
* @param {object} registry - A mesh-registry instance
|
|
13
|
+
* @returns {{ routed: boolean, targets?: string[], reason?: string }}
|
|
14
|
+
*/
|
|
15
|
+
export function routeMessage(message, registry) {
|
|
16
|
+
const { valid, errors } = validate(message);
|
|
17
|
+
if (!valid) {
|
|
18
|
+
return { routed: false, reason: `invalid message: ${errors.join(", ")}` };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const { to, from } = message;
|
|
22
|
+
|
|
23
|
+
// Broadcast
|
|
24
|
+
if (to === "*") {
|
|
25
|
+
const all = registry.listAll();
|
|
26
|
+
const targets = all
|
|
27
|
+
.map((a) => a.agentId)
|
|
28
|
+
.filter((id) => id !== from);
|
|
29
|
+
if (targets.length === 0) {
|
|
30
|
+
return { routed: false, reason: "broadcast: no agents registered" };
|
|
31
|
+
}
|
|
32
|
+
return { routed: true, targets };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Capability-based routing
|
|
36
|
+
if (to.startsWith("capability:")) {
|
|
37
|
+
const capability = to.slice("capability:".length);
|
|
38
|
+
if (!capability) {
|
|
39
|
+
return { routed: false, reason: "capability: empty capability name" };
|
|
40
|
+
}
|
|
41
|
+
const targets = registry.discover(capability);
|
|
42
|
+
if (targets.length === 0) {
|
|
43
|
+
return { routed: false, reason: `capability: no agents with "${capability}"` };
|
|
44
|
+
}
|
|
45
|
+
return { routed: true, targets };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Direct addressing
|
|
49
|
+
const agent = registry.getAgent(to);
|
|
50
|
+
if (!agent) {
|
|
51
|
+
return { routed: false, reason: `agent not found: "${to}"` };
|
|
52
|
+
}
|
|
53
|
+
return { routed: true, targets: [to] };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Routes a message and collects dead-letter info when delivery fails.
|
|
58
|
+
*
|
|
59
|
+
* @param {object} message
|
|
60
|
+
* @param {object} registry
|
|
61
|
+
* @returns {{ routed: boolean, targets?: string[], deadLetter?: object }}
|
|
62
|
+
*/
|
|
63
|
+
export function routeOrDeadLetter(message, registry) {
|
|
64
|
+
const result = routeMessage(message, registry);
|
|
65
|
+
if (!result.routed) {
|
|
66
|
+
return {
|
|
67
|
+
...result,
|
|
68
|
+
deadLetter: {
|
|
69
|
+
originalMessage: message,
|
|
70
|
+
reason: result.reason,
|
|
71
|
+
timestamp: new Date().toISOString(),
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return result;
|
|
76
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "triflux",
|
|
3
|
-
"version": "10.3.
|
|
3
|
+
"version": "10.3.4",
|
|
4
4
|
"description": "CLI-first multi-model orchestrator for Claude Code — route tasks to Codex, Gemini, and Claude",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"scripts",
|
|
25
25
|
"hooks",
|
|
26
26
|
"hud",
|
|
27
|
+
"mesh",
|
|
27
28
|
".claude-plugin",
|
|
28
29
|
"README.md",
|
|
29
30
|
"README.ko.md",
|
|
@@ -1,47 +1,47 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Installation: source /path/to/tfx.bash 또는 ~/.bashrc에 추가
|
|
3
|
-
|
|
4
|
-
_tfx_completion() {
|
|
5
|
-
local cur prev words cword
|
|
6
|
-
COMPREPLY=()
|
|
7
|
-
cur="${COMP_WORDS[COMP_CWORD]}"
|
|
8
|
-
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
|
9
|
-
words=("${COMP_WORDS[@]}")
|
|
10
|
-
cword=$COMP_CWORD
|
|
11
|
-
|
|
12
|
-
local commands="setup doctor multi hub auto codex gemini"
|
|
13
|
-
local multi_cmds="status stop kill attach list"
|
|
14
|
-
local hub_cmds="start stop status restart"
|
|
15
|
-
local flags="--thorough --quick --tmux --psmux --agents --no-attach --timeout"
|
|
16
|
-
|
|
17
|
-
if [[ $cword -eq 1 ]]; then
|
|
18
|
-
COMPREPLY=( $(compgen -W "${commands}" -- "$cur") )
|
|
19
|
-
return 0
|
|
20
|
-
fi
|
|
21
|
-
|
|
22
|
-
local cmd="${words[1]}"
|
|
23
|
-
case "${cmd}" in
|
|
24
|
-
multi)
|
|
25
|
-
if [[ $cword -eq 2 && ! "$cur" == -* ]]; then
|
|
26
|
-
COMPREPLY=( $(compgen -W "${multi_cmds}" -- "$cur") )
|
|
27
|
-
else
|
|
28
|
-
COMPREPLY=( $(compgen -W "${flags}" -- "$cur") )
|
|
29
|
-
fi
|
|
30
|
-
;;
|
|
31
|
-
hub)
|
|
32
|
-
if [[ $cword -eq 2 ]]; then
|
|
33
|
-
COMPREPLY=( $(compgen -W "${hub_cmds}" -- "$cur") )
|
|
34
|
-
fi
|
|
35
|
-
;;
|
|
36
|
-
doctor)
|
|
37
|
-
COMPREPLY=( $(compgen -W "--fix --reset" -- "$cur") )
|
|
38
|
-
;;
|
|
39
|
-
setup|auto|codex|gemini)
|
|
40
|
-
if [[ "$cur" == -* ]]; then
|
|
41
|
-
COMPREPLY=( $(compgen -W "${flags}" -- "$cur") )
|
|
42
|
-
fi
|
|
43
|
-
;;
|
|
44
|
-
esac
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
complete -F _tfx_completion tfx
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Installation: source /path/to/tfx.bash 또는 ~/.bashrc에 추가
|
|
3
|
+
|
|
4
|
+
_tfx_completion() {
|
|
5
|
+
local cur prev words cword
|
|
6
|
+
COMPREPLY=()
|
|
7
|
+
cur="${COMP_WORDS[COMP_CWORD]}"
|
|
8
|
+
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
|
9
|
+
words=("${COMP_WORDS[@]}")
|
|
10
|
+
cword=$COMP_CWORD
|
|
11
|
+
|
|
12
|
+
local commands="setup doctor multi hub auto codex gemini"
|
|
13
|
+
local multi_cmds="status stop kill attach list"
|
|
14
|
+
local hub_cmds="start stop status restart"
|
|
15
|
+
local flags="--thorough --quick --tmux --psmux --agents --no-attach --timeout"
|
|
16
|
+
|
|
17
|
+
if [[ $cword -eq 1 ]]; then
|
|
18
|
+
COMPREPLY=( $(compgen -W "${commands}" -- "$cur") )
|
|
19
|
+
return 0
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
local cmd="${words[1]}"
|
|
23
|
+
case "${cmd}" in
|
|
24
|
+
multi)
|
|
25
|
+
if [[ $cword -eq 2 && ! "$cur" == -* ]]; then
|
|
26
|
+
COMPREPLY=( $(compgen -W "${multi_cmds}" -- "$cur") )
|
|
27
|
+
else
|
|
28
|
+
COMPREPLY=( $(compgen -W "${flags}" -- "$cur") )
|
|
29
|
+
fi
|
|
30
|
+
;;
|
|
31
|
+
hub)
|
|
32
|
+
if [[ $cword -eq 2 ]]; then
|
|
33
|
+
COMPREPLY=( $(compgen -W "${hub_cmds}" -- "$cur") )
|
|
34
|
+
fi
|
|
35
|
+
;;
|
|
36
|
+
doctor)
|
|
37
|
+
COMPREPLY=( $(compgen -W "--fix --reset" -- "$cur") )
|
|
38
|
+
;;
|
|
39
|
+
setup|auto|codex|gemini)
|
|
40
|
+
if [[ "$cur" == -* ]]; then
|
|
41
|
+
COMPREPLY=( $(compgen -W "${flags}" -- "$cur") )
|
|
42
|
+
fi
|
|
43
|
+
;;
|
|
44
|
+
esac
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
complete -F _tfx_completion tfx
|