let-them-talk 5.3.0 → 5.4.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/CHANGELOG.md +3 -1
- package/README.md +158 -592
- package/SECURITY.md +3 -3
- package/USAGE.md +151 -0
- package/agent-contracts.js +447 -0
- package/api-agents.js +760 -0
- package/autonomy/decision-v2.js +380 -0
- package/autonomy/watchdog-policy.js +572 -0
- package/cli.js +454 -298
- package/conversation-templates/autonomous-feature.json +83 -22
- package/conversation-templates/code-review.json +69 -21
- package/conversation-templates/debug-squad.json +69 -21
- package/conversation-templates/feature-build.json +69 -21
- package/conversation-templates/research-write.json +69 -21
- package/dashboard.html +3148 -174
- package/dashboard.js +823 -786
- package/data-dir.js +58 -0
- package/docs/architecture/branch-semantics.md +157 -0
- package/docs/architecture/canonical-event-schema.md +88 -0
- package/docs/architecture/markdown-workspace.md +183 -0
- package/docs/architecture/runtime-contract.md +459 -0
- package/docs/architecture/runtime-migration-hardening.md +64 -0
- package/events/hooks.js +154 -0
- package/events/log.js +457 -0
- package/events/replay.js +33 -0
- package/events/schema.js +432 -0
- package/managed-team-integration.js +261 -0
- package/office/agents.js +704 -597
- package/office/animation.js +1 -1
- package/office/assets/arcade-cabinet.js +141 -0
- package/office/assets/archway.js +77 -0
- package/office/assets/bar-counter.js +91 -0
- package/office/assets/bar-stool.js +71 -0
- package/office/assets/beanbag.js +64 -0
- package/office/assets/bench.js +99 -0
- package/office/assets/bollard.js +87 -0
- package/office/assets/cactus.js +100 -0
- package/office/assets/carpet-tile.js +46 -0
- package/office/assets/chair.js +123 -0
- package/office/assets/chandelier.js +107 -0
- package/office/assets/coffee-machine.js +95 -0
- package/office/assets/coffee-table.js +81 -0
- package/office/assets/column.js +95 -0
- package/office/assets/desk-lamp.js +102 -0
- package/office/assets/desk.js +76 -0
- package/office/assets/dining-table.js +105 -0
- package/office/assets/door.js +70 -0
- package/office/assets/dual-monitor.js +72 -0
- package/office/assets/fence.js +76 -0
- package/office/assets/filing-cabinet.js +111 -0
- package/office/assets/floor-lamp.js +69 -0
- package/office/assets/floor-tile.js +54 -0
- package/office/assets/flower-pot.js +76 -0
- package/office/assets/foosball.js +95 -0
- package/office/assets/fridge.js +99 -0
- package/office/assets/gaming-chair.js +154 -0
- package/office/assets/gaming-desk.js +105 -0
- package/office/assets/glass-door.js +72 -0
- package/office/assets/glass-wall.js +64 -0
- package/office/assets/half-wall.js +49 -0
- package/office/assets/hanging-plant.js +112 -0
- package/office/assets/index.js +151 -0
- package/office/assets/indoor-tree.js +90 -0
- package/office/assets/l-sofa.js +153 -0
- package/office/assets/marble-floor.js +64 -0
- package/office/assets/materials.js +40 -0
- package/office/assets/meeting-table.js +88 -0
- package/office/assets/microwave.js +94 -0
- package/office/assets/monitor.js +67 -0
- package/office/assets/neon-strip.js +73 -0
- package/office/assets/painting.js +84 -0
- package/office/assets/palm-tree.js +108 -0
- package/office/assets/pc-tower.js +91 -0
- package/office/assets/pendant-light.js +67 -0
- package/office/assets/ping-pong.js +114 -0
- package/office/assets/plant.js +72 -0
- package/office/assets/planter-box.js +95 -0
- package/office/assets/pool-table.js +94 -0
- package/office/assets/printer.js +113 -0
- package/office/assets/reception-desk.js +133 -0
- package/office/assets/rug.js +78 -0
- package/office/assets/sculpture.js +85 -0
- package/office/assets/server-rack.js +98 -0
- package/office/assets/sink.js +109 -0
- package/office/assets/sofa.js +106 -0
- package/office/assets/speaker.js +83 -0
- package/office/assets/spotlight.js +83 -0
- package/office/assets/street-lamp.js +97 -0
- package/office/assets/trash-can.js +83 -0
- package/office/assets/treadmill.js +126 -0
- package/office/assets/trophy.js +89 -0
- package/office/assets/tv-screen.js +79 -0
- package/office/assets/vase.js +84 -0
- package/office/assets/wall-clock.js +84 -0
- package/office/assets/wall.js +53 -0
- package/office/assets/water-cooler.js +146 -0
- package/office/assets/whiteboard.js +115 -0
- package/office/assets.js +3 -431
- package/office/builder.js +791 -355
- package/office/campus-env.js +1012 -1119
- package/office/environment.js +2 -0
- package/office/gallery.js +997 -0
- package/office/index.js +165 -61
- package/office/navigation.js +173 -152
- package/office/player.js +178 -68
- package/office/robot-character.js +272 -0
- package/office/spectator-camera.js +33 -10
- package/office/state.js +2 -0
- package/office/world-save.js +35 -4
- package/package.json +57 -3
- package/providers/comfyui.js +383 -0
- package/providers/dalle.js +79 -0
- package/providers/gemini.js +181 -0
- package/providers/ollama.js +184 -0
- package/providers/replicate.js +115 -0
- package/providers/zai.js +183 -0
- package/runtime-descriptor.js +270 -0
- package/scripts/check-agent-contract-advisory.js +132 -0
- package/scripts/check-api-agent-parity.js +277 -0
- package/scripts/check-autonomy-v2-decision.js +207 -0
- package/scripts/check-autonomy-v2-execution.js +588 -0
- package/scripts/check-autonomy-v2-watchdog.js +224 -0
- package/scripts/check-branch-fork-snapshot.js +337 -0
- package/scripts/check-branch-isolation.js +787 -0
- package/scripts/check-branch-semantics.js +139 -0
- package/scripts/check-dashboard-control-plane.js +1304 -0
- package/scripts/check-docs-onboarding.js +490 -0
- package/scripts/check-event-schema.js +276 -0
- package/scripts/check-evidence-completion.js +239 -0
- package/scripts/check-invariants.js +992 -0
- package/scripts/check-lifecycle-hooks.js +525 -0
- package/scripts/check-managed-team-integration.js +166 -0
- package/scripts/check-markdown-workspace-export.js +548 -0
- package/scripts/check-markdown-workspace-safety.js +347 -0
- package/scripts/check-markdown-workspace.js +136 -0
- package/scripts/check-message-replay.js +429 -0
- package/scripts/check-migration-hardening.js +300 -0
- package/scripts/check-performance-indexing.js +272 -0
- package/scripts/check-provider-capabilities.js +316 -0
- package/scripts/check-runtime-contract.js +109 -0
- package/scripts/check-session-aware-context.js +172 -0
- package/scripts/check-session-lifecycle.js +210 -0
- package/scripts/export-markdown-workspace.js +84 -0
- package/scripts/fixtures/message-replay/clean.jsonl +2 -0
- package/scripts/fixtures/message-replay/corrupt-correction-payload.jsonl +1 -0
- package/scripts/fixtures/message-replay/corrupt-jsonl.jsonl +1 -0
- package/scripts/fixtures/message-replay/corrupt-payload.jsonl +1 -0
- package/scripts/fixtures/message-replay/out-of-order.jsonl +2 -0
- package/scripts/migrate-legacy-to-canonical.js +201 -0
- package/scripts/run-verification-suite.js +242 -0
- package/scripts/sync-packaged-docs.js +69 -0
- package/server.js +9546 -7216
- package/state/agents.js +161 -0
- package/state/canonical.js +3068 -0
- package/state/dashboard-queries.js +441 -0
- package/state/evidence.js +56 -0
- package/state/io.js +69 -0
- package/state/markdown-workspace.js +951 -0
- package/state/messages.js +669 -0
- package/state/sessions.js +683 -0
- package/state/tasks-workflows.js +92 -0
- package/templates/debate.json +2 -2
- package/templates/managed.json +4 -4
- package/templates/pair.json +2 -2
- package/templates/review.json +2 -2
- package/templates/team.json +3 -3
|
@@ -56,6 +56,12 @@ export function SpectatorCamera(camera, domElement) {
|
|
|
56
56
|
lastMouse.x = e.clientX;
|
|
57
57
|
lastMouse.y = e.clientY;
|
|
58
58
|
e.preventDefault();
|
|
59
|
+
|
|
60
|
+
// In player mode: request pointer lock on left click for FPS-style mouse
|
|
61
|
+
// Skip if world builder is open (needs free mouse cursor)
|
|
62
|
+
if (!self.enabled && e.button === 0 && !document.pointerLockElement && !window._builderActive) {
|
|
63
|
+
domElement.requestPointerLock();
|
|
64
|
+
}
|
|
59
65
|
}
|
|
60
66
|
|
|
61
67
|
function onMouseUp(e) {
|
|
@@ -65,12 +71,21 @@ export function SpectatorCamera(camera, domElement) {
|
|
|
65
71
|
}
|
|
66
72
|
|
|
67
73
|
function onMouseMove(e) {
|
|
68
|
-
var
|
|
69
|
-
var dy
|
|
70
|
-
|
|
71
|
-
|
|
74
|
+
var isLocked = document.pointerLockElement === domElement;
|
|
75
|
+
var dx, dy;
|
|
76
|
+
|
|
77
|
+
if (isLocked) {
|
|
78
|
+
// Pointer lock: use movementX/Y directly (no need for drag)
|
|
79
|
+
dx = e.movementX;
|
|
80
|
+
dy = e.movementY;
|
|
81
|
+
} else {
|
|
82
|
+
dx = e.clientX - lastMouse.x;
|
|
83
|
+
dy = e.clientY - lastMouse.y;
|
|
84
|
+
lastMouse.x = e.clientX;
|
|
85
|
+
lastMouse.y = e.clientY;
|
|
86
|
+
}
|
|
72
87
|
|
|
73
|
-
if (isRightDrag) {
|
|
88
|
+
if (isRightDrag || isLocked) {
|
|
74
89
|
// Look around (rotate) — always update euler, but only apply to camera in spectator mode
|
|
75
90
|
euler.y -= dx * self.lookSpeed;
|
|
76
91
|
euler.x -= dy * self.lookSpeed;
|
|
@@ -78,11 +93,11 @@ export function SpectatorCamera(camera, domElement) {
|
|
|
78
93
|
if (self.enabled) {
|
|
79
94
|
camera.quaternion.setFromEuler(euler);
|
|
80
95
|
}
|
|
81
|
-
// In player mode, euler is read by player.js for orbit camera
|
|
96
|
+
// In player mode, euler is read by player.js for orbit/FP camera
|
|
82
97
|
}
|
|
83
98
|
|
|
84
|
-
if ((isLeftDrag || isMiddleDrag) && self.enabled) {
|
|
85
|
-
// Pan (strafe) — only in spectator mode
|
|
99
|
+
if ((isLeftDrag || isMiddleDrag) && self.enabled && !isLocked) {
|
|
100
|
+
// Pan (strafe) — only in spectator mode, not during pointer lock
|
|
86
101
|
_panRight.setFromMatrixColumn(camera.matrixWorld, 0);
|
|
87
102
|
_panUp.setFromMatrixColumn(camera.matrixWorld, 1);
|
|
88
103
|
camera.position.addScaledVector(_panRight, -dx * self.panSpeed);
|
|
@@ -127,6 +142,12 @@ export function SpectatorCamera(camera, domElement) {
|
|
|
127
142
|
if (e.target === domElement) e.preventDefault();
|
|
128
143
|
}
|
|
129
144
|
|
|
145
|
+
// Release pointer lock on Escape (browser does this, but track it)
|
|
146
|
+
function onPointerLockChange() {
|
|
147
|
+
// Nothing special needed — isLocked check in onMouseMove handles it
|
|
148
|
+
}
|
|
149
|
+
document.addEventListener('pointerlockchange', onPointerLockChange);
|
|
150
|
+
|
|
130
151
|
// --- Public methods ---
|
|
131
152
|
self.update = function(dt) {
|
|
132
153
|
if (!self.enabled) return;
|
|
@@ -144,14 +165,16 @@ export function SpectatorCamera(camera, domElement) {
|
|
|
144
165
|
|
|
145
166
|
if (moveDir.lengthSq() > 0) {
|
|
146
167
|
moveDir.normalize();
|
|
147
|
-
// Transform to world space using camera
|
|
168
|
+
// Transform to world space using camera yaw for horizontal movement
|
|
148
169
|
var forward = new THREE.Vector3();
|
|
149
170
|
var right = new THREE.Vector3();
|
|
150
171
|
camera.getWorldDirection(forward);
|
|
172
|
+
forward.y = 0;
|
|
173
|
+
if (forward.lengthSq() > 0) forward.normalize();
|
|
151
174
|
right.crossVectors(forward, camera.up).normalize();
|
|
152
175
|
var worldUp = new THREE.Vector3(0, 1, 0);
|
|
153
176
|
|
|
154
|
-
// Forward/back
|
|
177
|
+
// Forward/back stays horizontal; vertical travel uses dedicated up/down controls
|
|
155
178
|
velocity.addScaledVector(forward, -moveDir.z * speed * dt);
|
|
156
179
|
velocity.addScaledVector(right, moveDir.x * speed * dt);
|
|
157
180
|
velocity.addScaledVector(worldUp, moveDir.y * speed * dt);
|
package/office/state.js
CHANGED
package/office/world-save.js
CHANGED
|
@@ -15,10 +15,31 @@ function getProjectParam() {
|
|
|
15
15
|
return window.activeProject ? '?project=' + encodeURIComponent(window.activeProject) : '';
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
function getScopedWorldUrl(path) {
|
|
19
|
+
if (typeof window.scopedApiUrl === 'function') {
|
|
20
|
+
return window.scopedApiUrl(path, null, { includeBranch: false });
|
|
21
|
+
}
|
|
22
|
+
return path + getProjectParam();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function clonePlacementEntry(entry) {
|
|
26
|
+
if (!entry || typeof entry !== 'object' || !entry.type) return null;
|
|
27
|
+
return {
|
|
28
|
+
id: entry.id || generatePlacementId(),
|
|
29
|
+
type: entry.type,
|
|
30
|
+
x: entry.x,
|
|
31
|
+
y: entry.y || 0,
|
|
32
|
+
z: entry.z,
|
|
33
|
+
rotY: entry.rotY || 0,
|
|
34
|
+
placed_by: entry.placed_by || 'user',
|
|
35
|
+
timestamp: entry.timestamp || new Date().toISOString()
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
18
39
|
// --- Load world layout from server ---
|
|
19
40
|
export async function loadWorld() {
|
|
20
41
|
try {
|
|
21
|
-
var res = await fetch('/api/world-layout'
|
|
42
|
+
var res = await fetch(getScopedWorldUrl('/api/world-layout'));
|
|
22
43
|
if (res.ok) {
|
|
23
44
|
var data = await res.json();
|
|
24
45
|
_placements = Array.isArray(data) ? data : [];
|
|
@@ -38,7 +59,7 @@ function scheduleSave() {
|
|
|
38
59
|
if (_saveTimeout) clearTimeout(_saveTimeout);
|
|
39
60
|
_saveTimeout = setTimeout(function() {
|
|
40
61
|
_saveTimeout = null;
|
|
41
|
-
fetch('/api/world-save'
|
|
62
|
+
fetch(getScopedWorldUrl('/api/world-save'), {
|
|
42
63
|
method: 'POST',
|
|
43
64
|
headers: { 'Content-Type': 'application/json', 'X-LTT-Request': '1' },
|
|
44
65
|
body: JSON.stringify(_placements)
|
|
@@ -50,7 +71,7 @@ function scheduleSave() {
|
|
|
50
71
|
|
|
51
72
|
// --- Add a single placement ---
|
|
52
73
|
export function addPlacement(type, x, y, z, rotY, placedBy) {
|
|
53
|
-
var entry = {
|
|
74
|
+
var entry = clonePlacementEntry({
|
|
54
75
|
id: generatePlacementId(),
|
|
55
76
|
type: type,
|
|
56
77
|
x: x,
|
|
@@ -59,12 +80,22 @@ export function addPlacement(type, x, y, z, rotY, placedBy) {
|
|
|
59
80
|
rotY: rotY || 0,
|
|
60
81
|
placed_by: placedBy || 'user',
|
|
61
82
|
timestamp: new Date().toISOString()
|
|
62
|
-
};
|
|
83
|
+
});
|
|
63
84
|
_placements.push(entry);
|
|
64
85
|
scheduleSave();
|
|
65
86
|
return entry;
|
|
66
87
|
}
|
|
67
88
|
|
|
89
|
+
export function restorePlacement(entry) {
|
|
90
|
+
var restored = clonePlacementEntry(entry);
|
|
91
|
+
if (!restored) return null;
|
|
92
|
+
var idx = _placements.findIndex(function(p) { return p.id === restored.id; });
|
|
93
|
+
if (idx === -1) _placements.push(restored);
|
|
94
|
+
else _placements[idx] = restored;
|
|
95
|
+
scheduleSave();
|
|
96
|
+
return restored;
|
|
97
|
+
}
|
|
98
|
+
|
|
68
99
|
// --- Remove a placement by ID ---
|
|
69
100
|
export function removePlacement(id) {
|
|
70
101
|
var idx = _placements.findIndex(function(p) { return p.id === id; });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "let-them-talk",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.4.0",
|
|
4
4
|
"description": "MCP message broker + web dashboard for inter-agent communication. Let AI CLI agents talk to each other.",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"bin": {
|
|
@@ -10,17 +10,66 @@
|
|
|
10
10
|
"scripts": {
|
|
11
11
|
"start": "node server.js",
|
|
12
12
|
"dashboard": "node dashboard.js",
|
|
13
|
-
"
|
|
13
|
+
"export:markdown-workspace": "node scripts/export-markdown-workspace.js",
|
|
14
|
+
"sync:packaged-docs": "node scripts/sync-packaged-docs.js",
|
|
15
|
+
"prepack": "npm run sync:packaged-docs",
|
|
16
|
+
"test": "npm run verify",
|
|
17
|
+
"verify": "npm run verify:contracts && npm run verify:replay && npm run verify:invariants",
|
|
18
|
+
"verify:docs-onboarding": "node scripts/check-docs-onboarding.js",
|
|
19
|
+
"verify:contracts": "npm run verify:contracts:runtime && npm run verify:contracts:schema && npm run verify:contracts:branches && npm run verify:contracts:markdown-workspace",
|
|
20
|
+
"verify:contracts:runtime": "node scripts/check-runtime-contract.js",
|
|
21
|
+
"verify:contracts:schema": "node scripts/check-event-schema.js",
|
|
22
|
+
"verify:contracts:branches": "node scripts/check-branch-semantics.js",
|
|
23
|
+
"verify:contracts:markdown-workspace": "node scripts/check-markdown-workspace.js",
|
|
24
|
+
"verify:replay": "npm run verify:replay:positive && npm run verify:replay:negative",
|
|
25
|
+
"verify:replay:positive": "npm run verify:replay:healthy && npm run verify:replay:clean",
|
|
26
|
+
"verify:replay:healthy": "node scripts/check-message-replay.js --scenario healthy",
|
|
27
|
+
"verify:replay:clean": "node scripts/check-message-replay.js --scenario clean",
|
|
28
|
+
"verify:replay:negative": "node scripts/run-verification-suite.js replay-negative",
|
|
29
|
+
"verify:invariants": "npm run verify:invariants:authority && npm run verify:invariants:dashboard-control-plane && npm run verify:invariants:performance-indexing && npm run verify:invariants:capabilities && npm run verify:invariants:api-agent-parity && npm run verify:invariants:dashboard-semantic-gap && npm run verify:invariants:migration-hardening && npm run verify:invariants:branches && npm run verify:invariants:sessions && npm run verify:invariants:evidence && npm run verify:invariants:context && npm run verify:invariants:autonomy-v2 && npm run verify:invariants:autonomy-v2-watchdog && npm run verify:invariants:autonomy-v2-execution && npm run verify:invariants:agent-contracts && npm run verify:invariants:managed-team-integration && npm run verify:invariants:lifecycle-hooks && npm run verify:invariants:markdown-workspace-export && npm run verify:invariants:markdown-workspace-safety",
|
|
30
|
+
"verify:invariants:authority": "node scripts/check-invariants.js --suite authority",
|
|
31
|
+
"verify:invariants:dashboard-control-plane": "node scripts/check-dashboard-control-plane.js",
|
|
32
|
+
"verify:invariants:performance-indexing": "node scripts/check-performance-indexing.js",
|
|
33
|
+
"verify:invariants:capabilities": "node scripts/check-provider-capabilities.js",
|
|
34
|
+
"verify:invariants:api-agent-parity": "node scripts/check-api-agent-parity.js",
|
|
35
|
+
"verify:invariants:dashboard-semantic-gap": "node scripts/run-verification-suite.js dashboard-semantic-gap",
|
|
36
|
+
"verify:invariants:migration-hardening": "node scripts/check-migration-hardening.js",
|
|
37
|
+
"verify:invariants:branches": "node scripts/check-branch-isolation.js && node scripts/check-branch-fork-snapshot.js",
|
|
38
|
+
"verify:invariants:branch-fork": "node scripts/check-branch-fork-snapshot.js",
|
|
39
|
+
"verify:invariants:sessions": "node scripts/check-session-lifecycle.js",
|
|
40
|
+
"verify:invariants:evidence": "node scripts/check-evidence-completion.js",
|
|
41
|
+
"verify:invariants:context": "node scripts/check-session-aware-context.js",
|
|
42
|
+
"verify:invariants:autonomy-v2": "node scripts/check-autonomy-v2-decision.js",
|
|
43
|
+
"verify:invariants:autonomy-v2-watchdog": "node scripts/check-autonomy-v2-watchdog.js",
|
|
44
|
+
"verify:invariants:autonomy-v2-execution": "node scripts/check-autonomy-v2-execution.js",
|
|
45
|
+
"verify:invariants:agent-contracts": "node scripts/check-agent-contract-advisory.js",
|
|
46
|
+
"verify:invariants:managed-team-integration": "node scripts/check-managed-team-integration.js",
|
|
47
|
+
"verify:invariants:lifecycle-hooks": "node scripts/check-lifecycle-hooks.js",
|
|
48
|
+
"verify:invariants:markdown-workspace-export": "node scripts/check-markdown-workspace-export.js",
|
|
49
|
+
"verify:invariants:markdown-workspace-safety": "node scripts/check-markdown-workspace-safety.js",
|
|
50
|
+
"verify:smoke": "node scripts/run-verification-suite.js smoke"
|
|
14
51
|
},
|
|
15
52
|
"engines": {
|
|
16
53
|
"node": ">=18.0.0"
|
|
17
54
|
},
|
|
18
55
|
"files": [
|
|
56
|
+
"data-dir.js",
|
|
19
57
|
"server.js",
|
|
20
58
|
"dashboard.js",
|
|
21
59
|
"dashboard.html",
|
|
60
|
+
"api-agents.js",
|
|
61
|
+
"runtime-descriptor.js",
|
|
62
|
+
"agent-contracts.js",
|
|
63
|
+
"managed-team-integration.js",
|
|
64
|
+
"autonomy/",
|
|
65
|
+
"events/",
|
|
66
|
+
"state/",
|
|
67
|
+
"providers/",
|
|
22
68
|
"office/",
|
|
23
69
|
"mods/",
|
|
70
|
+
"scripts/",
|
|
71
|
+
"docs/",
|
|
72
|
+
"USAGE.md",
|
|
24
73
|
"cli.js",
|
|
25
74
|
"templates/",
|
|
26
75
|
"conversation-templates/",
|
|
@@ -53,7 +102,12 @@
|
|
|
53
102
|
"author": "Dekelelz <contact@talk.unrealai.studio>",
|
|
54
103
|
"license": "SEE LICENSE IN LICENSE",
|
|
55
104
|
"dependencies": {
|
|
56
|
-
"@modelcontextprotocol/sdk": "1.
|
|
105
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
57
106
|
"three": "0.175.0"
|
|
107
|
+
},
|
|
108
|
+
"overrides": {
|
|
109
|
+
"hono": "^4.12.14",
|
|
110
|
+
"path-to-regexp": "^8.4.2",
|
|
111
|
+
"@hono/node-server": "^1.19.14"
|
|
58
112
|
}
|
|
59
113
|
}
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
// ComfyUI provider — queue workflows via REST API, poll for results
|
|
2
|
+
// Supports text-to-image, image-to-video, and 3D generation
|
|
3
|
+
// Workflow templates have their prompt nodes auto-replaced with user input
|
|
4
|
+
|
|
5
|
+
const http = require('http');
|
|
6
|
+
const https = require('https');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
var DEFAULT_PORT = 8188;
|
|
11
|
+
|
|
12
|
+
class ComfyUIProvider {
|
|
13
|
+
constructor(options) {
|
|
14
|
+
options = options || {};
|
|
15
|
+
this.endpoint = options.endpoint || 'http://127.0.0.1:8188';
|
|
16
|
+
this.model = options.model || 'default';
|
|
17
|
+
this.name = 'comfyui';
|
|
18
|
+
this.color = '#ff6b35'; // orange
|
|
19
|
+
this.comfyPath = options.comfyPath || 'G:/ComfyUI';
|
|
20
|
+
this.workflowsDir = path.join(this.comfyPath, 'user/default/workflows');
|
|
21
|
+
this._workflows = null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// List available workflow files
|
|
25
|
+
listWorkflows() {
|
|
26
|
+
try {
|
|
27
|
+
var files = fs.readdirSync(this.workflowsDir).filter(function(f) { return f.endsWith('.json'); });
|
|
28
|
+
return files.map(function(f) {
|
|
29
|
+
return { name: f.replace('.json', ''), file: f };
|
|
30
|
+
});
|
|
31
|
+
} catch (e) {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Load a workflow JSON by name
|
|
37
|
+
_loadWorkflow(name) {
|
|
38
|
+
// Try exact match first
|
|
39
|
+
var filePath = path.join(this.workflowsDir, name + '.json');
|
|
40
|
+
if (!fs.existsSync(filePath)) {
|
|
41
|
+
// Try fuzzy match
|
|
42
|
+
var files = this.listWorkflows();
|
|
43
|
+
var match = files.find(function(f) {
|
|
44
|
+
return f.name.toLowerCase().indexOf(name.toLowerCase()) !== -1;
|
|
45
|
+
});
|
|
46
|
+
if (match) filePath = path.join(this.workflowsDir, match.file);
|
|
47
|
+
}
|
|
48
|
+
if (!fs.existsSync(filePath)) return null;
|
|
49
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Convert UI-format workflow to API-format (what /prompt expects)
|
|
53
|
+
_toApiFormat(workflow) {
|
|
54
|
+
if (!workflow.nodes) return workflow; // already API format
|
|
55
|
+
|
|
56
|
+
var api = {};
|
|
57
|
+
var nodes = workflow.nodes;
|
|
58
|
+
var links = workflow.links || [];
|
|
59
|
+
|
|
60
|
+
// Build link map: linkId → { fromNode, fromSlot, toNode, toSlot }
|
|
61
|
+
var linkMap = {};
|
|
62
|
+
for (var li = 0; li < links.length; li++) {
|
|
63
|
+
var link = links[li];
|
|
64
|
+
// link format: [linkId, fromNodeId, fromSlotIdx, toNodeId, toSlotIdx, type]
|
|
65
|
+
linkMap[link[0]] = { from: link[1], fromSlot: link[2], to: link[3], toSlot: link[4] };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (var ni = 0; ni < nodes.length; ni++) {
|
|
69
|
+
var node = nodes[ni];
|
|
70
|
+
if (!node.type || node.type === 'Note' || node.type === 'MarkdownNote') continue;
|
|
71
|
+
|
|
72
|
+
var inputs = {};
|
|
73
|
+
|
|
74
|
+
// Get input connections from node.inputs
|
|
75
|
+
if (node.inputs) {
|
|
76
|
+
for (var ii = 0; ii < node.inputs.length; ii++) {
|
|
77
|
+
var inp = node.inputs[ii];
|
|
78
|
+
if (inp.link != null && linkMap[inp.link]) {
|
|
79
|
+
var lk = linkMap[inp.link];
|
|
80
|
+
inputs[inp.name] = [String(lk.from), lk.fromSlot];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Get widget values — these are the non-connected inputs
|
|
86
|
+
if (node.widgets_values && node.widgets_values.length > 0) {
|
|
87
|
+
var widgetNames = this._getWidgetNames(node.type);
|
|
88
|
+
for (var wi = 0; wi < node.widgets_values.length && wi < widgetNames.length; wi++) {
|
|
89
|
+
var wName = widgetNames[wi];
|
|
90
|
+
if (!inputs[wName]) {
|
|
91
|
+
inputs[wName] = node.widgets_values[wi];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
api[String(node.id)] = {
|
|
97
|
+
class_type: node.type,
|
|
98
|
+
inputs: inputs,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return api;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Map common node types to their widget parameter names
|
|
106
|
+
_getWidgetNames(classType) {
|
|
107
|
+
var maps = {
|
|
108
|
+
'CLIPTextEncode': ['text'],
|
|
109
|
+
'KSampler': ['seed', 'control_after_generate', 'steps', 'cfg', 'sampler_name', 'scheduler', 'denoise'],
|
|
110
|
+
'KSamplerAdvanced': ['add_noise', 'noise_seed', 'control_after_generate', 'steps', 'cfg', 'sampler_name', 'scheduler', 'start_at_step', 'end_at_step', 'return_with_leftover_noise'],
|
|
111
|
+
'EmptySD3LatentImage': ['width', 'height', 'batch_size'],
|
|
112
|
+
'EmptyLatentImage': ['width', 'height', 'batch_size'],
|
|
113
|
+
'UnetLoaderGGUF': ['unet_name'],
|
|
114
|
+
'UNETLoader': ['unet_name', 'weight_dtype'],
|
|
115
|
+
'DualCLIPLoader': ['clip_name1', 'clip_name2', 'type'],
|
|
116
|
+
'CLIPLoader': ['clip_name', 'type'],
|
|
117
|
+
'VAELoader': ['vae_name'],
|
|
118
|
+
'VAEDecode': [],
|
|
119
|
+
'SaveImage': ['filename_prefix'],
|
|
120
|
+
'SaveVideo': ['output_path', 'filename_prefix'],
|
|
121
|
+
'LoadImage': ['image', 'upload'],
|
|
122
|
+
'ModelSamplingSD3': ['shift'],
|
|
123
|
+
'WanImageToVideo': ['width', 'height', 'length', 'batch_size'],
|
|
124
|
+
'CreateVideo': ['frame_rate'],
|
|
125
|
+
'UpscaleModelLoader': ['model_name'],
|
|
126
|
+
'ImageUpscaleWithModel': [],
|
|
127
|
+
'LoraLoaderModelOnly': ['lora_name', 'strength_model'],
|
|
128
|
+
};
|
|
129
|
+
return maps[classType] || [];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Inject prompt text into the workflow (replace CLIPTextEncode positive prompt)
|
|
133
|
+
_injectPrompt(apiWorkflow, prompt, negativePrompt) {
|
|
134
|
+
var positiveFound = false;
|
|
135
|
+
for (var nodeId in apiWorkflow) {
|
|
136
|
+
var node = apiWorkflow[nodeId];
|
|
137
|
+
if (node.class_type === 'CLIPTextEncode' && node.inputs) {
|
|
138
|
+
if (!positiveFound && node.inputs.text) {
|
|
139
|
+
// First CLIPTextEncode = positive prompt (unless it looks negative)
|
|
140
|
+
var existing = (node.inputs.text || '').toLowerCase();
|
|
141
|
+
if (existing.indexOf('blur') !== -1 || existing.indexOf('bad') !== -1 || existing.indexOf('ugly') !== -1 || existing.indexOf('overexposed') !== -1) {
|
|
142
|
+
// This is the negative prompt node — set negative if provided
|
|
143
|
+
if (negativePrompt) node.inputs.text = negativePrompt;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
node.inputs.text = prompt;
|
|
147
|
+
positiveFound = true;
|
|
148
|
+
} else if (positiveFound && negativePrompt) {
|
|
149
|
+
// Second CLIPTextEncode after positive = negative
|
|
150
|
+
node.inputs.text = negativePrompt;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return apiWorkflow;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async generate(prompt, options) {
|
|
158
|
+
options = options || {};
|
|
159
|
+
var workflowName = options.workflow || this.model || 'flux_text_to_image';
|
|
160
|
+
|
|
161
|
+
// Load workflow
|
|
162
|
+
var workflow = this._loadWorkflow(workflowName);
|
|
163
|
+
if (!workflow) {
|
|
164
|
+
throw new Error('Workflow not found: ' + workflowName + '. Available: ' + this.listWorkflows().map(function(w) { return w.name; }).join(', '));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Convert to API format and inject prompt
|
|
168
|
+
var apiWorkflow = this._toApiFormat(workflow);
|
|
169
|
+
this._injectPrompt(apiWorkflow, prompt, options.negativePrompt || '');
|
|
170
|
+
|
|
171
|
+
// Randomize seed
|
|
172
|
+
for (var nid in apiWorkflow) {
|
|
173
|
+
var n = apiWorkflow[nid];
|
|
174
|
+
if (n.inputs && (n.inputs.seed !== undefined || n.inputs.noise_seed !== undefined)) {
|
|
175
|
+
var seedKey = n.inputs.seed !== undefined ? 'seed' : 'noise_seed';
|
|
176
|
+
n.inputs[seedKey] = Math.floor(Math.random() * 2147483647);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Queue the prompt
|
|
181
|
+
var promptId = await this._queuePrompt(apiWorkflow);
|
|
182
|
+
if (!promptId) throw new Error('Failed to queue ComfyUI prompt');
|
|
183
|
+
|
|
184
|
+
// Poll for completion
|
|
185
|
+
var result = await this._waitForResult(promptId);
|
|
186
|
+
if (!result) throw new Error('ComfyUI generation timed out');
|
|
187
|
+
|
|
188
|
+
// Download the output
|
|
189
|
+
if (result.images && result.images.length > 0) {
|
|
190
|
+
var img = result.images[0];
|
|
191
|
+
var imageData = await this._downloadOutput(img.filename, img.subfolder, img.type);
|
|
192
|
+
return {
|
|
193
|
+
type: 'image',
|
|
194
|
+
data: imageData.toString('base64'),
|
|
195
|
+
format: img.filename.endsWith('.png') ? 'png' : 'jpg',
|
|
196
|
+
model: workflowName,
|
|
197
|
+
prompt: prompt,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (result.videos && result.videos.length > 0) {
|
|
202
|
+
var vid = result.videos[0];
|
|
203
|
+
var videoData = await this._downloadOutput(vid.filename, vid.subfolder, vid.type);
|
|
204
|
+
return {
|
|
205
|
+
type: 'image', // treat as image for now (thumbnail)
|
|
206
|
+
data: videoData.toString('base64'),
|
|
207
|
+
format: 'mp4',
|
|
208
|
+
model: workflowName,
|
|
209
|
+
prompt: prompt,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (result.gltf && result.gltf.length > 0) {
|
|
214
|
+
return {
|
|
215
|
+
type: 'text',
|
|
216
|
+
data: '3D model generated: ' + result.gltf[0].filename,
|
|
217
|
+
model: workflowName,
|
|
218
|
+
prompt: prompt,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
throw new Error('No output from ComfyUI');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
_queuePrompt(apiWorkflow) {
|
|
226
|
+
var url = new URL(this.endpoint);
|
|
227
|
+
var transport = url.protocol === 'https:' ? https : http;
|
|
228
|
+
var body = JSON.stringify({ prompt: apiWorkflow });
|
|
229
|
+
var self = this;
|
|
230
|
+
|
|
231
|
+
return new Promise(function(resolve, reject) {
|
|
232
|
+
var req = transport.request({
|
|
233
|
+
hostname: url.hostname,
|
|
234
|
+
port: url.port || DEFAULT_PORT,
|
|
235
|
+
path: '/prompt',
|
|
236
|
+
method: 'POST',
|
|
237
|
+
headers: { 'Content-Type': 'application/json' },
|
|
238
|
+
timeout: 30000,
|
|
239
|
+
}, function(res) {
|
|
240
|
+
var data = '';
|
|
241
|
+
res.on('data', function(c) { data += c; });
|
|
242
|
+
res.on('end', function() {
|
|
243
|
+
try {
|
|
244
|
+
var r = JSON.parse(data);
|
|
245
|
+
if (r.error) { reject(new Error('ComfyUI error: ' + (r.error.message || JSON.stringify(r.error)))); return; }
|
|
246
|
+
resolve(r.prompt_id || null);
|
|
247
|
+
} catch (e) { reject(new Error('Invalid ComfyUI response')); }
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
req.on('error', function(e) { reject(new Error('ComfyUI connection failed: ' + e.message + '. Is ComfyUI running?')); });
|
|
251
|
+
req.on('timeout', function() { req.destroy(); reject(new Error('ComfyUI queue timeout')); });
|
|
252
|
+
req.write(body);
|
|
253
|
+
req.end();
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
_waitForResult(promptId) {
|
|
258
|
+
var self = this;
|
|
259
|
+
var maxWait = 300000; // 5 minutes max
|
|
260
|
+
var pollInterval = 2000;
|
|
261
|
+
var elapsed = 0;
|
|
262
|
+
|
|
263
|
+
return new Promise(function(resolve, reject) {
|
|
264
|
+
var timer = setInterval(function() {
|
|
265
|
+
elapsed += pollInterval;
|
|
266
|
+
if (elapsed > maxWait) {
|
|
267
|
+
clearInterval(timer);
|
|
268
|
+
reject(new Error('ComfyUI generation timed out (5 min)'));
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
self._checkHistory(promptId).then(function(result) {
|
|
273
|
+
if (result) {
|
|
274
|
+
clearInterval(timer);
|
|
275
|
+
resolve(result);
|
|
276
|
+
}
|
|
277
|
+
}).catch(function() {
|
|
278
|
+
// ignore polling errors, keep trying
|
|
279
|
+
});
|
|
280
|
+
}, pollInterval);
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
_checkHistory(promptId) {
|
|
285
|
+
var url = new URL(this.endpoint);
|
|
286
|
+
var transport = url.protocol === 'https:' ? https : http;
|
|
287
|
+
|
|
288
|
+
return new Promise(function(resolve, reject) {
|
|
289
|
+
var req = transport.request({
|
|
290
|
+
hostname: url.hostname,
|
|
291
|
+
port: url.port || DEFAULT_PORT,
|
|
292
|
+
path: '/history/' + promptId,
|
|
293
|
+
method: 'GET',
|
|
294
|
+
timeout: 10000,
|
|
295
|
+
}, function(res) {
|
|
296
|
+
var data = '';
|
|
297
|
+
res.on('data', function(c) { data += c; });
|
|
298
|
+
res.on('end', function() {
|
|
299
|
+
try {
|
|
300
|
+
var history = JSON.parse(data);
|
|
301
|
+
var entry = history[promptId];
|
|
302
|
+
if (!entry || !entry.outputs) { resolve(null); return; }
|
|
303
|
+
|
|
304
|
+
// Collect outputs
|
|
305
|
+
var images = [];
|
|
306
|
+
var videos = [];
|
|
307
|
+
var gltf = [];
|
|
308
|
+
|
|
309
|
+
for (var nodeId in entry.outputs) {
|
|
310
|
+
var out = entry.outputs[nodeId];
|
|
311
|
+
if (out.images) images = images.concat(out.images);
|
|
312
|
+
if (out.videos) videos = videos.concat(out.videos);
|
|
313
|
+
if (out.gltf) gltf = gltf.concat(out.gltf);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (images.length > 0 || videos.length > 0 || gltf.length > 0) {
|
|
317
|
+
resolve({ images: images, videos: videos, gltf: gltf });
|
|
318
|
+
} else {
|
|
319
|
+
resolve(null); // not done yet
|
|
320
|
+
}
|
|
321
|
+
} catch (e) { resolve(null); }
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
req.on('error', function() { resolve(null); });
|
|
325
|
+
req.on('timeout', function() { req.destroy(); resolve(null); });
|
|
326
|
+
req.end();
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
_downloadOutput(filename, subfolder, type) {
|
|
331
|
+
var url = new URL(this.endpoint);
|
|
332
|
+
var transport = url.protocol === 'https:' ? https : http;
|
|
333
|
+
var queryPath = '/view?filename=' + encodeURIComponent(filename);
|
|
334
|
+
if (subfolder) queryPath += '&subfolder=' + encodeURIComponent(subfolder);
|
|
335
|
+
if (type) queryPath += '&type=' + encodeURIComponent(type);
|
|
336
|
+
|
|
337
|
+
return new Promise(function(resolve, reject) {
|
|
338
|
+
var req = transport.request({
|
|
339
|
+
hostname: url.hostname,
|
|
340
|
+
port: url.port || DEFAULT_PORT,
|
|
341
|
+
path: queryPath,
|
|
342
|
+
method: 'GET',
|
|
343
|
+
timeout: 60000,
|
|
344
|
+
}, function(res) {
|
|
345
|
+
var chunks = [];
|
|
346
|
+
res.on('data', function(c) { chunks.push(c); });
|
|
347
|
+
res.on('end', function() { resolve(Buffer.concat(chunks)); });
|
|
348
|
+
});
|
|
349
|
+
req.on('error', function(e) { reject(new Error('Failed to download ComfyUI output: ' + e.message)); });
|
|
350
|
+
req.on('timeout', function() { req.destroy(); reject(new Error('Download timeout')); });
|
|
351
|
+
req.end();
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async checkHealth() {
|
|
356
|
+
var url = new URL(this.endpoint);
|
|
357
|
+
var transport = url.protocol === 'https:' ? https : http;
|
|
358
|
+
return new Promise(function(resolve) {
|
|
359
|
+
var req = transport.request({
|
|
360
|
+
hostname: url.hostname,
|
|
361
|
+
port: url.port || DEFAULT_PORT,
|
|
362
|
+
path: '/system_stats',
|
|
363
|
+
method: 'GET',
|
|
364
|
+
timeout: 5000,
|
|
365
|
+
}, function(res) {
|
|
366
|
+
var data = '';
|
|
367
|
+
res.on('data', function(c) { data += c; });
|
|
368
|
+
res.on('end', function() { resolve(res.statusCode === 200); });
|
|
369
|
+
});
|
|
370
|
+
req.on('error', function() { resolve(false); });
|
|
371
|
+
req.on('timeout', function() { req.destroy(); resolve(false); });
|
|
372
|
+
req.end();
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async listModels() {
|
|
377
|
+
return this.listWorkflows().map(function(w) {
|
|
378
|
+
return { name: w.name, size: 'local', description: 'ComfyUI workflow' };
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
module.exports = { ComfyUIProvider };
|