triflux 10.3.2 → 10.3.3

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 (55) hide show
  1. package/.claude-plugin/plugin.json +22 -22
  2. package/LICENSE +21 -21
  3. package/README.ko.md +16 -0
  4. package/README.md +8 -0
  5. package/hooks/hook-registry.json +256 -256
  6. package/hub/adaptive-inject.mjs +1 -1
  7. package/hub/assign-callbacks.mjs +120 -120
  8. package/hub/delegator/index.mjs +14 -14
  9. package/hub/delegator/tool-definitions.mjs +35 -35
  10. package/hub/hitl.mjs +143 -143
  11. package/hub/router.mjs +791 -791
  12. package/hub/session-fingerprint.mjs +1 -1
  13. package/hub/team/cli/commands/attach.mjs +37 -37
  14. package/hub/team/cli/commands/debug.mjs +74 -74
  15. package/hub/team/cli/commands/focus.mjs +53 -53
  16. package/hub/team/cli/commands/list.mjs +24 -24
  17. package/hub/team/cli/commands/start/start-in-process.mjs +40 -40
  18. package/hub/team/cli/commands/start/start-mux.mjs +73 -73
  19. package/hub/team/cli/commands/start/start-wt.mjs +69 -69
  20. package/hub/team/cli/commands/tasks.mjs +13 -13
  21. package/hub/team/cli/render.mjs +30 -30
  22. package/hub/team/cli/services/attach-fallback.mjs +54 -54
  23. package/hub/team/cli/services/member-selector.mjs +30 -30
  24. package/hub/team/cli/services/native-control.mjs +116 -116
  25. package/hub/team/cli/services/task-model.mjs +30 -30
  26. package/hub/team/notify.mjs +1 -1
  27. package/hub/team/orchestrator.mjs +161 -161
  28. package/hub/team/session.mjs +611 -611
  29. package/hub/team/shared.mjs +13 -13
  30. package/hub/tray.mjs +368 -368
  31. package/hub/workers/codex-mcp.mjs +507 -507
  32. package/hub/workers/factory.mjs +21 -21
  33. package/mesh/index.mjs +63 -0
  34. package/mesh/mesh-budget.mjs +128 -0
  35. package/mesh/mesh-heartbeat.mjs +100 -0
  36. package/mesh/mesh-protocol.mjs +96 -0
  37. package/mesh/mesh-queue.mjs +165 -0
  38. package/mesh/mesh-registry.mjs +78 -0
  39. package/mesh/mesh-router.mjs +76 -0
  40. package/package.json +2 -1
  41. package/scripts/completions/tfx.bash +47 -47
  42. package/scripts/completions/tfx.fish +44 -44
  43. package/scripts/completions/tfx.zsh +83 -83
  44. package/scripts/hub-ensure.mjs +120 -120
  45. package/scripts/keyword-detector.mjs +272 -272
  46. package/scripts/keyword-rules-expander.mjs +521 -521
  47. package/scripts/lib/mcp-server-catalog.mjs +118 -118
  48. package/scripts/notion-read.mjs +553 -553
  49. package/scripts/test-tfx-route-no-claude-native.mjs +57 -57
  50. package/scripts/tfx-batch-stats.mjs +96 -96
  51. package/skills/.omc/state/agent-replay-8f0e10a9-9693-4410-96f5-a6b07e8ed995.jsonl +0 -1
  52. package/skills/.omc/state/idle-notif-cooldown.json +0 -3
  53. package/skills/.omc/state/last-tool-error.json +0 -7
  54. package/skills/.omc/state/subagent-tracking.json +0 -7
  55. package/skills/tfx-remote-spawn/references/hosts.json +0 -16
@@ -1,21 +1,21 @@
1
- // hub/workers/factory.mjs — Worker 생성 팩토리
2
-
3
- import { GeminiWorker } from './gemini-worker.mjs';
4
- import { ClaudeWorker } from './claude-worker.mjs';
5
- import { CodexMcpWorker } from './codex-mcp.mjs';
6
- import { DelegatorMcpWorker } from './delegator-mcp.mjs';
7
-
8
- export function createWorker(type, opts = {}) {
9
- switch (type) {
10
- case 'gemini':
11
- return new GeminiWorker(opts);
12
- case 'claude':
13
- return new ClaudeWorker(opts);
14
- case 'codex':
15
- return new CodexMcpWorker(opts);
16
- case 'delegator':
17
- return new DelegatorMcpWorker(opts);
18
- default:
19
- throw new Error(`Unknown worker type: ${type}`);
20
- }
21
- }
1
+ // hub/workers/factory.mjs — Worker 생성 팩토리
2
+
3
+ import { GeminiWorker } from './gemini-worker.mjs';
4
+ import { ClaudeWorker } from './claude-worker.mjs';
5
+ import { CodexMcpWorker } from './codex-mcp.mjs';
6
+ import { DelegatorMcpWorker } from './delegator-mcp.mjs';
7
+
8
+ export function createWorker(type, opts = {}) {
9
+ switch (type) {
10
+ case 'gemini':
11
+ return new GeminiWorker(opts);
12
+ case 'claude':
13
+ return new ClaudeWorker(opts);
14
+ case 'codex':
15
+ return new CodexMcpWorker(opts);
16
+ case 'delegator':
17
+ return new DelegatorMcpWorker(opts);
18
+ default:
19
+ throw new Error(`Unknown worker type: ${type}`);
20
+ }
21
+ }
package/mesh/index.mjs ADDED
@@ -0,0 +1,63 @@
1
+ import { readdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ export { MSG_TYPES, createMessage, serialize, deserialize, validate } from "./mesh-protocol.mjs";
5
+ export { createRegistry } from "./mesh-registry.mjs";
6
+ export { createMeshBudget } from "./mesh-budget.mjs";
7
+ export { routeMessage, routeOrDeadLetter } from "./mesh-router.mjs";
8
+ export { createMessageQueue } from "./mesh-queue.mjs";
9
+ export { createHeartbeatMonitor } from "./mesh-heartbeat.mjs";
10
+
11
+ /**
12
+ * Loads skills assigned to a specific agent from a skills directory.
13
+ * Reuses the same directory-scan approach as generateSkillDocs().
14
+ *
15
+ * @param {string} agentId - The agent identifier
16
+ * @param {string} skillsDir - Path to the skills directory
17
+ * @returns {Promise<string[]>} Array of skill names available to this agent
18
+ */
19
+ export async function loadSkillsForAgent(agentId, skillsDir) {
20
+ if (!agentId || typeof agentId !== "string") {
21
+ throw new TypeError("agentId must be a non-empty string");
22
+ }
23
+ if (!skillsDir || typeof skillsDir !== "string") {
24
+ throw new TypeError("skillsDir must be a non-empty string");
25
+ }
26
+
27
+ let entries;
28
+ try {
29
+ entries = readdirSync(skillsDir, { withFileTypes: true });
30
+ } catch {
31
+ return [];
32
+ }
33
+
34
+ const skills = [];
35
+ for (const entry of entries) {
36
+ if (!entry.isDirectory()) continue;
37
+ const skillName = entry.name;
38
+ const skillPath = join(skillsDir, skillName, "SKILL.md");
39
+ let skillContent = null;
40
+ try {
41
+ const { readFileSync } = await import("node:fs");
42
+ skillContent = readFileSync(skillPath, "utf8");
43
+ } catch {
44
+ // Skill has no SKILL.md — include it anyway
45
+ }
46
+
47
+ // If SKILL.md mentions the agentId or no agent restriction, include it
48
+ const isRestricted = skillContent
49
+ ? /^agents?\s*:/im.test(skillContent)
50
+ : false;
51
+
52
+ if (!isRestricted) {
53
+ skills.push(skillName);
54
+ continue;
55
+ }
56
+
57
+ if (skillContent && skillContent.includes(agentId)) {
58
+ skills.push(skillName);
59
+ }
60
+ }
61
+
62
+ return skills;
63
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Warning level thresholds mirroring context-monitor.mjs classifyContextThreshold().
3
+ * Reproduced here to avoid cross-module dependency.
4
+ */
5
+ const WARNING_LEVELS = Object.freeze({
6
+ critical: 90,
7
+ warn: 80,
8
+ info: 60,
9
+ ok: 0,
10
+ });
11
+
12
+ /**
13
+ * Clamps a percentage value to [0, 100].
14
+ * @param {number} value
15
+ * @returns {number}
16
+ */
17
+ function clampPercent(value) {
18
+ const n = Number(value);
19
+ if (!Number.isFinite(n)) return 0;
20
+ return Math.min(100, Math.max(0, n));
21
+ }
22
+
23
+ /**
24
+ * Classifies a usage percentage into a warning level.
25
+ * Mirrors context-monitor.mjs classifyContextThreshold().
26
+ * @param {number} percent
27
+ * @returns {{ level: string, message: string }}
28
+ */
29
+ function classifyLevel(percent) {
30
+ const p = clampPercent(percent);
31
+ if (p >= WARNING_LEVELS.critical) return { level: "critical", message: "에이전트 분할 또는 세션 교체 권장" };
32
+ if (p >= WARNING_LEVELS.warn) return { level: "warn", message: "압축 권장" };
33
+ if (p >= WARNING_LEVELS.info) return { level: "info", message: "컨텍스트 절반 이상 사용" };
34
+ return { level: "ok", message: "" };
35
+ }
36
+
37
+ /**
38
+ * Creates a per-agent token budget manager.
39
+ * @returns {object} Budget API
40
+ */
41
+ export function createMeshBudget() {
42
+ // Map<agentId, { allocated: number, consumed: number }>
43
+ const budgets = new Map();
44
+
45
+ /**
46
+ * Allocates a token budget to an agent.
47
+ * @param {string} agentId
48
+ * @param {number} tokenLimit
49
+ */
50
+ function allocate(agentId, tokenLimit) {
51
+ if (!agentId || typeof agentId !== "string") {
52
+ throw new TypeError("agentId must be a non-empty string");
53
+ }
54
+ const limit = Math.max(0, Math.round(Number(tokenLimit) || 0));
55
+ const existing = budgets.get(agentId);
56
+ budgets.set(agentId, {
57
+ allocated: limit,
58
+ consumed: existing?.consumed ?? 0,
59
+ });
60
+ }
61
+
62
+ /**
63
+ * Records token consumption for an agent.
64
+ * @param {string} agentId
65
+ * @param {number} tokens
66
+ * @returns {{ remaining: number, percent: number, level: string }}
67
+ */
68
+ function consume(agentId, tokens) {
69
+ const budget = budgets.get(agentId);
70
+ if (!budget) {
71
+ throw new Error(`No budget allocated for agent: ${agentId}`);
72
+ }
73
+ const amount = Math.max(0, Math.round(Number(tokens) || 0));
74
+ const updated = {
75
+ allocated: budget.allocated,
76
+ consumed: budget.consumed + amount,
77
+ };
78
+ budgets.set(agentId, updated);
79
+
80
+ const remaining = Math.max(0, updated.allocated - updated.consumed);
81
+ const percent = updated.allocated > 0
82
+ ? clampPercent((updated.consumed / updated.allocated) * 100)
83
+ : 100;
84
+ const { level } = classifyLevel(percent);
85
+ return { remaining, percent, level };
86
+ }
87
+
88
+ /**
89
+ * Returns the budget status for an agent.
90
+ * @param {string} agentId
91
+ * @returns {{ allocated: number, consumed: number, remaining: number, level: string }}
92
+ */
93
+ function getStatus(agentId) {
94
+ const budget = budgets.get(agentId);
95
+ if (!budget) {
96
+ return { allocated: 0, consumed: 0, remaining: 0, level: "ok" };
97
+ }
98
+ const remaining = Math.max(0, budget.allocated - budget.consumed);
99
+ const percent = budget.allocated > 0
100
+ ? clampPercent((budget.consumed / budget.allocated) * 100)
101
+ : 0;
102
+ const { level } = classifyLevel(percent);
103
+ return { allocated: budget.allocated, consumed: budget.consumed, remaining, level };
104
+ }
105
+
106
+ /**
107
+ * Resets consumed tokens for all agents (keeps allocations).
108
+ */
109
+ function resetAll() {
110
+ for (const [agentId, budget] of budgets) {
111
+ budgets.set(agentId, { allocated: budget.allocated, consumed: 0 });
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Returns a snapshot of all current allocations.
117
+ * @returns {Map<string, { allocated: number, consumed: number }>}
118
+ */
119
+ function listAllocations() {
120
+ const snap = new Map();
121
+ for (const [id, b] of budgets) {
122
+ snap.set(id, Object.freeze({ ...b }));
123
+ }
124
+ return snap;
125
+ }
126
+
127
+ return { allocate, consume, getStatus, resetAll, listAllocations };
128
+ }
@@ -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
+ }