chainlesschain 0.45.75 → 0.45.76

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.
@@ -0,0 +1,189 @@
1
+ /**
2
+ * GatewayBase — shared foundation for messaging platform gateways.
3
+ *
4
+ * Each gateway (Telegram, Discord, etc.) extends this class.
5
+ * Provides: session-per-chat management, message mapping, rate limiting,
6
+ * and integration with the agent loop.
7
+ *
8
+ * @module gateway-base
9
+ */
10
+
11
+ import { EventEmitter } from "node:events";
12
+
13
+ // ─── Constants ──────────────────────────────────────────────────────
14
+
15
+ const DEFAULT_MAX_RESPONSE_LENGTH = 4000;
16
+ const DEFAULT_RATE_LIMIT_WINDOW = 60000; // 1 minute
17
+ const DEFAULT_RATE_LIMIT_MAX = 20; // messages per window
18
+
19
+ // ─── GatewayBase ────────────────────────────────────────────────────
20
+
21
+ export class GatewayBase extends EventEmitter {
22
+ /**
23
+ * @param {object} options
24
+ * @param {string} options.platform - Platform name (e.g. "telegram", "discord")
25
+ * @param {number} [options.maxResponseLength] - Max chars per response
26
+ * @param {number} [options.rateLimitWindow] - Rate limit window in ms
27
+ * @param {number} [options.rateLimitMax] - Max messages per window
28
+ */
29
+ constructor(options = {}) {
30
+ super();
31
+ this.platform = options.platform || "unknown";
32
+ this.maxResponseLength =
33
+ options.maxResponseLength || DEFAULT_MAX_RESPONSE_LENGTH;
34
+ this.rateLimitWindow = options.rateLimitWindow || DEFAULT_RATE_LIMIT_WINDOW;
35
+ this.rateLimitMax = options.rateLimitMax || DEFAULT_RATE_LIMIT_MAX;
36
+
37
+ /** @type {Map<string, { messages: object[], lastActivity: number }>} */
38
+ this.sessions = new Map();
39
+
40
+ /** @type {Map<string, number[]>} */
41
+ this._rateLimitBuckets = new Map();
42
+
43
+ this._running = false;
44
+ }
45
+
46
+ // ── Lifecycle ───────────────────────────────────────────────────
47
+
48
+ /** Start the gateway. Override in subclass. */
49
+ async start() {
50
+ this._running = true;
51
+ this.emit("started", { platform: this.platform });
52
+ }
53
+
54
+ /** Stop the gateway. Override in subclass. */
55
+ async stop() {
56
+ this._running = false;
57
+ this.sessions.clear();
58
+ this._rateLimitBuckets.clear();
59
+ this.emit("stopped", { platform: this.platform });
60
+ }
61
+
62
+ /** @returns {boolean} */
63
+ isRunning() {
64
+ return this._running;
65
+ }
66
+
67
+ // ── Session management ──────────────────────────────────────────
68
+
69
+ /**
70
+ * Get or create a session for a chat.
71
+ * @param {string} chatId - Platform-specific chat identifier
72
+ * @returns {{ messages: object[], lastActivity: number, isNew: boolean }}
73
+ */
74
+ getOrCreateSession(chatId) {
75
+ if (this.sessions.has(chatId)) {
76
+ const session = this.sessions.get(chatId);
77
+ session.lastActivity = Date.now();
78
+ return { ...session, isNew: false };
79
+ }
80
+
81
+ const session = {
82
+ messages: [],
83
+ lastActivity: Date.now(),
84
+ };
85
+ this.sessions.set(chatId, session);
86
+ return { ...session, isNew: true };
87
+ }
88
+
89
+ /**
90
+ * Add a message to a chat session.
91
+ * @param {string} chatId
92
+ * @param {string} role - "user" | "assistant"
93
+ * @param {string} content
94
+ */
95
+ addMessage(chatId, role, content) {
96
+ const session = this.sessions.get(chatId);
97
+ if (session) {
98
+ session.messages.push({ role, content });
99
+ session.lastActivity = Date.now();
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Clear a chat session.
105
+ * @param {string} chatId
106
+ */
107
+ clearSession(chatId) {
108
+ this.sessions.delete(chatId);
109
+ }
110
+
111
+ /**
112
+ * Get active session count.
113
+ * @returns {number}
114
+ */
115
+ getSessionCount() {
116
+ return this.sessions.size;
117
+ }
118
+
119
+ // ── Rate limiting ───────────────────────────────────────────────
120
+
121
+ /**
122
+ * Check if a chat is rate-limited.
123
+ * @param {string} chatId
124
+ * @returns {boolean}
125
+ */
126
+ isRateLimited(chatId) {
127
+ const now = Date.now();
128
+ const bucket = this._rateLimitBuckets.get(chatId) || [];
129
+ // Clean old entries
130
+ const recent = bucket.filter((ts) => now - ts < this.rateLimitWindow);
131
+ this._rateLimitBuckets.set(chatId, recent);
132
+ return recent.length >= this.rateLimitMax;
133
+ }
134
+
135
+ /**
136
+ * Record a message for rate limiting.
137
+ * @param {string} chatId
138
+ */
139
+ recordMessage(chatId) {
140
+ const bucket = this._rateLimitBuckets.get(chatId) || [];
141
+ bucket.push(Date.now());
142
+ this._rateLimitBuckets.set(chatId, bucket);
143
+ }
144
+
145
+ // ── Message formatting ──────────────────────────────────────────
146
+
147
+ /**
148
+ * Split a long response into chunks.
149
+ * @param {string} text
150
+ * @param {number} [maxLength]
151
+ * @returns {string[]}
152
+ */
153
+ splitResponse(text, maxLength) {
154
+ const limit = maxLength || this.maxResponseLength;
155
+ if (!text || text.length <= limit) return [text || ""];
156
+
157
+ const chunks = [];
158
+ let remaining = text;
159
+ while (remaining.length > 0) {
160
+ if (remaining.length <= limit) {
161
+ chunks.push(remaining);
162
+ break;
163
+ }
164
+ // Try to split at last newline within limit
165
+ let splitIdx = remaining.lastIndexOf("\n", limit);
166
+ if (splitIdx < limit * 0.5) {
167
+ // No good newline split point — split at limit
168
+ splitIdx = limit;
169
+ }
170
+ chunks.push(remaining.substring(0, splitIdx));
171
+ remaining = remaining.substring(splitIdx).trimStart();
172
+ }
173
+ return chunks;
174
+ }
175
+
176
+ // ── Stats ───────────────────────────────────────────────────────
177
+
178
+ /**
179
+ * Get gateway statistics.
180
+ * @returns {{ platform: string, running: boolean, sessions: number }}
181
+ */
182
+ getStats() {
183
+ return {
184
+ platform: this.platform,
185
+ running: this._running,
186
+ sessions: this.sessions.size,
187
+ };
188
+ }
189
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Telegram Formatter — converts agent responses to Telegram-compatible markup.
3
+ *
4
+ * Telegram uses a subset of Markdown (MarkdownV2) with different escaping rules.
5
+ *
6
+ * @module telegram-formatter
7
+ */
8
+
9
+ // Characters that must be escaped in MarkdownV2
10
+ const ESCAPE_CHARS = /([_*[\]()~`>#+\-=|{}.!])/g;
11
+
12
+ /**
13
+ * Escape text for Telegram MarkdownV2.
14
+ * @param {string} text
15
+ * @returns {string}
16
+ */
17
+ export function escapeMarkdownV2(text) {
18
+ if (!text) return "";
19
+ return text.replace(ESCAPE_CHARS, "\\$1");
20
+ }
21
+
22
+ /**
23
+ * Convert standard Markdown to Telegram MarkdownV2.
24
+ * Handles: bold, italic, code blocks, inline code, links.
25
+ * @param {string} markdown
26
+ * @returns {string}
27
+ */
28
+ export function toTelegramMarkdown(markdown) {
29
+ if (!markdown) return "";
30
+
31
+ let result = markdown;
32
+
33
+ // Preserve code blocks (don't escape inside them)
34
+ const codeBlocks = [];
35
+ result = result.replace(/```[\s\S]*?```/g, (match) => {
36
+ codeBlocks.push(match);
37
+ return `__CODE_BLOCK_${codeBlocks.length - 1}__`;
38
+ });
39
+
40
+ // Preserve inline code
41
+ const inlineCode = [];
42
+ result = result.replace(/`[^`]+`/g, (match) => {
43
+ inlineCode.push(match);
44
+ return `__INLINE_CODE_${inlineCode.length - 1}__`;
45
+ });
46
+
47
+ // Escape special characters in text
48
+ result = escapeMarkdownV2(result);
49
+
50
+ // Restore code blocks and inline code (unescaped)
51
+ for (let i = codeBlocks.length - 1; i >= 0; i--) {
52
+ result = result.replace(
53
+ `__CODE_BLOCK_${i}__`.replace(ESCAPE_CHARS, "\\$1"),
54
+ codeBlocks[i],
55
+ );
56
+ }
57
+ for (let i = inlineCode.length - 1; i >= 0; i--) {
58
+ result = result.replace(
59
+ `__INLINE_CODE_${i}__`.replace(ESCAPE_CHARS, "\\$1"),
60
+ inlineCode[i],
61
+ );
62
+ }
63
+
64
+ return result;
65
+ }
66
+
67
+ /**
68
+ * Format an agent response for Telegram (plain text fallback).
69
+ * Strips complex markdown, keeps it readable.
70
+ * @param {string} response
71
+ * @param {object} [options]
72
+ * @param {number} [options.maxLength=4000]
73
+ * @returns {string}
74
+ */
75
+ export function formatForTelegram(response, options = {}) {
76
+ if (!response) return "";
77
+ const maxLength = options.maxLength || 4000;
78
+
79
+ let text = response;
80
+
81
+ // Convert headers to bold
82
+ text = text.replace(/^#{1,6}\s+(.+)$/gm, "*$1*");
83
+
84
+ // Keep code blocks as-is (Telegram supports ```)
85
+ // Keep bold (**text** → *text*)
86
+ text = text.replace(/\*\*(.+?)\*\*/g, "*$1*");
87
+
88
+ if (text.length > maxLength) {
89
+ text = text.substring(0, maxLength - 3) + "...";
90
+ }
91
+
92
+ return text;
93
+ }
package/src/index.js CHANGED
@@ -47,6 +47,7 @@ import { registerA2aCommand } from "./commands/a2a.js";
47
47
  // Phase 7: Security & Evolution
48
48
  import { registerSandboxCommand } from "./commands/sandbox.js";
49
49
  import { registerEvolutionCommand } from "./commands/evolution.js";
50
+ import { registerLearningCommand } from "./commands/learning.js";
50
51
 
51
52
  // Phase 7: EvoMap Federation + DAO Governance
52
53
  import { registerDaoCommand } from "./commands/dao.js";
@@ -168,6 +169,7 @@ export function createProgram() {
168
169
  // Phase 7: Security & Evolution
169
170
  registerSandboxCommand(program);
170
171
  registerEvolutionCommand(program);
172
+ registerLearningCommand(program);
171
173
 
172
174
  // Phase 7: EvoMap Federation + DAO Governance
173
175
  registerDaoCommand(program);
@@ -4,6 +4,8 @@
4
4
  */
5
5
 
6
6
  import crypto from "crypto";
7
+ import fs from "fs";
8
+ import path from "path";
7
9
 
8
10
  /** @type {Map<string, object>} In-memory app cache */
9
11
  const _apps = new Map();
@@ -220,14 +222,12 @@ export function saveDesign(db, appId, design) {
220
222
  ).run(versionId, appId, newVersion, JSON.stringify(design));
221
223
 
222
224
  if (!_versions.has(appId)) _versions.set(appId, []);
223
- _versions
224
- .get(appId)
225
- .push({
226
- id: versionId,
227
- app_id: appId,
228
- version: newVersion,
229
- snapshot: design,
230
- });
225
+ _versions.get(appId).push({
226
+ id: versionId,
227
+ app_id: appId,
228
+ version: newVersion,
229
+ snapshot: design,
230
+ });
231
231
 
232
232
  if (_apps.has(appId)) {
233
233
  _apps.get(appId).design = design;
@@ -375,3 +375,131 @@ export function listApps(db) {
375
375
  updated_at: r.updated_at,
376
376
  }));
377
377
  }
378
+
379
+ /**
380
+ * Get a single application by ID from the database.
381
+ *
382
+ * @param {object} db
383
+ * @param {string} appId
384
+ * @returns {object|null}
385
+ */
386
+ export function getApp(db, appId) {
387
+ const row = db.prepare(`SELECT * FROM lowcode_apps WHERE id = ?`).get(appId);
388
+ if (!row) return null;
389
+ return {
390
+ id: row.id,
391
+ name: row.name,
392
+ description: row.description,
393
+ design: row.design
394
+ ? JSON.parse(row.design)
395
+ : { components: [], layout: {} },
396
+ status: row.status,
397
+ version: row.version,
398
+ platform: row.platform,
399
+ };
400
+ }
401
+
402
+ /**
403
+ * Generate a static HTML bundle string for an app's design.
404
+ *
405
+ * @param {object} app - App object with design
406
+ * @returns {{ "index.html": string, "app.js": string, "style.css": string }}
407
+ */
408
+ function generateStaticBundle(app) {
409
+ const design = app.design || { components: [], layout: {} };
410
+ const components = design.components || [];
411
+
412
+ const componentHtml = components
413
+ .map(
414
+ (c, i) =>
415
+ ` <div class="lc-component lc-${(c.type || c.name || "widget").toLowerCase()}" id="comp-${i}">${c.label || c.type || c.name || "Component"}</div>`,
416
+ )
417
+ .join("\n");
418
+
419
+ const html = `<!DOCTYPE html>
420
+ <html lang="en">
421
+ <head>
422
+ <meta charset="UTF-8">
423
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
424
+ <title>${app.name || "Low-Code App"}</title>
425
+ <link rel="stylesheet" href="style.css">
426
+ </head>
427
+ <body>
428
+ <div id="app">
429
+ <h1>${app.name || "Low-Code App"}</h1>
430
+ <p>${app.description || ""}</p>
431
+ <div class="lc-container">
432
+ ${componentHtml || " <p>No components defined</p>"}
433
+ </div>
434
+ </div>
435
+ <script src="app.js"><\/script>
436
+ </body>
437
+ </html>`;
438
+
439
+ const css = `/* Generated by ChainlessChain Low-Code Platform */
440
+ * { margin: 0; padding: 0; box-sizing: border-box; }
441
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; padding: 24px; background: #f5f5f5; }
442
+ #app { max-width: 1200px; margin: 0 auto; }
443
+ h1 { margin-bottom: 8px; color: #1a1a1a; }
444
+ p { margin-bottom: 16px; color: #666; }
445
+ .lc-container { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 16px; }
446
+ .lc-component { background: #fff; border: 1px solid #e0e0e0; border-radius: 8px; padding: 16px; }
447
+ `;
448
+
449
+ const js = `// Generated by ChainlessChain Low-Code Platform
450
+ // App: ${app.name || "Untitled"} v${app.version || 1}
451
+ (function() {
452
+ console.log("Low-Code App initialized: ${app.name || "Untitled"}");
453
+ var config = ${JSON.stringify({ id: app.id, name: app.name, version: app.version, platform: app.platform })};
454
+ document.querySelectorAll(".lc-component").forEach(function(el) {
455
+ el.addEventListener("click", function() { el.classList.toggle("active"); });
456
+ });
457
+ })();
458
+ `;
459
+
460
+ return { "index.html": html, "app.js": js, "style.css": css };
461
+ }
462
+
463
+ /**
464
+ * Deploy an application by generating a static bundle and writing it to disk.
465
+ *
466
+ * @param {object} db
467
+ * @param {string} appId
468
+ * @param {{ outputDir?: string }} options
469
+ * @returns {{ appId: string, outputDir: string, files: string[], deployedAt: string }}
470
+ */
471
+ export function deployApp(db, appId, options = {}) {
472
+ const app = getApp(db, appId);
473
+ if (!app) {
474
+ throw new Error(`App '${appId}' not found`);
475
+ }
476
+ if (app.status !== "published") {
477
+ throw new Error(
478
+ `App '${appId}' must be published before deploying (current status: ${app.status})`,
479
+ );
480
+ }
481
+
482
+ const outputDir =
483
+ options.outputDir ||
484
+ path.join(process.cwd(), ".chainlesschain", "deploys", appId);
485
+ fs.mkdirSync(outputDir, { recursive: true });
486
+
487
+ const bundle = generateStaticBundle(app);
488
+ const fileNames = [];
489
+ for (const [fileName, content] of Object.entries(bundle)) {
490
+ fs.writeFileSync(path.join(outputDir, fileName), content, "utf-8");
491
+ fileNames.push(fileName);
492
+ }
493
+
494
+ // Update app status to deployed
495
+ db.prepare(
496
+ `UPDATE lowcode_apps SET status = ?, updated_at = datetime('now') WHERE id = ?`,
497
+ ).run("deployed", appId);
498
+
499
+ if (_apps.has(appId)) {
500
+ _apps.get(appId).status = "deployed";
501
+ }
502
+
503
+ const deployedAt = new Date().toISOString();
504
+ return { appId, outputDir, files: fileNames, deployedAt };
505
+ }
@@ -52,11 +52,18 @@ export class CLIAutonomousAgent extends EventEmitter {
52
52
  /**
53
53
  * Initialize with required dependencies.
54
54
  */
55
- initialize({ llmChat, toolExecutor, hookManager, maxIterations } = {}) {
55
+ initialize({
56
+ llmChat,
57
+ toolExecutor,
58
+ hookManager,
59
+ maxIterations,
60
+ iterationBudget,
61
+ } = {}) {
56
62
  this._llmChat = llmChat || null;
57
63
  this._toolExecutor = toolExecutor || null;
58
64
  this._hookManager = hookManager || null;
59
65
  if (maxIterations) this._maxIterations = maxIterations;
66
+ this._iterationBudget = iterationBudget || null; // shared budget from caller
60
67
  this._initialized = true;
61
68
  }
62
69
 
@@ -12,6 +12,7 @@ import { generateInstinctPrompt } from "./instinct-manager.js";
12
12
  import { recallMemory } from "./hierarchical-memory.js";
13
13
  import { BM25Search } from "./bm25-search.js";
14
14
  import { createHash } from "crypto";
15
+ import { readUserProfile } from "./user-profile.js";
15
16
 
16
17
  // Exported for test injection
17
18
  export const _deps = {
@@ -19,6 +20,7 @@ export const _deps = {
19
20
  recallMemory,
20
21
  BM25Search,
21
22
  createHash,
23
+ readUserProfile,
22
24
  };
23
25
 
24
26
  // ─── System prompt cleaning regexes (match desktop KV-Cache optimization) ───
@@ -105,6 +107,19 @@ export class CLIContextEngineering {
105
107
  }
106
108
  }
107
109
 
110
+ // 2b. User profile injection (USER.md)
111
+ try {
112
+ const profile = _deps.readUserProfile();
113
+ if (profile) {
114
+ result.push({
115
+ role: "system",
116
+ content: `## User Profile\n${profile}`,
117
+ });
118
+ }
119
+ } catch (_err) {
120
+ // User profile injection failed — skip silently
121
+ }
122
+
108
123
  // 3. Memory injection (scoped: higher threshold, namespace-aware)
109
124
  if (this.db && userQuery) {
110
125
  try {