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.
Files changed (166) hide show
  1. package/CHANGELOG.md +3 -1
  2. package/README.md +158 -592
  3. package/SECURITY.md +3 -3
  4. package/USAGE.md +151 -0
  5. package/agent-contracts.js +447 -0
  6. package/api-agents.js +760 -0
  7. package/autonomy/decision-v2.js +380 -0
  8. package/autonomy/watchdog-policy.js +572 -0
  9. package/cli.js +454 -298
  10. package/conversation-templates/autonomous-feature.json +83 -22
  11. package/conversation-templates/code-review.json +69 -21
  12. package/conversation-templates/debug-squad.json +69 -21
  13. package/conversation-templates/feature-build.json +69 -21
  14. package/conversation-templates/research-write.json +69 -21
  15. package/dashboard.html +3148 -174
  16. package/dashboard.js +823 -786
  17. package/data-dir.js +58 -0
  18. package/docs/architecture/branch-semantics.md +157 -0
  19. package/docs/architecture/canonical-event-schema.md +88 -0
  20. package/docs/architecture/markdown-workspace.md +183 -0
  21. package/docs/architecture/runtime-contract.md +459 -0
  22. package/docs/architecture/runtime-migration-hardening.md +64 -0
  23. package/events/hooks.js +154 -0
  24. package/events/log.js +457 -0
  25. package/events/replay.js +33 -0
  26. package/events/schema.js +432 -0
  27. package/managed-team-integration.js +261 -0
  28. package/office/agents.js +704 -597
  29. package/office/animation.js +1 -1
  30. package/office/assets/arcade-cabinet.js +141 -0
  31. package/office/assets/archway.js +77 -0
  32. package/office/assets/bar-counter.js +91 -0
  33. package/office/assets/bar-stool.js +71 -0
  34. package/office/assets/beanbag.js +64 -0
  35. package/office/assets/bench.js +99 -0
  36. package/office/assets/bollard.js +87 -0
  37. package/office/assets/cactus.js +100 -0
  38. package/office/assets/carpet-tile.js +46 -0
  39. package/office/assets/chair.js +123 -0
  40. package/office/assets/chandelier.js +107 -0
  41. package/office/assets/coffee-machine.js +95 -0
  42. package/office/assets/coffee-table.js +81 -0
  43. package/office/assets/column.js +95 -0
  44. package/office/assets/desk-lamp.js +102 -0
  45. package/office/assets/desk.js +76 -0
  46. package/office/assets/dining-table.js +105 -0
  47. package/office/assets/door.js +70 -0
  48. package/office/assets/dual-monitor.js +72 -0
  49. package/office/assets/fence.js +76 -0
  50. package/office/assets/filing-cabinet.js +111 -0
  51. package/office/assets/floor-lamp.js +69 -0
  52. package/office/assets/floor-tile.js +54 -0
  53. package/office/assets/flower-pot.js +76 -0
  54. package/office/assets/foosball.js +95 -0
  55. package/office/assets/fridge.js +99 -0
  56. package/office/assets/gaming-chair.js +154 -0
  57. package/office/assets/gaming-desk.js +105 -0
  58. package/office/assets/glass-door.js +72 -0
  59. package/office/assets/glass-wall.js +64 -0
  60. package/office/assets/half-wall.js +49 -0
  61. package/office/assets/hanging-plant.js +112 -0
  62. package/office/assets/index.js +151 -0
  63. package/office/assets/indoor-tree.js +90 -0
  64. package/office/assets/l-sofa.js +153 -0
  65. package/office/assets/marble-floor.js +64 -0
  66. package/office/assets/materials.js +40 -0
  67. package/office/assets/meeting-table.js +88 -0
  68. package/office/assets/microwave.js +94 -0
  69. package/office/assets/monitor.js +67 -0
  70. package/office/assets/neon-strip.js +73 -0
  71. package/office/assets/painting.js +84 -0
  72. package/office/assets/palm-tree.js +108 -0
  73. package/office/assets/pc-tower.js +91 -0
  74. package/office/assets/pendant-light.js +67 -0
  75. package/office/assets/ping-pong.js +114 -0
  76. package/office/assets/plant.js +72 -0
  77. package/office/assets/planter-box.js +95 -0
  78. package/office/assets/pool-table.js +94 -0
  79. package/office/assets/printer.js +113 -0
  80. package/office/assets/reception-desk.js +133 -0
  81. package/office/assets/rug.js +78 -0
  82. package/office/assets/sculpture.js +85 -0
  83. package/office/assets/server-rack.js +98 -0
  84. package/office/assets/sink.js +109 -0
  85. package/office/assets/sofa.js +106 -0
  86. package/office/assets/speaker.js +83 -0
  87. package/office/assets/spotlight.js +83 -0
  88. package/office/assets/street-lamp.js +97 -0
  89. package/office/assets/trash-can.js +83 -0
  90. package/office/assets/treadmill.js +126 -0
  91. package/office/assets/trophy.js +89 -0
  92. package/office/assets/tv-screen.js +79 -0
  93. package/office/assets/vase.js +84 -0
  94. package/office/assets/wall-clock.js +84 -0
  95. package/office/assets/wall.js +53 -0
  96. package/office/assets/water-cooler.js +146 -0
  97. package/office/assets/whiteboard.js +115 -0
  98. package/office/assets.js +3 -431
  99. package/office/builder.js +791 -355
  100. package/office/campus-env.js +1012 -1119
  101. package/office/environment.js +2 -0
  102. package/office/gallery.js +997 -0
  103. package/office/index.js +165 -61
  104. package/office/navigation.js +173 -152
  105. package/office/player.js +178 -68
  106. package/office/robot-character.js +272 -0
  107. package/office/spectator-camera.js +33 -10
  108. package/office/state.js +2 -0
  109. package/office/world-save.js +35 -4
  110. package/package.json +57 -3
  111. package/providers/comfyui.js +383 -0
  112. package/providers/dalle.js +79 -0
  113. package/providers/gemini.js +181 -0
  114. package/providers/ollama.js +184 -0
  115. package/providers/replicate.js +115 -0
  116. package/providers/zai.js +183 -0
  117. package/runtime-descriptor.js +270 -0
  118. package/scripts/check-agent-contract-advisory.js +132 -0
  119. package/scripts/check-api-agent-parity.js +277 -0
  120. package/scripts/check-autonomy-v2-decision.js +207 -0
  121. package/scripts/check-autonomy-v2-execution.js +588 -0
  122. package/scripts/check-autonomy-v2-watchdog.js +224 -0
  123. package/scripts/check-branch-fork-snapshot.js +337 -0
  124. package/scripts/check-branch-isolation.js +787 -0
  125. package/scripts/check-branch-semantics.js +139 -0
  126. package/scripts/check-dashboard-control-plane.js +1304 -0
  127. package/scripts/check-docs-onboarding.js +490 -0
  128. package/scripts/check-event-schema.js +276 -0
  129. package/scripts/check-evidence-completion.js +239 -0
  130. package/scripts/check-invariants.js +992 -0
  131. package/scripts/check-lifecycle-hooks.js +525 -0
  132. package/scripts/check-managed-team-integration.js +166 -0
  133. package/scripts/check-markdown-workspace-export.js +548 -0
  134. package/scripts/check-markdown-workspace-safety.js +347 -0
  135. package/scripts/check-markdown-workspace.js +136 -0
  136. package/scripts/check-message-replay.js +429 -0
  137. package/scripts/check-migration-hardening.js +300 -0
  138. package/scripts/check-performance-indexing.js +272 -0
  139. package/scripts/check-provider-capabilities.js +316 -0
  140. package/scripts/check-runtime-contract.js +109 -0
  141. package/scripts/check-session-aware-context.js +172 -0
  142. package/scripts/check-session-lifecycle.js +210 -0
  143. package/scripts/export-markdown-workspace.js +84 -0
  144. package/scripts/fixtures/message-replay/clean.jsonl +2 -0
  145. package/scripts/fixtures/message-replay/corrupt-correction-payload.jsonl +1 -0
  146. package/scripts/fixtures/message-replay/corrupt-jsonl.jsonl +1 -0
  147. package/scripts/fixtures/message-replay/corrupt-payload.jsonl +1 -0
  148. package/scripts/fixtures/message-replay/out-of-order.jsonl +2 -0
  149. package/scripts/migrate-legacy-to-canonical.js +201 -0
  150. package/scripts/run-verification-suite.js +242 -0
  151. package/scripts/sync-packaged-docs.js +69 -0
  152. package/server.js +9546 -7216
  153. package/state/agents.js +161 -0
  154. package/state/canonical.js +3068 -0
  155. package/state/dashboard-queries.js +441 -0
  156. package/state/evidence.js +56 -0
  157. package/state/io.js +69 -0
  158. package/state/markdown-workspace.js +951 -0
  159. package/state/messages.js +669 -0
  160. package/state/sessions.js +683 -0
  161. package/state/tasks-workflows.js +92 -0
  162. package/templates/debate.json +2 -2
  163. package/templates/managed.json +4 -4
  164. package/templates/pair.json +2 -2
  165. package/templates/review.json +2 -2
  166. 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 dx = e.clientX - lastMouse.x;
69
- var dy = e.clientY - lastMouse.y;
70
- lastMouse.x = e.clientX;
71
- lastMouse.y = e.clientY;
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 orientation
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 along camera look direction (projected on XZ for ground feel, or full 3D)
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
@@ -22,4 +22,6 @@ export const S = {
22
22
  monitorCanvases: {},
23
23
  fpsCounter: 0,
24
24
  fpsTime: 0,
25
+ galleryScreens: null,
26
+ cachedMedia: [],
25
27
  };
@@ -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' + getProjectParam());
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' + getProjectParam(), {
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.0",
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
- "test": "echo \"No tests configured\""
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.27.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 };