@documentation-robotics/cli 0.1.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 (169) hide show
  1. package/PYTHON_CLI_PARITY.md +265 -0
  2. package/PYTHON_CLI_STRUCTURE_UPDATE.md +246 -0
  3. package/README.md +926 -0
  4. package/biome.json +42 -0
  5. package/bunfig.toml +3 -0
  6. package/dist/ai/claude-client.js +71 -0
  7. package/dist/ai/claude-client.js.map +7 -0
  8. package/dist/ai/context-provider.js +129 -0
  9. package/dist/ai/context-provider.js.map +7 -0
  10. package/dist/ai/tools.js +231 -0
  11. package/dist/ai/tools.js.map +7 -0
  12. package/dist/cli.js +224 -0
  13. package/dist/cli.js.map +7 -0
  14. package/dist/commands/add.js +51 -0
  15. package/dist/commands/add.js.map +7 -0
  16. package/dist/commands/changeset.js +174 -0
  17. package/dist/commands/changeset.js.map +7 -0
  18. package/dist/commands/chat.js +111 -0
  19. package/dist/commands/chat.js.map +7 -0
  20. package/dist/commands/conformance.js +132 -0
  21. package/dist/commands/conformance.js.map +7 -0
  22. package/dist/commands/delete.js +31 -0
  23. package/dist/commands/delete.js.map +7 -0
  24. package/dist/commands/element.js +28 -0
  25. package/dist/commands/element.js.map +7 -0
  26. package/dist/commands/export.js +65 -0
  27. package/dist/commands/export.js.map +7 -0
  28. package/dist/commands/info.js +40 -0
  29. package/dist/commands/info.js.map +7 -0
  30. package/dist/commands/init.js +45 -0
  31. package/dist/commands/init.js.map +7 -0
  32. package/dist/commands/list.js +32 -0
  33. package/dist/commands/list.js.map +7 -0
  34. package/dist/commands/migrate.js +36 -0
  35. package/dist/commands/migrate.js.map +7 -0
  36. package/dist/commands/project.js +74 -0
  37. package/dist/commands/project.js.map +7 -0
  38. package/dist/commands/relationship.js +118 -0
  39. package/dist/commands/relationship.js.map +7 -0
  40. package/dist/commands/search.js +48 -0
  41. package/dist/commands/search.js.map +7 -0
  42. package/dist/commands/show.js +38 -0
  43. package/dist/commands/show.js.map +7 -0
  44. package/dist/commands/trace.js +60 -0
  45. package/dist/commands/trace.js.map +7 -0
  46. package/dist/commands/update.js +37 -0
  47. package/dist/commands/update.js.map +7 -0
  48. package/dist/commands/upgrade.js +152 -0
  49. package/dist/commands/upgrade.js.map +7 -0
  50. package/dist/commands/validate.js +71 -0
  51. package/dist/commands/validate.js.map +7 -0
  52. package/dist/commands/version.js +20 -0
  53. package/dist/commands/version.js.map +7 -0
  54. package/dist/commands/visualize.js +51 -0
  55. package/dist/commands/visualize.js.map +7 -0
  56. package/dist/core/active-changeset.js +52 -0
  57. package/dist/core/active-changeset.js.map +7 -0
  58. package/dist/core/changeset.js +244 -0
  59. package/dist/core/changeset.js.map +7 -0
  60. package/dist/core/dependency-tracker.js +322 -0
  61. package/dist/core/dependency-tracker.js.map +7 -0
  62. package/dist/core/element.js +65 -0
  63. package/dist/core/element.js.map +7 -0
  64. package/dist/core/layer.js +75 -0
  65. package/dist/core/layer.js.map +7 -0
  66. package/dist/core/manifest.js +57 -0
  67. package/dist/core/manifest.js.map +7 -0
  68. package/dist/core/migration-registry.js +131 -0
  69. package/dist/core/migration-registry.js.map +7 -0
  70. package/dist/core/model.js +455 -0
  71. package/dist/core/model.js.map +7 -0
  72. package/dist/core/projection-engine.js +288 -0
  73. package/dist/core/projection-engine.js.map +7 -0
  74. package/dist/core/reference-registry.js +188 -0
  75. package/dist/core/reference-registry.js.map +7 -0
  76. package/dist/core/relationship-registry.js +114 -0
  77. package/dist/core/relationship-registry.js.map +7 -0
  78. package/dist/core/relationships.js +78 -0
  79. package/dist/core/relationships.js.map +7 -0
  80. package/dist/export/archimate-exporter.js +111 -0
  81. package/dist/export/archimate-exporter.js.map +7 -0
  82. package/dist/export/export-manager.js +63 -0
  83. package/dist/export/export-manager.js.map +7 -0
  84. package/dist/export/graphml-exporter.js +39 -0
  85. package/dist/export/graphml-exporter.js.map +7 -0
  86. package/dist/export/index.js +17 -0
  87. package/dist/export/index.js.map +7 -0
  88. package/dist/export/json-schema-exporter.js +60 -0
  89. package/dist/export/json-schema-exporter.js.map +7 -0
  90. package/dist/export/markdown-exporter.js +99 -0
  91. package/dist/export/markdown-exporter.js.map +7 -0
  92. package/dist/export/openapi-exporter.js +85 -0
  93. package/dist/export/openapi-exporter.js.map +7 -0
  94. package/dist/export/plantuml-exporter.js +60 -0
  95. package/dist/export/plantuml-exporter.js.map +7 -0
  96. package/dist/export/types.js +36 -0
  97. package/dist/export/types.js.map +7 -0
  98. package/dist/schemas/bundled/01-motivation-layer.schema.json +942 -0
  99. package/dist/schemas/bundled/02-business-layer.schema.json +1293 -0
  100. package/dist/schemas/bundled/03-security-layer.schema.json +2177 -0
  101. package/dist/schemas/bundled/04-application-layer.schema.json +1328 -0
  102. package/dist/schemas/bundled/05-technology-layer.schema.json +1465 -0
  103. package/dist/schemas/bundled/06-api-layer.schema.json +1743 -0
  104. package/dist/schemas/bundled/07-data-model-layer.schema.json +1253 -0
  105. package/dist/schemas/bundled/08-datastore-layer.schema.json +892 -0
  106. package/dist/schemas/bundled/09-ux-layer.schema.json +1506 -0
  107. package/dist/schemas/bundled/10-navigation-layer.schema.json +843 -0
  108. package/dist/schemas/bundled/11-apm-observability-layer.schema.json +1430 -0
  109. package/dist/schemas/bundled/12-testing-layer.schema.json +1091 -0
  110. package/dist/schemas/bundled/common/layer-extensions.schema.json +266 -0
  111. package/dist/schemas/bundled/common/predicates.schema.json +493 -0
  112. package/dist/schemas/bundled/common/relationships.schema.json +791 -0
  113. package/dist/schemas/bundled/common/source-references.schema.json +71 -0
  114. package/dist/schemas/bundled/link-registry.json +1839 -0
  115. package/dist/schemas/bundled/link-registry.schema.json +339 -0
  116. package/dist/schemas/bundled/relationship-catalog.json +1302 -0
  117. package/dist/schemas/bundled/relationship-type.schema.json +221 -0
  118. package/dist/server/server.js +1207 -0
  119. package/dist/server/server.js.map +7 -0
  120. package/dist/server/telemetry-middleware.js +24 -0
  121. package/dist/server/telemetry-middleware.js.map +7 -0
  122. package/dist/telemetry/console-interceptor.js +34 -0
  123. package/dist/telemetry/console-interceptor.js.map +7 -0
  124. package/dist/telemetry/index.js +54 -0
  125. package/dist/telemetry/index.js.map +7 -0
  126. package/dist/telemetry/resilient-exporter.js +41 -0
  127. package/dist/telemetry/resilient-exporter.js.map +7 -0
  128. package/dist/telemetry/resilient-log-exporter.js +41 -0
  129. package/dist/telemetry/resilient-log-exporter.js.map +7 -0
  130. package/dist/telemetry/test-instrumentation.js +32 -0
  131. package/dist/telemetry/test-instrumentation.js.map +7 -0
  132. package/dist/types/index.js +1 -0
  133. package/dist/types/index.js.map +7 -0
  134. package/dist/utils/element-utils.js +11 -0
  135. package/dist/utils/element-utils.js.map +7 -0
  136. package/dist/utils/errors.js +105 -0
  137. package/dist/utils/errors.js.map +7 -0
  138. package/dist/utils/file-io.js +69 -0
  139. package/dist/utils/file-io.js.map +7 -0
  140. package/dist/utils/globals.js +28 -0
  141. package/dist/utils/globals.js.map +7 -0
  142. package/dist/utils/model-path.js +22 -0
  143. package/dist/utils/model-path.js.map +7 -0
  144. package/dist/utils/project-paths.js +45 -0
  145. package/dist/utils/project-paths.js.map +7 -0
  146. package/dist/utils/spec-installer.js +85 -0
  147. package/dist/utils/spec-installer.js.map +7 -0
  148. package/dist/utils/spec-version.js +33 -0
  149. package/dist/utils/spec-version.js.map +7 -0
  150. package/dist/validators/index.js +15 -0
  151. package/dist/validators/index.js.map +7 -0
  152. package/dist/validators/naming-validator.js +82 -0
  153. package/dist/validators/naming-validator.js.map +7 -0
  154. package/dist/validators/reference-validator.js +106 -0
  155. package/dist/validators/reference-validator.js.map +7 -0
  156. package/dist/validators/schema-validator.js +148 -0
  157. package/dist/validators/schema-validator.js.map +7 -0
  158. package/dist/validators/semantic-validator.js +80 -0
  159. package/dist/validators/semantic-validator.js.map +7 -0
  160. package/dist/validators/types.js +48 -0
  161. package/dist/validators/types.js.map +7 -0
  162. package/dist/validators/validator.js +67 -0
  163. package/dist/validators/validator.js.map +7 -0
  164. package/docs/TEST-INSTRUMENTATION.md +405 -0
  165. package/docs/telemetry.md +427 -0
  166. package/package.json +70 -0
  167. package/test-debug.js +33 -0
  168. package/test-special.mjs +46 -0
  169. package/test_runner.ts +7 -0
@@ -0,0 +1,1207 @@
1
+ import { Hono } from "hono";
2
+ import { upgradeWebSocket, websocket } from "hono/bun";
3
+ import { cors } from "hono/cors";
4
+ import { serve } from "bun";
5
+ import { Model } from "../core/model.js";
6
+ import { telemetryMiddleware } from "./telemetry-middleware.js";
7
+ class VisualizationServer {
8
+ app;
9
+ model;
10
+ _server;
11
+ clients = /* @__PURE__ */ new Set();
12
+ watcher;
13
+ annotations = /* @__PURE__ */ new Map();
14
+ // annotationId -> annotation
15
+ annotationsByElement = /* @__PURE__ */ new Map();
16
+ // elementId -> Set<annotationId>
17
+ changesets = /* @__PURE__ */ new Map();
18
+ // changesetId -> changeset
19
+ authToken;
20
+ authEnabled = !0;
21
+ // Enabled by default for security
22
+ activeChatProcesses = /* @__PURE__ */ new Map();
23
+ // conversationId -> Bun.spawn process
24
+ chatConversationCounter = 0;
25
+ constructor(model, options) {
26
+ this.app = new Hono(), this.model = model, this.authEnabled = options?.authEnabled ?? process.env.DR_AUTH_ENABLED !== "false", this.authToken = options?.authToken || process.env.DR_AUTH_TOKEN || this.generateAuthToken(), this.app.use("/*", cors()), this.app.use("/*", telemetryMiddleware), this.app.use("/api/*", async (c, next) => {
27
+ if (!this.authEnabled)
28
+ return next();
29
+ const authHeader = c.req.header("Authorization"), queryToken = c.req.query("token");
30
+ let providedToken = null;
31
+ return authHeader && authHeader.startsWith("Bearer ") ? providedToken = authHeader.substring(7) : queryToken && (providedToken = queryToken), providedToken ? providedToken !== this.authToken ? c.json({ error: "Invalid authentication token" }, 403) : next() : c.json({ error: "Authentication required. Please provide a valid token." }, 401);
32
+ }), this.setupRoutes(), this.authEnabled && process.env.VERBOSE && console.log(`[Auth] Authentication enabled. Token: ${this.authToken}`);
33
+ }
34
+ /**
35
+ * Generate a random auth token
36
+ */
37
+ generateAuthToken() {
38
+ return `dr-${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
39
+ }
40
+ /**
41
+ * Setup HTTP routes and WebSocket handler
42
+ */
43
+ setupRoutes() {
44
+ this.app.get("/", (c) => c.html(this.getViewerHTML())), this.app.get("/health", (c) => c.json({
45
+ status: "ok",
46
+ version: "0.1.0"
47
+ })), this.app.get("/api/model", async (c) => {
48
+ try {
49
+ const modelData = await this.serializeModel();
50
+ return c.json(modelData);
51
+ } catch (error) {
52
+ const message = error instanceof Error ? error.message : String(error);
53
+ return c.json({ error: message }, 500);
54
+ }
55
+ }), this.app.get("/api/layers/:name", async (c) => {
56
+ try {
57
+ const layerName = c.req.param("name"), layer = await this.model.getLayer(layerName);
58
+ if (!layer)
59
+ return c.json({ error: "Layer not found" }, 404);
60
+ const elements = layer.listElements().map((e) => e.toJSON());
61
+ return c.json({
62
+ name: layerName,
63
+ elements,
64
+ elementCount: elements.length
65
+ });
66
+ } catch (error) {
67
+ const message = error instanceof Error ? error.message : String(error);
68
+ return c.json({ error: message }, 500);
69
+ }
70
+ }), this.app.get("/api/elements/:id", async (c) => {
71
+ try {
72
+ const elementId = c.req.param("id"), element = await this.findElement(elementId);
73
+ return element ? c.json({
74
+ ...element.toJSON(),
75
+ annotations: this.annotations.get(elementId) || []
76
+ }) : c.json({ error: "Element not found" }, 404);
77
+ } catch (error) {
78
+ const message = error instanceof Error ? error.message : String(error);
79
+ return c.json({ error: message }, 500);
80
+ }
81
+ }), this.app.get("/api/spec", async (c) => {
82
+ try {
83
+ const schemas = await this.loadSchemas();
84
+ return c.json({
85
+ version: "0.1.0",
86
+ type: "schema-collection",
87
+ description: "JSON Schema definitions from dr CLI",
88
+ source: "dr-cli",
89
+ schemas,
90
+ schemaCount: Object.keys(schemas).length
91
+ });
92
+ } catch (error) {
93
+ const message = error instanceof Error ? error.message : String(error);
94
+ return c.json({ error: message }, 500);
95
+ }
96
+ }), this.app.get("/api/link-registry", async (c) => {
97
+ try {
98
+ const linkRegistry = await this.loadLinkRegistry();
99
+ return c.json(linkRegistry);
100
+ } catch (error) {
101
+ const message = error instanceof Error ? error.message : String(error);
102
+ return c.json({ error: message }, 404);
103
+ }
104
+ }), this.app.get("/api/annotations", (c) => {
105
+ const elementId = c.req.query("elementId");
106
+ if (elementId) {
107
+ const annotationIds = this.annotationsByElement.get(elementId) || /* @__PURE__ */ new Set(), annotations = Array.from(annotationIds).map((id) => this.annotations.get(id)).filter((a) => a !== void 0);
108
+ return c.json({ annotations });
109
+ } else {
110
+ const annotations = Array.from(this.annotations.values());
111
+ return c.json({ annotations });
112
+ }
113
+ }), this.app.post("/api/annotations", async (c) => {
114
+ try {
115
+ const body = await c.req.json();
116
+ if (!body.elementId || !body.content || !body.author)
117
+ return c.json({
118
+ error: "Missing required fields: elementId, content, author"
119
+ }, 400);
120
+ if (!await this.findElement(body.elementId))
121
+ return c.json({ error: "Element not found" }, 404);
122
+ const annotation = {
123
+ id: this.generateAnnotationId(),
124
+ elementId: String(body.elementId),
125
+ author: String(body.author),
126
+ content: String(body.content),
127
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
128
+ tags: body.tags || []
129
+ };
130
+ return this.annotations.set(annotation.id, annotation), this.annotationsByElement.has(annotation.elementId) || this.annotationsByElement.set(annotation.elementId, /* @__PURE__ */ new Set()), this.annotationsByElement.get(annotation.elementId).add(annotation.id), await this.broadcastMessage({
131
+ type: "annotation.added",
132
+ annotationId: annotation.id,
133
+ elementId: annotation.elementId,
134
+ timestamp: annotation.createdAt
135
+ }), c.json(annotation, 201);
136
+ } catch (error) {
137
+ const message = error instanceof Error ? error.message : String(error);
138
+ return c.json({ error: message }, 500);
139
+ }
140
+ }), this.app.put("/api/annotations/:annotationId", async (c) => {
141
+ try {
142
+ const annotationId = c.req.param("annotationId"), annotation = this.annotations.get(annotationId);
143
+ if (!annotation)
144
+ return c.json({ error: "Annotation not found" }, 404);
145
+ const body = await c.req.json();
146
+ return body.content !== void 0 && (annotation.content = String(body.content)), body.tags !== void 0 && (annotation.tags = body.tags), annotation.updatedAt = (/* @__PURE__ */ new Date()).toISOString(), await this.broadcastMessage({
147
+ type: "annotation.updated",
148
+ annotationId: annotation.id,
149
+ timestamp: annotation.updatedAt
150
+ }), c.json(annotation);
151
+ } catch (error) {
152
+ const message = error instanceof Error ? error.message : String(error);
153
+ return c.json({ error: message }, 500);
154
+ }
155
+ }), this.app.delete("/api/annotations/:annotationId", async (c) => {
156
+ try {
157
+ const annotationId = c.req.param("annotationId"), annotation = this.annotations.get(annotationId);
158
+ if (!annotation)
159
+ return c.json({ error: "Annotation not found" }, 404);
160
+ this.annotations.delete(annotationId);
161
+ const elementAnnotations = this.annotationsByElement.get(annotation.elementId);
162
+ return elementAnnotations && elementAnnotations.delete(annotationId), await this.broadcastMessage({
163
+ type: "annotation.deleted",
164
+ annotationId,
165
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
166
+ }), c.body(null, 204);
167
+ } catch (error) {
168
+ const message = error instanceof Error ? error.message : String(error);
169
+ return c.json({ error: message }, 500);
170
+ }
171
+ }), this.app.get("/api/changesets", (c) => {
172
+ const changesets = {};
173
+ for (const [id, changeset] of this.changesets)
174
+ changesets[id] = {
175
+ name: changeset.metadata.name,
176
+ status: changeset.metadata.status,
177
+ type: changeset.metadata.type,
178
+ created_at: changeset.metadata.created_at,
179
+ elements_count: changeset.changes.changes.length
180
+ };
181
+ return c.json({
182
+ version: "1.0.0",
183
+ changesets
184
+ });
185
+ }), this.app.get("/api/changesets/:changesetId", (c) => {
186
+ const changesetId = c.req.param("changesetId"), changeset = this.changesets.get(changesetId);
187
+ return changeset ? c.json(changeset) : c.json({ error: "Changeset not found" }, 404);
188
+ }), this.app.get("/api/elements/:id/annotations", (c) => {
189
+ const elementId = c.req.param("id"), annotationIds = this.annotationsByElement.get(elementId) || /* @__PURE__ */ new Set(), annotations = Array.from(annotationIds).map((id) => this.annotations.get(id)).filter((a) => a !== void 0);
190
+ return c.json({ elementId, annotations });
191
+ }), this.app.post("/api/elements/:id/annotations", async (c) => {
192
+ try {
193
+ const elementId = c.req.param("id");
194
+ if (!await this.findElement(elementId))
195
+ return c.json({ error: "Element not found" }, 404);
196
+ const body = await c.req.json(), content = (body.content || body.text) && String(body.content || body.text).trim();
197
+ if (!content)
198
+ return c.json({ error: "Annotation content cannot be empty" }, 400);
199
+ const author = body.author ? String(body.author).trim() : "Anonymous", annotation = {
200
+ id: this.generateAnnotationId(),
201
+ elementId,
202
+ author,
203
+ content,
204
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
205
+ tags: body.tags || []
206
+ };
207
+ return this.annotations.set(annotation.id, annotation), this.annotationsByElement.has(elementId) || this.annotationsByElement.set(elementId, /* @__PURE__ */ new Set()), this.annotationsByElement.get(elementId).add(annotation.id), await this.broadcastMessage({
208
+ type: "annotation.added",
209
+ annotationId: annotation.id,
210
+ elementId: annotation.elementId,
211
+ timestamp: annotation.createdAt
212
+ }), c.json({ success: !0, annotation });
213
+ } catch (error) {
214
+ const message = error instanceof Error ? error.message : String(error);
215
+ return c.json({ error: message }, 500);
216
+ }
217
+ }), this.app.get("/ws", async (c, next) => {
218
+ if (this.authEnabled) {
219
+ const queryToken = c.req.query("token");
220
+ if (!queryToken)
221
+ return c.json({ error: "Authentication required. Provide token as query parameter: ?token=YOUR_TOKEN" }, 401);
222
+ if (queryToken !== this.authToken)
223
+ return c.json({ error: "Invalid authentication token" }, 403);
224
+ }
225
+ return next();
226
+ }, upgradeWebSocket(() => ({
227
+ onOpen: (_evt, ws) => {
228
+ this.clients.add(ws), ws.send(JSON.stringify({
229
+ type: "connected",
230
+ version: "0.1.0",
231
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
232
+ })), process.env.VERBOSE && console.log(`[WebSocket] Client connected (total: ${this.clients.size})`);
233
+ },
234
+ onClose: (_evt, ws) => {
235
+ this.clients.delete(ws), process.env.VERBOSE && console.log(`[WebSocket] Client disconnected (total: ${this.clients.size})`);
236
+ },
237
+ onMessage: async (message, ws) => {
238
+ try {
239
+ const rawData = message.data ?? message;
240
+ let msgStr;
241
+ typeof rawData == "string" ? msgStr = rawData : rawData instanceof Buffer || rawData instanceof Uint8Array ? msgStr = new TextDecoder().decode(rawData) : msgStr = String(rawData);
242
+ const data = JSON.parse(msgStr);
243
+ data.jsonrpc === "2.0" ? await this.handleJSONRPCMessage(ws, data) : await this.handleWSMessage(ws, data);
244
+ } catch (error) {
245
+ const errorMsg = error instanceof Error ? error.message : String(error);
246
+ process.env.DEBUG && console.error(`[WebSocket] Error handling message: ${errorMsg}`), ws.send(JSON.stringify({
247
+ type: "error",
248
+ message: errorMsg
249
+ }));
250
+ }
251
+ },
252
+ onError: (error) => {
253
+ const message = error instanceof Error ? error.message : String(error);
254
+ console.error(`[WebSocket] Error: ${message}`);
255
+ }
256
+ })));
257
+ }
258
+ /**
259
+ * Serialize model to JSON for client consumption
260
+ */
261
+ async serializeModel() {
262
+ const layers = {};
263
+ for (const [name, layer] of this.model.layers) {
264
+ const elements = layer.listElements().map((e) => {
265
+ const annotationIds = this.annotationsByElement.get(e.id) || /* @__PURE__ */ new Set(), annotations = Array.from(annotationIds).map((id) => this.annotations.get(id)).filter((a) => a !== void 0);
266
+ return {
267
+ ...e.toJSON(),
268
+ annotations
269
+ };
270
+ });
271
+ layers[name] = {
272
+ name,
273
+ elements,
274
+ elementCount: elements.length
275
+ };
276
+ }
277
+ return {
278
+ manifest: this.model.manifest.toJSON(),
279
+ layers,
280
+ totalElements: Array.from(this.model.layers.values()).reduce((sum, layer) => sum + layer.listElements().length, 0)
281
+ };
282
+ }
283
+ /**
284
+ * Generate unique annotation ID
285
+ */
286
+ generateAnnotationId() {
287
+ return `ann-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
288
+ }
289
+ /**
290
+ * Broadcast message to all connected WebSocket clients
291
+ */
292
+ async broadcastMessage(message) {
293
+ const messageStr = JSON.stringify(message);
294
+ let failureCount = 0;
295
+ for (const client of this.clients)
296
+ try {
297
+ client.send(messageStr);
298
+ } catch (error) {
299
+ failureCount++;
300
+ const msg = error instanceof Error ? error.message : String(error);
301
+ process.env.VERBOSE && console.warn(`[WebSocket] Failed to send message to client: ${msg}`);
302
+ }
303
+ failureCount > 0 && process.env.VERBOSE && console.warn(`[WebSocket] Failed to send message to ${failureCount}/${this.clients.size} clients`);
304
+ }
305
+ /**
306
+ * Find an element across all layers
307
+ */
308
+ async findElement(id) {
309
+ for (const layer of this.model.layers.values()) {
310
+ const element = layer.getElement(id);
311
+ if (element)
312
+ return element;
313
+ }
314
+ return null;
315
+ }
316
+ /**
317
+ * Handle WebSocket messages
318
+ */
319
+ async handleWSMessage(ws, data) {
320
+ switch (data.type) {
321
+ case "subscribe":
322
+ const topics = data.topics || ["model", "annotations"];
323
+ ws.send(JSON.stringify({
324
+ type: "subscribed",
325
+ topics,
326
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
327
+ }));
328
+ const modelData = await this.serializeModel();
329
+ ws.send(JSON.stringify({
330
+ type: "model",
331
+ data: modelData,
332
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
333
+ }));
334
+ break;
335
+ case "ping":
336
+ ws.send(JSON.stringify({
337
+ type: "pong",
338
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
339
+ }));
340
+ break;
341
+ case "annotate":
342
+ data.annotation && await this.broadcastMessage({
343
+ type: "annotation",
344
+ data: data.annotation
345
+ });
346
+ break;
347
+ default:
348
+ ws.send(JSON.stringify({
349
+ type: "error",
350
+ message: `Unknown message type: ${data.type}`
351
+ }));
352
+ }
353
+ }
354
+ /**
355
+ * Handle JSON-RPC 2.0 messages for chat functionality
356
+ */
357
+ async handleJSONRPCMessage(ws, data) {
358
+ const { method, params, id } = data, sendResponse = (result) => {
359
+ ws.send(JSON.stringify({
360
+ jsonrpc: "2.0",
361
+ result,
362
+ id
363
+ }));
364
+ }, sendError = (code, message, data2) => {
365
+ ws.send(JSON.stringify({
366
+ jsonrpc: "2.0",
367
+ error: { code, message, data: data2 },
368
+ id
369
+ }));
370
+ };
371
+ try {
372
+ switch (method) {
373
+ case "chat.status":
374
+ const claudeAvailable = await this.checkClaudeCodeCLI();
375
+ sendResponse({
376
+ sdk_available: claudeAvailable,
377
+ sdk_version: claudeAvailable ? "claude-code-cli" : null,
378
+ error_message: claudeAvailable ? null : "Claude Code CLI not found in PATH"
379
+ });
380
+ break;
381
+ case "chat.send":
382
+ if (!params || !params.message) {
383
+ sendError(-32602, "Message cannot be empty");
384
+ return;
385
+ }
386
+ if (!await this.checkClaudeCodeCLI()) {
387
+ sendError(-32001, "Claude Code CLI not available. Install Claude Code to enable chat.");
388
+ return;
389
+ }
390
+ const conversationId = `conv-${++this.chatConversationCounter}-${Date.now()}`;
391
+ await this.launchClaudeCodeChat(ws, conversationId, params.message, id);
392
+ break;
393
+ case "chat.cancel":
394
+ let cancelled = !1, cancelledConvId = null;
395
+ for (const [convId, process2] of this.activeChatProcesses)
396
+ try {
397
+ process2.kill(), this.activeChatProcesses.delete(convId), cancelled = !0, cancelledConvId = convId;
398
+ break;
399
+ } catch {
400
+ }
401
+ sendResponse({
402
+ cancelled,
403
+ conversation_id: cancelledConvId
404
+ });
405
+ break;
406
+ default:
407
+ sendError(-32601, `Method not found: ${method}`);
408
+ }
409
+ } catch (error) {
410
+ const errorMsg = error instanceof Error ? error.message : String(error);
411
+ sendError(-32603, "Internal error", errorMsg);
412
+ }
413
+ }
414
+ /**
415
+ * Check if Claude Code CLI is available
416
+ */
417
+ async checkClaudeCodeCLI() {
418
+ try {
419
+ return Bun.spawnSync({
420
+ cmd: ["which", "claude"],
421
+ stdout: "pipe",
422
+ stderr: "pipe"
423
+ }).exitCode === 0;
424
+ } catch {
425
+ return !1;
426
+ }
427
+ }
428
+ /**
429
+ * Launch Claude Code CLI with dr-architect agent and stream responses
430
+ */
431
+ async launchClaudeCodeChat(ws, conversationId, message, requestId) {
432
+ try {
433
+ const proc = Bun.spawn({
434
+ cmd: [
435
+ "claude",
436
+ "--agent",
437
+ "dr-architect",
438
+ "--print",
439
+ "--dangerously-skip-permissions",
440
+ "--verbose",
441
+ "--output-format",
442
+ "stream-json"
443
+ ],
444
+ cwd: this.model.rootPath,
445
+ stdin: "pipe",
446
+ stdout: "pipe",
447
+ stderr: "pipe"
448
+ });
449
+ this.activeChatProcesses.set(conversationId, proc), proc.stdin?.write(new TextEncoder().encode(message)), proc.stdin?.end();
450
+ const stdoutReader = proc.stdout.getReader(), decoder = new TextDecoder();
451
+ (async () => {
452
+ let accumulatedText = "", buffer = "";
453
+ try {
454
+ for (; ; ) {
455
+ const { done, value } = await stdoutReader.read();
456
+ if (done)
457
+ break;
458
+ const chunk = decoder.decode(value, { stream: !0 });
459
+ buffer += chunk;
460
+ const lines = buffer.split(`
461
+ `);
462
+ buffer = lines.pop() || "";
463
+ for (const line of lines)
464
+ if (line.trim())
465
+ try {
466
+ const event = JSON.parse(line);
467
+ if (event.type === "assistant") {
468
+ const content = event.message?.content || [];
469
+ for (const block of content)
470
+ block.type === "text" ? (accumulatedText += block.text, ws.send(JSON.stringify({
471
+ jsonrpc: "2.0",
472
+ method: "chat.response.chunk",
473
+ params: {
474
+ conversation_id: conversationId,
475
+ content: block.text,
476
+ is_final: !1,
477
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
478
+ }
479
+ }))) : block.type === "tool_use" && ws.send(JSON.stringify({
480
+ jsonrpc: "2.0",
481
+ method: "chat.tool.invoke",
482
+ params: {
483
+ conversation_id: conversationId,
484
+ tool_name: block.name,
485
+ tool_input: block.input,
486
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
487
+ }
488
+ }));
489
+ } else event.type === "result" && ws.send(JSON.stringify({
490
+ jsonrpc: "2.0",
491
+ method: "chat.tool.result",
492
+ params: {
493
+ conversation_id: conversationId,
494
+ result: event.result,
495
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
496
+ }
497
+ }));
498
+ } catch {
499
+ ws.send(JSON.stringify({
500
+ jsonrpc: "2.0",
501
+ method: "chat.response.chunk",
502
+ params: {
503
+ conversation_id: conversationId,
504
+ content: line + `
505
+ `,
506
+ is_final: !1,
507
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
508
+ }
509
+ }));
510
+ }
511
+ }
512
+ buffer.trim() && ws.send(JSON.stringify({
513
+ jsonrpc: "2.0",
514
+ method: "chat.response.chunk",
515
+ params: {
516
+ conversation_id: conversationId,
517
+ content: buffer,
518
+ is_final: !0,
519
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
520
+ }
521
+ }));
522
+ const exitCode = await proc.exited;
523
+ this.activeChatProcesses.delete(conversationId), ws.send(JSON.stringify({
524
+ jsonrpc: "2.0",
525
+ result: {
526
+ conversation_id: conversationId,
527
+ status: exitCode === 0 ? "complete" : "error",
528
+ exit_code: exitCode,
529
+ full_response: accumulatedText,
530
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
531
+ },
532
+ id: requestId
533
+ }));
534
+ } catch (error) {
535
+ const errorMsg = error instanceof Error ? error.message : String(error);
536
+ ws.send(JSON.stringify({
537
+ jsonrpc: "2.0",
538
+ error: {
539
+ code: -32603,
540
+ message: `Chat failed: ${errorMsg}`
541
+ },
542
+ id: requestId
543
+ })), this.activeChatProcesses.delete(conversationId);
544
+ try {
545
+ proc.kill();
546
+ } catch {
547
+ }
548
+ }
549
+ })();
550
+ } catch (error) {
551
+ const errorMsg = error instanceof Error ? error.message : String(error);
552
+ ws.send(JSON.stringify({
553
+ jsonrpc: "2.0",
554
+ error: {
555
+ code: -32603,
556
+ message: `Failed to launch Claude Code: ${errorMsg}`
557
+ },
558
+ id: requestId
559
+ }));
560
+ }
561
+ }
562
+ /**
563
+ * Setup file watcher for model changes
564
+ */
565
+ setupFileWatcher() {
566
+ const drPath = `${this.model.rootPath}/.dr`, bunWatch = globalThis.Bun?.watch;
567
+ if (!bunWatch) {
568
+ console.warn("[Watcher] Bun.watch not available, file watching disabled");
569
+ return;
570
+ }
571
+ this.watcher = bunWatch(drPath, {
572
+ recursive: !0,
573
+ onChange: async (_event, path) => {
574
+ process.env.VERBOSE && console.log(`[Watcher] File changed: ${path}`);
575
+ try {
576
+ this.model = await Model.load(this.model.rootPath, { lazyLoad: !1 });
577
+ const modelData = await this.serializeModel(), message = JSON.stringify({
578
+ type: "model.updated",
579
+ data: modelData,
580
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
581
+ });
582
+ for (const client of this.clients)
583
+ try {
584
+ client.send(message);
585
+ } catch (error) {
586
+ if (process.env.DEBUG) {
587
+ const msg = error instanceof Error ? error.message : String(error);
588
+ console.debug(`[Watcher] Failed to send update: ${msg}`);
589
+ }
590
+ }
591
+ } catch (error) {
592
+ const message = error instanceof Error ? error.message : String(error);
593
+ console.error(`[Watcher] Failed to reload model: ${message}`);
594
+ }
595
+ }
596
+ });
597
+ }
598
+ /**
599
+ * Load all JSON schemas from bundled directory
600
+ */
601
+ async loadSchemas() {
602
+ const schemasPath = new URL("../schemas/bundled/", import.meta.url).pathname, schemas = {};
603
+ try {
604
+ const fs = await import("fs/promises"), path = await import("path"), fileList = await fs.readdir(schemasPath);
605
+ for (const file of fileList)
606
+ if (file.endsWith(".schema.json") || file.endsWith(".json")) {
607
+ const filePath = path.join(schemasPath, file), content = await fs.readFile(filePath, "utf-8");
608
+ schemas[file] = JSON.parse(content);
609
+ }
610
+ return schemas;
611
+ } catch (error) {
612
+ throw console.error("Failed to load schemas:", error), new Error("Failed to load schema files");
613
+ }
614
+ }
615
+ /**
616
+ * Load link registry from bundled directory
617
+ */
618
+ async loadLinkRegistry() {
619
+ const linkRegistryPath = new URL("../schemas/bundled/link-registry.json", import.meta.url).pathname;
620
+ try {
621
+ const content = await (await import("fs/promises")).readFile(linkRegistryPath, "utf-8");
622
+ return JSON.parse(content);
623
+ } catch (error) {
624
+ throw console.error("Failed to load link registry:", error), new Error("Link registry not found");
625
+ }
626
+ }
627
+ /**
628
+ * Get viewer HTML
629
+ */
630
+ getViewerHTML() {
631
+ return `<!DOCTYPE html>
632
+ <html>
633
+ <head>
634
+ <meta charset="UTF-8">
635
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
636
+ <title>Documentation Robotics Viewer</title>
637
+ <style>
638
+ * {
639
+ margin: 0;
640
+ padding: 0;
641
+ box-sizing: border-box;
642
+ }
643
+
644
+ body {
645
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
646
+ background: #f5f5f5;
647
+ color: #333;
648
+ }
649
+
650
+ header {
651
+ background: white;
652
+ border-bottom: 1px solid #e0e0e0;
653
+ padding: 20px;
654
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
655
+ }
656
+
657
+ .header-content {
658
+ max-width: 1400px;
659
+ margin: 0 auto;
660
+ display: flex;
661
+ justify-content: space-between;
662
+ align-items: center;
663
+ }
664
+
665
+ h1 {
666
+ font-size: 24px;
667
+ font-weight: 600;
668
+ }
669
+
670
+ .status-badge {
671
+ display: inline-block;
672
+ padding: 6px 12px;
673
+ border-radius: 4px;
674
+ font-size: 12px;
675
+ font-weight: 500;
676
+ text-transform: uppercase;
677
+ letter-spacing: 0.5px;
678
+ }
679
+
680
+ .status-badge.connected {
681
+ background: #4caf50;
682
+ color: white;
683
+ }
684
+
685
+ .status-badge.disconnected {
686
+ background: #f44336;
687
+ color: white;
688
+ }
689
+
690
+ .container {
691
+ max-width: 1400px;
692
+ margin: 0 auto;
693
+ padding: 20px;
694
+ }
695
+
696
+ .content {
697
+ display: grid;
698
+ grid-template-columns: 300px 1fr;
699
+ gap: 20px;
700
+ height: calc(100vh - 100px);
701
+ }
702
+
703
+ .sidebar {
704
+ background: white;
705
+ border-radius: 4px;
706
+ overflow-y: auto;
707
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
708
+ padding: 16px;
709
+ }
710
+
711
+ .main-panel {
712
+ background: white;
713
+ border-radius: 4px;
714
+ overflow-y: auto;
715
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
716
+ padding: 20px;
717
+ }
718
+
719
+ .layer-group {
720
+ margin-bottom: 16px;
721
+ }
722
+
723
+ .layer-header {
724
+ font-weight: 600;
725
+ padding: 8px 0;
726
+ border-bottom: 2px solid #e0e0e0;
727
+ margin-bottom: 8px;
728
+ cursor: pointer;
729
+ user-select: none;
730
+ display: flex;
731
+ align-items: center;
732
+ }
733
+
734
+ .layer-header:hover {
735
+ color: #2196F3;
736
+ }
737
+
738
+ .layer-header::before {
739
+ content: '\u25B6';
740
+ margin-right: 8px;
741
+ display: inline-block;
742
+ transition: transform 0.2s;
743
+ }
744
+
745
+ .layer-header.expanded::before {
746
+ transform: rotate(90deg);
747
+ }
748
+
749
+ .layer-elements {
750
+ margin-left: 16px;
751
+ display: none;
752
+ }
753
+
754
+ .layer-elements.expanded {
755
+ display: block;
756
+ }
757
+
758
+ .element-item {
759
+ padding: 8px;
760
+ margin-bottom: 4px;
761
+ background: #f9f9f9;
762
+ border-left: 3px solid #e0e0e0;
763
+ cursor: pointer;
764
+ border-radius: 2px;
765
+ transition: all 0.2s;
766
+ font-size: 13px;
767
+ }
768
+
769
+ .element-item:hover {
770
+ background: #f0f0f0;
771
+ border-left-color: #2196F3;
772
+ }
773
+
774
+ .element-item.selected {
775
+ background: #e3f2fd;
776
+ border-left-color: #2196F3;
777
+ font-weight: 500;
778
+ }
779
+
780
+ .element-details {
781
+ min-height: 300px;
782
+ }
783
+
784
+ .detail-header {
785
+ border-bottom: 2px solid #e0e0e0;
786
+ padding-bottom: 12px;
787
+ margin-bottom: 16px;
788
+ }
789
+
790
+ .detail-header h2 {
791
+ font-size: 18px;
792
+ margin-bottom: 4px;
793
+ }
794
+
795
+ .detail-header .element-type {
796
+ font-size: 12px;
797
+ color: #999;
798
+ text-transform: uppercase;
799
+ letter-spacing: 0.5px;
800
+ }
801
+
802
+ .detail-section {
803
+ margin-bottom: 16px;
804
+ }
805
+
806
+ .detail-section h3 {
807
+ font-size: 13px;
808
+ font-weight: 600;
809
+ text-transform: uppercase;
810
+ color: #999;
811
+ margin-bottom: 8px;
812
+ letter-spacing: 0.5px;
813
+ }
814
+
815
+ .detail-value {
816
+ font-size: 14px;
817
+ line-height: 1.6;
818
+ color: #333;
819
+ word-break: break-word;
820
+ }
821
+
822
+ .annotations-list {
823
+ max-height: 200px;
824
+ overflow-y: auto;
825
+ border: 1px solid #e0e0e0;
826
+ border-radius: 4px;
827
+ padding: 8px;
828
+ }
829
+
830
+ .annotation-item {
831
+ padding: 8px;
832
+ margin-bottom: 8px;
833
+ background: #fffde7;
834
+ border-left: 3px solid #fbc02d;
835
+ border-radius: 2px;
836
+ font-size: 12px;
837
+ }
838
+
839
+ .annotation-item .author {
840
+ font-weight: 600;
841
+ color: #666;
842
+ }
843
+
844
+ .annotation-item .text {
845
+ margin: 4px 0 0 0;
846
+ color: #333;
847
+ }
848
+
849
+ .annotation-item .timestamp {
850
+ font-size: 11px;
851
+ color: #999;
852
+ margin-top: 4px;
853
+ }
854
+
855
+ .annotation-form {
856
+ display: grid;
857
+ gap: 8px;
858
+ margin-top: 12px;
859
+ }
860
+
861
+ .annotation-form input,
862
+ .annotation-form textarea {
863
+ padding: 8px;
864
+ border: 1px solid #e0e0e0;
865
+ border-radius: 4px;
866
+ font-family: inherit;
867
+ font-size: 12px;
868
+ }
869
+
870
+ .annotation-form textarea {
871
+ resize: vertical;
872
+ min-height: 60px;
873
+ }
874
+
875
+ .annotation-form button {
876
+ padding: 8px 12px;
877
+ background: #2196F3;
878
+ color: white;
879
+ border: none;
880
+ border-radius: 4px;
881
+ font-weight: 500;
882
+ cursor: pointer;
883
+ transition: background 0.2s;
884
+ }
885
+
886
+ .annotation-form button:hover {
887
+ background: #1976D2;
888
+ }
889
+
890
+ .loading {
891
+ text-align: center;
892
+ padding: 40px;
893
+ color: #999;
894
+ }
895
+
896
+ .loading::after {
897
+ content: '';
898
+ display: inline-block;
899
+ width: 16px;
900
+ height: 16px;
901
+ border: 2px solid #f3f3f3;
902
+ border-top: 2px solid #2196F3;
903
+ border-radius: 50%;
904
+ animation: spin 1s linear infinite;
905
+ margin-left: 8px;
906
+ }
907
+
908
+ @keyframes spin {
909
+ 0% { transform: rotate(0deg); }
910
+ 100% { transform: rotate(360deg); }
911
+ }
912
+
913
+ .stats {
914
+ font-size: 12px;
915
+ color: #999;
916
+ margin-bottom: 16px;
917
+ padding-bottom: 12px;
918
+ border-bottom: 1px solid #e0e0e0;
919
+ }
920
+ </style>
921
+ </head>
922
+ <body>
923
+ <header>
924
+ <div class="header-content">
925
+ <div>
926
+ <h1>\u{1F4DA} Documentation Robotics Viewer</h1>
927
+ </div>
928
+ <div class="status-badge connected" id="status">\u25CF Connected</div>
929
+ </div>
930
+ </header>
931
+
932
+ <div class="container">
933
+ <div class="content">
934
+ <div class="sidebar">
935
+ <div class="stats" id="stats">Loading...</div>
936
+ <div id="model-tree">
937
+ <div class="loading">Loading model...</div>
938
+ </div>
939
+ </div>
940
+
941
+ <div class="main-panel">
942
+ <div id="element-details">
943
+ <div class="loading">Select an element to view details</div>
944
+ </div>
945
+ </div>
946
+ </div>
947
+ </div>
948
+
949
+ <script>
950
+ let currentModel = null;
951
+ let selectedElementId = null;
952
+
953
+ const statusEl = document.getElementById('status');
954
+ const modelTreeEl = document.getElementById('model-tree');
955
+ const detailsEl = document.getElementById('element-details');
956
+ const statsEl = document.getElementById('stats');
957
+
958
+ /**
959
+ * Escape HTML special characters to prevent XSS
960
+ * @param {string} text - Text to escape
961
+ * @returns {string} HTML-escaped text
962
+ */
963
+ function escapeHtml(text) {
964
+ if (!text) return '';
965
+ const div = document.createElement('div');
966
+ div.textContent = text;
967
+ return div.innerHTML;
968
+ }
969
+
970
+ const ws = new WebSocket(\`ws://\${window.location.host}/ws\`);
971
+
972
+ ws.addEventListener('open', () => {
973
+ console.log('WebSocket connected');
974
+ updateStatus(true);
975
+ ws.send(JSON.stringify({ type: 'subscribe' }));
976
+ });
977
+
978
+ ws.addEventListener('message', (event) => {
979
+ try {
980
+ const message = JSON.parse(event.data);
981
+
982
+ if (message.type === 'model' || message.type === 'model-update') {
983
+ currentModel = message.data;
984
+ renderModel();
985
+ } else if (message.type === 'annotation') {
986
+ // Update annotations for the element
987
+ renderSelectedElement();
988
+ } else if (message.type === 'error') {
989
+ console.error('WebSocket error:', message.message);
990
+ }
991
+ } catch (error) {
992
+ console.error('Failed to parse message:', error);
993
+ }
994
+ });
995
+
996
+ ws.addEventListener('close', () => {
997
+ console.log('WebSocket disconnected');
998
+ updateStatus(false);
999
+ });
1000
+
1001
+ ws.addEventListener('error', (error) => {
1002
+ console.error('WebSocket error:', error);
1003
+ updateStatus(false);
1004
+ });
1005
+
1006
+ function updateStatus(connected) {
1007
+ if (connected) {
1008
+ statusEl.className = 'status-badge connected';
1009
+ statusEl.textContent = '\u25CF Connected';
1010
+ } else {
1011
+ statusEl.className = 'status-badge disconnected';
1012
+ statusEl.textContent = '\u25CF Disconnected';
1013
+ }
1014
+ }
1015
+
1016
+ function renderModel() {
1017
+ if (!currentModel) return;
1018
+
1019
+ // Update stats
1020
+ const layerCount = Object.keys(currentModel.layers).length;
1021
+ const elementCount = currentModel.totalElements || 0;
1022
+ statsEl.textContent = \`\${layerCount} layers \xB7 \${elementCount} elements\`;
1023
+
1024
+ // Render layers and elements
1025
+ modelTreeEl.innerHTML = '';
1026
+ const layers = Object.entries(currentModel.layers)
1027
+ .sort((a, b) => a[0].localeCompare(b[0]));
1028
+
1029
+ for (const [layerName, layerData] of layers) {
1030
+ const layerDiv = document.createElement('div');
1031
+ layerDiv.className = 'layer-group';
1032
+
1033
+ const headerDiv = document.createElement('div');
1034
+ headerDiv.className = 'layer-header';
1035
+ headerDiv.textContent = layerName;
1036
+ headerDiv.addEventListener('click', () => {
1037
+ headerDiv.classList.toggle('expanded');
1038
+ elementsDiv.classList.toggle('expanded');
1039
+ });
1040
+
1041
+ const elementsDiv = document.createElement('div');
1042
+ elementsDiv.className = 'layer-elements';
1043
+
1044
+ for (const element of layerData.elements) {
1045
+ const elemDiv = document.createElement('div');
1046
+ elemDiv.className = 'element-item';
1047
+ if (element.id === selectedElementId) {
1048
+ elemDiv.classList.add('selected');
1049
+ }
1050
+ elemDiv.innerHTML = \`\${escapeHtml(element.id)}<br><span style="font-size: 11px; color: #999;">\${escapeHtml(element.name)}</span>\`;
1051
+ elemDiv.addEventListener('click', () => {
1052
+ selectedElementId = element.id;
1053
+ document.querySelectorAll('.element-item').forEach(el => el.classList.remove('selected'));
1054
+ elemDiv.classList.add('selected');
1055
+ renderSelectedElement();
1056
+ });
1057
+
1058
+ elementsDiv.appendChild(elemDiv);
1059
+ }
1060
+
1061
+ layerDiv.appendChild(headerDiv);
1062
+ layerDiv.appendChild(elementsDiv);
1063
+ modelTreeEl.appendChild(layerDiv);
1064
+ }
1065
+ }
1066
+
1067
+ function renderSelectedElement() {
1068
+ if (!selectedElementId || !currentModel) {
1069
+ detailsEl.innerHTML = '<div class="loading">Select an element to view details</div>';
1070
+ return;
1071
+ }
1072
+
1073
+ // Find element in model
1074
+ let element = null;
1075
+ for (const layer of Object.values(currentModel.layers)) {
1076
+ element = layer.elements.find(e => e.id === selectedElementId);
1077
+ if (element) break;
1078
+ }
1079
+
1080
+ if (!element) {
1081
+ detailsEl.innerHTML = '<div class="loading">Element not found</div>';
1082
+ return;
1083
+ }
1084
+
1085
+ let html = \`
1086
+ <div class="detail-header">
1087
+ <div class="element-type">\${escapeHtml(element.type)}</div>
1088
+ <h2>\${escapeHtml(element.name)}</h2>
1089
+ <div style="font-size: 12px; color: #666; margin-top: 4px;"><code>\${escapeHtml(element.id)}</code></div>
1090
+ </div>
1091
+ \`;
1092
+
1093
+ if (element.description) {
1094
+ html += \`
1095
+ <div class="detail-section">
1096
+ <h3>Description</h3>
1097
+ <div class="detail-value">\${escapeHtml(element.description)}</div>
1098
+ </div>
1099
+ \`;
1100
+ }
1101
+
1102
+ if (element.properties && Object.keys(element.properties).length > 0) {
1103
+ html += \`
1104
+ <div class="detail-section">
1105
+ <h3>Properties</h3>
1106
+ <div class="detail-value"><pre>\${escapeHtml(JSON.stringify(element.properties, null, 2))}</pre></div>
1107
+ </div>
1108
+ \`;
1109
+ }
1110
+
1111
+ if (element.annotations && element.annotations.length > 0) {
1112
+ html += \`
1113
+ <div class="detail-section">
1114
+ <h3>Annotations</h3>
1115
+ <div class="annotations-list">
1116
+ \${element.annotations.map(ann => \`
1117
+ <div class="annotation-item">
1118
+ <div class="author">\${escapeHtml(ann.author)}</div>
1119
+ <div class="text">\${escapeHtml(ann.text)}</div>
1120
+ <div class="timestamp">\${new Date(ann.timestamp).toLocaleString()}</div>
1121
+ </div>
1122
+ \`).join('')}
1123
+ </div>
1124
+ </div>
1125
+ \`;
1126
+ }
1127
+
1128
+ html += \`
1129
+ <div class="detail-section">
1130
+ <h3>Add Annotation</h3>
1131
+ <form class="annotation-form" onsubmit="addAnnotation(event, '\${escapeHtml(element.id)}')">
1132
+ <input type="text" placeholder="Author name" id="ann-author" required>
1133
+ <textarea placeholder="Annotation text" id="ann-text" required></textarea>
1134
+ <button type="submit">Add Annotation</button>
1135
+ </form>
1136
+ </div>
1137
+ \`;
1138
+
1139
+ detailsEl.innerHTML = html;
1140
+ }
1141
+
1142
+ function addAnnotation(event, elementId) {
1143
+ event.preventDefault();
1144
+
1145
+ const author = document.getElementById('ann-author').value;
1146
+ const text = document.getElementById('ann-text').value;
1147
+
1148
+ if (!author || !text) return;
1149
+
1150
+ // Send to server
1151
+ fetch(\`/api/elements/\${elementId}/annotations\`, {
1152
+ method: 'POST',
1153
+ headers: { 'Content-Type': 'application/json' },
1154
+ body: JSON.stringify({ author, text })
1155
+ })
1156
+ .then(r => r.json())
1157
+ .then(data => {
1158
+ if (data.success) {
1159
+ document.getElementById('ann-author').value = '';
1160
+ document.getElementById('ann-text').value = '';
1161
+ }
1162
+ })
1163
+ .catch(err => console.error('Failed to add annotation:', err));
1164
+ }
1165
+ </script>
1166
+ </body>
1167
+ </html>`;
1168
+ }
1169
+ /**
1170
+ * Start the server
1171
+ */
1172
+ async start(port = 8080) {
1173
+ this.setupFileWatcher(), this._server = serve({
1174
+ port,
1175
+ fetch: this.app.fetch,
1176
+ websocket
1177
+ // Add WebSocket handler from hono/bun
1178
+ }), console.log(`\u2713 Visualization server running at http://localhost:${port}`);
1179
+ }
1180
+ /**
1181
+ * Get the current auth token
1182
+ */
1183
+ getAuthToken() {
1184
+ return this.authToken;
1185
+ }
1186
+ /**
1187
+ * Check if authentication is enabled
1188
+ */
1189
+ isAuthEnabled() {
1190
+ return this.authEnabled;
1191
+ }
1192
+ /**
1193
+ * Stop the server
1194
+ */
1195
+ stop() {
1196
+ for (const client of this.clients)
1197
+ try {
1198
+ client.close();
1199
+ } catch {
1200
+ }
1201
+ this.clients.clear(), this.watcher && this.watcher.close?.(), this._server && this._server.stop();
1202
+ }
1203
+ }
1204
+ export {
1205
+ VisualizationServer
1206
+ };
1207
+ //# sourceMappingURL=server.js.map