chainlesschain 0.37.12 → 0.40.1

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 (48) hide show
  1. package/package.json +3 -2
  2. package/src/commands/agent.js +7 -1
  3. package/src/commands/ask.js +24 -9
  4. package/src/commands/chat.js +7 -1
  5. package/src/commands/cli-anything.js +266 -0
  6. package/src/commands/compliance.js +216 -0
  7. package/src/commands/dao.js +312 -0
  8. package/src/commands/dlp.js +278 -0
  9. package/src/commands/evomap.js +558 -0
  10. package/src/commands/hardening.js +230 -0
  11. package/src/commands/matrix.js +168 -0
  12. package/src/commands/nostr.js +185 -0
  13. package/src/commands/pqc.js +162 -0
  14. package/src/commands/scim.js +218 -0
  15. package/src/commands/serve.js +109 -0
  16. package/src/commands/siem.js +156 -0
  17. package/src/commands/social.js +480 -0
  18. package/src/commands/terraform.js +148 -0
  19. package/src/constants.js +1 -0
  20. package/src/index.js +60 -0
  21. package/src/lib/autonomous-agent.js +487 -0
  22. package/src/lib/cli-anything-bridge.js +379 -0
  23. package/src/lib/cli-context-engineering.js +472 -0
  24. package/src/lib/compliance-manager.js +290 -0
  25. package/src/lib/content-recommender.js +205 -0
  26. package/src/lib/dao-governance.js +296 -0
  27. package/src/lib/dlp-engine.js +304 -0
  28. package/src/lib/evomap-client.js +135 -0
  29. package/src/lib/evomap-federation.js +240 -0
  30. package/src/lib/evomap-governance.js +250 -0
  31. package/src/lib/evomap-manager.js +227 -0
  32. package/src/lib/git-integration.js +1 -1
  33. package/src/lib/hardening-manager.js +275 -0
  34. package/src/lib/llm-providers.js +14 -1
  35. package/src/lib/matrix-bridge.js +196 -0
  36. package/src/lib/nostr-bridge.js +195 -0
  37. package/src/lib/permanent-memory.js +370 -0
  38. package/src/lib/plan-mode.js +211 -0
  39. package/src/lib/pqc-manager.js +196 -0
  40. package/src/lib/scim-manager.js +212 -0
  41. package/src/lib/session-manager.js +38 -0
  42. package/src/lib/siem-exporter.js +137 -0
  43. package/src/lib/social-manager.js +283 -0
  44. package/src/lib/task-model-selector.js +232 -0
  45. package/src/lib/terraform-manager.js +201 -0
  46. package/src/lib/ws-server.js +474 -0
  47. package/src/repl/agent-repl.js +796 -41
  48. package/src/repl/chat-repl.js +14 -6
@@ -0,0 +1,474 @@
1
+ /**
2
+ * ChainlessChain WebSocket Server
3
+ *
4
+ * Exposes CLI commands over WebSocket for remote access by IDE plugins,
5
+ * web frontends, automation scripts, etc. Commands are executed by spawning
6
+ * child processes — all 60+ CLI commands are available immediately.
7
+ */
8
+
9
+ import { EventEmitter } from "node:events";
10
+ import { spawn } from "node:child_process";
11
+ import { fileURLToPath } from "node:url";
12
+ import { dirname, join } from "node:path";
13
+ import { WebSocketServer } from "ws";
14
+
15
+ const __filename = fileURLToPath(import.meta.url);
16
+ const __dirname = dirname(__filename);
17
+
18
+ /** Absolute path to the CLI entry point */
19
+ const BIN_PATH = join(__dirname, "..", "..", "bin", "chainlesschain.js");
20
+
21
+ /** Commands that must not be executed via WebSocket */
22
+ const BLOCKED_COMMANDS = new Set(["serve", "chat", "agent", "setup"]);
23
+
24
+ /** Heartbeat interval (ms) */
25
+ const HEARTBEAT_INTERVAL = 30_000;
26
+
27
+ /**
28
+ * Tokenize a command string into an array of arguments.
29
+ * Handles double-quoted and single-quoted strings. Does NOT invoke a shell.
30
+ */
31
+ export function tokenizeCommand(input) {
32
+ const args = [];
33
+ let current = "";
34
+ let inDouble = false;
35
+ let inSingle = false;
36
+ let escape = false;
37
+
38
+ for (const ch of input) {
39
+ if (escape) {
40
+ current += ch;
41
+ escape = false;
42
+ continue;
43
+ }
44
+ if (ch === "\\" && inDouble) {
45
+ escape = true;
46
+ continue;
47
+ }
48
+ if (ch === '"' && !inSingle) {
49
+ inDouble = !inDouble;
50
+ continue;
51
+ }
52
+ if (ch === "'" && !inDouble) {
53
+ inSingle = !inSingle;
54
+ continue;
55
+ }
56
+ if ((ch === " " || ch === "\t") && !inDouble && !inSingle) {
57
+ if (current.length > 0) {
58
+ args.push(current);
59
+ current = "";
60
+ }
61
+ continue;
62
+ }
63
+ current += ch;
64
+ }
65
+ if (current.length > 0) {
66
+ args.push(current);
67
+ }
68
+ return args;
69
+ }
70
+
71
+ export class ChainlessChainWSServer extends EventEmitter {
72
+ /**
73
+ * @param {object} options
74
+ * @param {number} [options.port=18800]
75
+ * @param {string} [options.host="127.0.0.1"]
76
+ * @param {string} [options.token] - If set, clients must authenticate first
77
+ * @param {number} [options.maxConnections=10]
78
+ * @param {number} [options.timeout=30000] - Command execution timeout (ms)
79
+ */
80
+ constructor(options = {}) {
81
+ super();
82
+ this.port = options.port || 18800;
83
+ this.host = options.host || "127.0.0.1";
84
+ this.token = options.token || null;
85
+ this.maxConnections = options.maxConnections || 10;
86
+ this.timeout = options.timeout || 30000;
87
+
88
+ /** @type {WebSocketServer|null} */
89
+ this.wss = null;
90
+
91
+ /** Connected clients: clientId → { ws, authenticated, connectedAt } */
92
+ this.clients = new Map();
93
+
94
+ /** Running child processes: requestId → ChildProcess */
95
+ this.processes = new Map();
96
+
97
+ this._heartbeatTimer = null;
98
+ this._clientCounter = 0;
99
+ }
100
+
101
+ /** Start the WebSocket server */
102
+ start() {
103
+ return new Promise((resolve, reject) => {
104
+ this.wss = new WebSocketServer({
105
+ port: this.port,
106
+ host: this.host,
107
+ });
108
+
109
+ this.wss.on("listening", () => {
110
+ this._startHeartbeat();
111
+ this.emit("listening", { port: this.port, host: this.host });
112
+ resolve();
113
+ });
114
+
115
+ this.wss.on("error", (err) => {
116
+ this.emit("error", err);
117
+ reject(err);
118
+ });
119
+
120
+ this.wss.on("connection", (ws, req) => this._handleConnection(ws, req));
121
+ });
122
+ }
123
+
124
+ /** Stop the server and clean up */
125
+ async stop() {
126
+ if (this._heartbeatTimer) {
127
+ clearInterval(this._heartbeatTimer);
128
+ this._heartbeatTimer = null;
129
+ }
130
+
131
+ // Kill all running child processes
132
+ for (const [id, child] of this.processes) {
133
+ try {
134
+ child.kill("SIGTERM");
135
+ } catch (_err) {
136
+ // Process may have already exited
137
+ }
138
+ this.processes.delete(id);
139
+ }
140
+
141
+ // Close all client connections
142
+ for (const [, client] of this.clients) {
143
+ try {
144
+ client.ws.close(1001, "Server shutting down");
145
+ } catch (_err) {
146
+ // Connection may already be closed
147
+ }
148
+ }
149
+ this.clients.clear();
150
+
151
+ // Close the server
152
+ if (this.wss) {
153
+ await new Promise((resolve) => {
154
+ this.wss.close(() => resolve());
155
+ });
156
+ this.wss = null;
157
+ }
158
+
159
+ this.emit("stopped");
160
+ }
161
+
162
+ /** @private */
163
+ _handleConnection(ws, req) {
164
+ if (this.clients.size >= this.maxConnections) {
165
+ ws.close(1013, "Max connections reached");
166
+ return;
167
+ }
168
+
169
+ const clientId = `client-${++this._clientCounter}`;
170
+ const clientIp =
171
+ req.socket.remoteAddress || req.headers["x-forwarded-for"] || "unknown";
172
+
173
+ this.clients.set(clientId, {
174
+ ws,
175
+ authenticated: !this.token, // If no token required, auto-authenticated
176
+ connectedAt: Date.now(),
177
+ ip: clientIp,
178
+ alive: true,
179
+ });
180
+
181
+ this.emit("connection", { clientId, ip: clientIp });
182
+
183
+ ws.on("message", (data) => {
184
+ try {
185
+ const message = JSON.parse(data.toString("utf8"));
186
+ this._handleMessage(clientId, ws, message);
187
+ } catch (_err) {
188
+ this._send(ws, {
189
+ type: "error",
190
+ code: "INVALID_JSON",
191
+ message: "Failed to parse message as JSON",
192
+ });
193
+ }
194
+ });
195
+
196
+ ws.on("close", () => {
197
+ this.clients.delete(clientId);
198
+ this.emit("disconnection", { clientId });
199
+ });
200
+
201
+ ws.on("pong", () => {
202
+ const client = this.clients.get(clientId);
203
+ if (client) client.alive = true;
204
+ });
205
+ }
206
+
207
+ /** @private */
208
+ _handleMessage(clientId, ws, message) {
209
+ const { id, type } = message;
210
+
211
+ if (!id) {
212
+ this._send(ws, {
213
+ type: "error",
214
+ code: "MISSING_ID",
215
+ message: 'Message must include an "id" field',
216
+ });
217
+ return;
218
+ }
219
+
220
+ // Check authentication
221
+ const client = this.clients.get(clientId);
222
+ if (this.token && !client.authenticated && type !== "auth") {
223
+ this._send(ws, {
224
+ id,
225
+ type: "error",
226
+ code: "AUTH_REQUIRED",
227
+ message: "Authentication required. Send an auth message first.",
228
+ });
229
+ return;
230
+ }
231
+
232
+ switch (type) {
233
+ case "auth":
234
+ this._handleAuth(clientId, ws, message);
235
+ break;
236
+ case "ping":
237
+ this._send(ws, { id, type: "pong", serverTime: Date.now() });
238
+ break;
239
+ case "execute":
240
+ this._executeCommand(id, ws, message.command, false);
241
+ break;
242
+ case "stream":
243
+ this._executeCommand(id, ws, message.command, true);
244
+ break;
245
+ case "cancel":
246
+ this._cancelRequest(id, ws);
247
+ break;
248
+ default:
249
+ this._send(ws, {
250
+ id,
251
+ type: "error",
252
+ code: "UNKNOWN_TYPE",
253
+ message: `Unknown message type: ${type}`,
254
+ });
255
+ }
256
+ }
257
+
258
+ /** @private */
259
+ _handleAuth(clientId, ws, message) {
260
+ const { id, token } = message;
261
+ const success = token === this.token;
262
+ const client = this.clients.get(clientId);
263
+
264
+ if (success && client) {
265
+ client.authenticated = true;
266
+ }
267
+
268
+ this._send(ws, {
269
+ id,
270
+ type: "auth-result",
271
+ success,
272
+ ...(success ? {} : { message: "Invalid token" }),
273
+ });
274
+
275
+ if (!success) {
276
+ // Disconnect after failed auth
277
+ setTimeout(() => ws.close(4001, "Authentication failed"), 100);
278
+ }
279
+ }
280
+
281
+ /** @private */
282
+ _executeCommand(id, ws, command, stream) {
283
+ if (!command || typeof command !== "string") {
284
+ this._send(ws, {
285
+ id,
286
+ type: "error",
287
+ code: "INVALID_COMMAND",
288
+ message: "Command must be a non-empty string",
289
+ });
290
+ return;
291
+ }
292
+
293
+ const args = tokenizeCommand(command.trim());
294
+ if (args.length === 0) {
295
+ this._send(ws, {
296
+ id,
297
+ type: "error",
298
+ code: "INVALID_COMMAND",
299
+ message: "Empty command",
300
+ });
301
+ return;
302
+ }
303
+
304
+ // Block dangerous/interactive commands
305
+ const baseCmd = args[0];
306
+ if (BLOCKED_COMMANDS.has(baseCmd)) {
307
+ this._send(ws, {
308
+ id,
309
+ type: "error",
310
+ code: "COMMAND_BLOCKED",
311
+ message: `Command "${baseCmd}" cannot be executed via WebSocket (interactive or recursive)`,
312
+ });
313
+ return;
314
+ }
315
+
316
+ const child = spawn(process.execPath, [BIN_PATH, ...args], {
317
+ env: {
318
+ ...process.env,
319
+ FORCE_COLOR: "0",
320
+ NO_SPINNER: "1",
321
+ },
322
+ stdio: ["pipe", "pipe", "pipe"],
323
+ windowsHide: true,
324
+ });
325
+
326
+ this.processes.set(id, child);
327
+ this.emit("command:start", { id, command, stream });
328
+
329
+ // Timeout handling
330
+ const timer = setTimeout(() => {
331
+ if (this.processes.has(id)) {
332
+ try {
333
+ child.kill("SIGTERM");
334
+ } catch (_err) {
335
+ // Process may have already exited
336
+ }
337
+ this.processes.delete(id);
338
+ this._send(ws, {
339
+ id,
340
+ type: "error",
341
+ code: "COMMAND_TIMEOUT",
342
+ message: `Command timed out after ${this.timeout}ms`,
343
+ });
344
+ }
345
+ }, this.timeout);
346
+
347
+ if (stream) {
348
+ // Stream mode: send chunks as they arrive
349
+ child.stdout.on("data", (data) => {
350
+ this._send(ws, {
351
+ id,
352
+ type: "stream-data",
353
+ channel: "stdout",
354
+ data: data.toString("utf8"),
355
+ });
356
+ });
357
+
358
+ child.stderr.on("data", (data) => {
359
+ this._send(ws, {
360
+ id,
361
+ type: "stream-data",
362
+ channel: "stderr",
363
+ data: data.toString("utf8"),
364
+ });
365
+ });
366
+
367
+ child.on("close", (exitCode) => {
368
+ clearTimeout(timer);
369
+ this.processes.delete(id);
370
+ this._send(ws, {
371
+ id,
372
+ type: "stream-end",
373
+ exitCode: exitCode ?? 1,
374
+ });
375
+ this.emit("command:end", { id, exitCode });
376
+ });
377
+ } else {
378
+ // Buffered mode: collect all output then send result
379
+ const stdoutChunks = [];
380
+ const stderrChunks = [];
381
+
382
+ child.stdout.on("data", (data) => stdoutChunks.push(data));
383
+ child.stderr.on("data", (data) => stderrChunks.push(data));
384
+
385
+ child.on("close", (exitCode) => {
386
+ clearTimeout(timer);
387
+ this.processes.delete(id);
388
+
389
+ const stdout = Buffer.concat(stdoutChunks).toString("utf8");
390
+ const stderr = Buffer.concat(stderrChunks).toString("utf8");
391
+
392
+ this._send(ws, {
393
+ id,
394
+ type: "result",
395
+ success: exitCode === 0,
396
+ exitCode: exitCode ?? 1,
397
+ stdout,
398
+ stderr,
399
+ });
400
+ this.emit("command:end", { id, exitCode });
401
+ });
402
+ }
403
+
404
+ child.on("error", (err) => {
405
+ clearTimeout(timer);
406
+ this.processes.delete(id);
407
+ this._send(ws, {
408
+ id,
409
+ type: "error",
410
+ code: "SPAWN_ERROR",
411
+ message: err.message,
412
+ });
413
+ });
414
+ }
415
+
416
+ /** @private */
417
+ _cancelRequest(id, ws) {
418
+ const child = this.processes.get(id);
419
+ if (child) {
420
+ try {
421
+ child.kill("SIGTERM");
422
+ } catch (_err) {
423
+ // Process may have already exited
424
+ }
425
+ this.processes.delete(id);
426
+ this._send(ws, {
427
+ id,
428
+ type: "result",
429
+ success: false,
430
+ exitCode: -1,
431
+ stdout: "",
432
+ stderr: "Cancelled by client",
433
+ });
434
+ } else {
435
+ this._send(ws, {
436
+ id,
437
+ type: "error",
438
+ code: "NOT_FOUND",
439
+ message: `No running command with id "${id}"`,
440
+ });
441
+ }
442
+ }
443
+
444
+ /** @private — ping/pong heartbeat to detect dead connections */
445
+ _startHeartbeat() {
446
+ this._heartbeatTimer = setInterval(() => {
447
+ for (const [clientId, client] of this.clients) {
448
+ if (!client.alive) {
449
+ client.ws.terminate();
450
+ this.clients.delete(clientId);
451
+ this.emit("disconnection", { clientId, reason: "heartbeat timeout" });
452
+ continue;
453
+ }
454
+ client.alive = false;
455
+ try {
456
+ client.ws.ping();
457
+ } catch (_err) {
458
+ // Connection may be closing
459
+ }
460
+ }
461
+ }, HEARTBEAT_INTERVAL);
462
+ }
463
+
464
+ /** @private — safe JSON send */
465
+ _send(ws, data) {
466
+ if (ws.readyState === ws.OPEN) {
467
+ try {
468
+ ws.send(JSON.stringify(data));
469
+ } catch (_err) {
470
+ // Connection may have just closed
471
+ }
472
+ }
473
+ }
474
+ }